implement endpoint to list files in a directory
This commit is contained in:
parent
5023028c50
commit
2eee67bdd1
@ -1,6 +1,17 @@
|
|||||||
|
use axum::http::StatusCode;
|
||||||
|
use problem_details::ProblemDetails;
|
||||||
|
use std::fmt::Debug;
|
||||||
|
use tracing::error;
|
||||||
|
|
||||||
pub const ERROR_DETAILS_PATH_TRAVERSAL: &str = "You must not traverse!";
|
pub const ERROR_DETAILS_PATH_TRAVERSAL: &str = "You must not traverse!";
|
||||||
pub const ERROR_DETAILS_INTERNAL_ERROR: &str =
|
pub const ERROR_DETAILS_INTERNAL_ERROR: &str =
|
||||||
"Internal Error! Check the logs of the backend service for details.";
|
"Internal Error! Check the logs of the backend service for details.";
|
||||||
pub const ERROR_DETAILS_ABSOLUTE_PATH_NOT_ALLOWED: &str = "Absolute paths are not allowed.";
|
pub const ERROR_DETAILS_ABSOLUTE_PATH_NOT_ALLOWED: &str = "Absolute paths are not allowed.";
|
||||||
pub const ERROR_DETAILS_NO_NAME_PROVIDED_MULTIPART_FIELD: &str =
|
pub const ERROR_DETAILS_NO_NAME_PROVIDED_MULTIPART_FIELD: &str =
|
||||||
"No name provided for multipart form field";
|
"No name provided for multipart form field";
|
||||||
|
|
||||||
|
pub fn to_internal_error(error: impl Debug) -> ProblemDetails {
|
||||||
|
error!("{:?}", error);
|
||||||
|
ProblemDetails::from_status_code(StatusCode::INTERNAL_SERVER_ERROR)
|
||||||
|
.with_detail(ERROR_DETAILS_INTERNAL_ERROR)
|
||||||
|
}
|
||||||
|
|||||||
24
casket-backend/src/extractor_helper.rs
Normal file
24
casket-backend/src/extractor_helper.rs
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
use axum::extract::FromRequestParts;
|
||||||
|
use axum::http::request::Parts;
|
||||||
|
use axum::http::StatusCode;
|
||||||
|
use problem_details::ProblemDetails;
|
||||||
|
use std::fmt::Debug;
|
||||||
|
|
||||||
|
pub struct WithProblemDetails<T>(pub T);
|
||||||
|
|
||||||
|
impl<T, S> FromRequestParts<S> for WithProblemDetails<T>
|
||||||
|
where
|
||||||
|
T: FromRequestParts<S>,
|
||||||
|
T::Rejection: Debug,
|
||||||
|
S: Sync,
|
||||||
|
{
|
||||||
|
type Rejection = ProblemDetails;
|
||||||
|
|
||||||
|
async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
|
||||||
|
match T::from_request_parts(parts, state).await {
|
||||||
|
Ok(inner) => Ok(Self(inner)),
|
||||||
|
Err(rejection) => Err(ProblemDetails::from_status_code(StatusCode::BAD_REQUEST)
|
||||||
|
.with_detail(format!("{rejection:?}"))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
107
casket-backend/src/files/list.rs
Normal file
107
casket-backend/src/files/list.rs
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
use crate::errors::to_internal_error;
|
||||||
|
use crate::extractor_helper::WithProblemDetails;
|
||||||
|
use crate::files::build_system_path;
|
||||||
|
use crate::{errors, AppState};
|
||||||
|
use axum::extract::{Query, State};
|
||||||
|
use axum::http::StatusCode;
|
||||||
|
use axum::{debug_handler, Json};
|
||||||
|
use problem_details::ProblemDetails;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::io::Error;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use tokio::fs::{read_dir, DirEntry};
|
||||||
|
use tracing::{debug, error};
|
||||||
|
|
||||||
|
#[debug_handler]
|
||||||
|
pub async fn query_file_tree(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
axum::extract::Path(user_id): axum::extract::Path<String>,
|
||||||
|
WithProblemDetails(Query(query)): WithProblemDetails<Query<FileQuery>>,
|
||||||
|
) -> Result<Json<PathElements>, ProblemDetails> {
|
||||||
|
let path = build_system_path(&state.config.files.directory, &user_id, &query.path)?;
|
||||||
|
debug!("Loading path: {:?}", path);
|
||||||
|
Ok(PathElements::new(load_directory(path, query.nesting).await?).into())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn load_directory(path: PathBuf, nesting: u32) -> Result<Vec<PathElement>, ProblemDetails> {
|
||||||
|
match read_dir(path).await {
|
||||||
|
Err(error) => Err(map_io_error(error)),
|
||||||
|
Ok(mut value) => {
|
||||||
|
let mut result = vec![];
|
||||||
|
while let Some(next) = value.next_entry().await.map_err(map_io_error)? {
|
||||||
|
result.push(PathElement::from(next, nesting).await?);
|
||||||
|
}
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn map_io_error(error: Error) -> ProblemDetails {
|
||||||
|
match error.kind() {
|
||||||
|
std::io::ErrorKind::NotFound => ProblemDetails::from_status_code(StatusCode::NOT_FOUND),
|
||||||
|
_ => to_internal_error(error),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct FileQuery {
|
||||||
|
path: PathBuf,
|
||||||
|
#[serde(default = "u32::min_value")]
|
||||||
|
nesting: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
pub struct PathElements {
|
||||||
|
files: Vec<PathElement>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PathElements {
|
||||||
|
pub fn new(files: Vec<PathElement>) -> Self {
|
||||||
|
Self { files }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
pub struct PathElement {
|
||||||
|
name: String,
|
||||||
|
is_dir: bool,
|
||||||
|
children: Option<Vec<PathElement>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PathElement {
|
||||||
|
async fn from(value: DirEntry, nesting: u32) -> Result<Self, ProblemDetails> {
|
||||||
|
let metadata = value.metadata().await.unwrap();
|
||||||
|
debug!("File type: {:?}", value.file_type().await.unwrap());
|
||||||
|
let is_dir = metadata.is_dir();
|
||||||
|
if is_dir || metadata.is_file() {
|
||||||
|
Ok(PathElement {
|
||||||
|
name: value.file_name().into_string().unwrap(),
|
||||||
|
is_dir,
|
||||||
|
children: load_children(value.path(), nesting, is_dir).await?,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
error!(
|
||||||
|
"File <{:?}> is neither a directory nor a file. ",
|
||||||
|
value.file_name()
|
||||||
|
);
|
||||||
|
Err(
|
||||||
|
ProblemDetails::from_status_code(StatusCode::INTERNAL_SERVER_ERROR)
|
||||||
|
.with_detail(errors::ERROR_DETAILS_INTERNAL_ERROR),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn load_children(
|
||||||
|
path: PathBuf,
|
||||||
|
nesting: u32,
|
||||||
|
is_dir: bool,
|
||||||
|
) -> Result<Option<Vec<PathElement>>, ProblemDetails> {
|
||||||
|
if nesting == 0 {
|
||||||
|
Ok(None)
|
||||||
|
} else if !is_dir {
|
||||||
|
Ok(Some(vec![]))
|
||||||
|
} else {
|
||||||
|
Ok(Some(Box::pin(load_directory(path, nesting - 1)).await?))
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -15,7 +15,7 @@ fn build_system_path(
|
|||||||
Err(
|
Err(
|
||||||
ProblemDetails::from_status_code(StatusCode::BAD_REQUEST).with_detail(
|
ProblemDetails::from_status_code(StatusCode::BAD_REQUEST).with_detail(
|
||||||
errors::ERROR_DETAILS_ABSOLUTE_PATH_NOT_ALLOWED.to_owned()
|
errors::ERROR_DETAILS_ABSOLUTE_PATH_NOT_ALLOWED.to_owned()
|
||||||
+ format!(" Provided Path: {:?}", user_path).as_str(),
|
+ format!(" Provided Path: {user_path:?}",).as_str(),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
use crate::errors::to_internal_error;
|
||||||
use crate::files::build_system_path;
|
use crate::files::build_system_path;
|
||||||
use crate::{errors, AppState};
|
use crate::{errors, AppState};
|
||||||
use axum::body::Bytes;
|
use axum::body::Bytes;
|
||||||
@ -9,13 +10,11 @@ use futures::Stream;
|
|||||||
use futures_util::stream::MapErr;
|
use futures_util::stream::MapErr;
|
||||||
use futures_util::{StreamExt, TryStreamExt};
|
use futures_util::{StreamExt, TryStreamExt};
|
||||||
use problem_details::ProblemDetails;
|
use problem_details::ProblemDetails;
|
||||||
use std::fmt::Debug;
|
|
||||||
use std::io;
|
use std::io;
|
||||||
use std::io::Error;
|
use std::io::Error;
|
||||||
use std::path::{Component, Path, PathBuf};
|
use std::path::PathBuf;
|
||||||
use tokio::fs::{create_dir_all, File};
|
use tokio::fs::{create_dir_all, File};
|
||||||
use tokio::io::AsyncWriteExt;
|
use tokio::io::AsyncWriteExt;
|
||||||
use tracing::error;
|
|
||||||
|
|
||||||
#[debug_handler]
|
#[debug_handler]
|
||||||
pub async fn handle_file_uploads(
|
pub async fn handle_file_uploads(
|
||||||
@ -66,12 +65,6 @@ pub async fn save_file(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn to_internal_error(error: impl Debug) -> ProblemDetails {
|
|
||||||
error!("{:?}", error);
|
|
||||||
ProblemDetails::from_status_code(StatusCode::INTERNAL_SERVER_ERROR)
|
|
||||||
.with_detail(errors::ERROR_DETAILS_INTERNAL_ERROR)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn map_error_to_io_error(field: Field) -> MapErr<Field, fn(MultipartError) -> Error> {
|
fn map_error_to_io_error(field: Field) -> MapErr<Field, fn(MultipartError) -> Error> {
|
||||||
field.map_err(|err| Error::new(io::ErrorKind::Other, err))
|
field.map_err(|err| Error::new(io::ErrorKind::Other, err))
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,6 +4,7 @@ mod config;
|
|||||||
mod errors;
|
mod errors;
|
||||||
mod files;
|
mod files;
|
||||||
mod routes;
|
mod routes;
|
||||||
|
mod extractor_helper;
|
||||||
|
|
||||||
use axum::Router;
|
use axum::Router;
|
||||||
use std::env;
|
use std::env;
|
||||||
@ -11,7 +12,7 @@ use std::process::exit;
|
|||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use tracing::{debug, error, info};
|
use tracing::{debug, error, info};
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct AppState {
|
pub struct AppState {
|
||||||
config: config::Config,
|
config: config::Config,
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,11 +2,13 @@ use crate::files;
|
|||||||
use crate::AppState;
|
use crate::AppState;
|
||||||
use axum::routing::post;
|
use axum::routing::post;
|
||||||
use axum::{response::Html, routing::get, Router};
|
use axum::{response::Html, routing::get, Router};
|
||||||
|
use files::list::query_file_tree;
|
||||||
|
|
||||||
pub fn routes() -> Router<AppState> {
|
pub fn routes() -> Router<AppState> {
|
||||||
Router::new()
|
Router::new().route("/", get(handler)).route(
|
||||||
.route("/", get(handler))
|
"/api/v1/user/{:user_id}/files",
|
||||||
.route("/api/v1/user/{:user_id}/files", post(files::upload::handle_file_uploads))
|
post(files::upload::handle_file_uploads).get(query_file_tree),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handler() -> Html<&'static str> {
|
async fn handler() -> Html<&'static str> {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user