diff --git a/casket-backend/src/files/list.rs b/casket-backend/src/files/list.rs index 5833f2f..f480ddd 100644 --- a/casket-backend/src/files/list.rs +++ b/casket-backend/src/files/list.rs @@ -12,6 +12,7 @@ use std::path::PathBuf; use tokio::fs::{read_dir, DirEntry}; use tracing::{debug, error}; +/// Handler to query a file tree. The query parameters can be used to control what is is queried. #[debug_handler] pub async fn query_file_tree( State(state): State, @@ -20,56 +21,86 @@ pub async fn query_file_tree( ) -> Result, 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, 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), - } + PathElements::load(path, query.nesting) + .await + .map(Json::from) } +/// Query to control how a file tree is queried. #[derive(Deserialize)] pub struct FileQuery { + /// Path which is used as base path to query the tree. path: PathBuf, #[serde(default = "u32::min_value")] + /// The amount of layers the tree should be loaded for. Can be used to cache a certain amount + /// of levels for a better UX when browsing the data. nesting: u32, } +/// Represents the root of a file tree. #[derive(Serialize)] pub struct PathElements { files: Vec, } +/// Represents a node in the file tree. +#[derive(Serialize)] +pub struct PathElement { + /// The name of the directory or file. + name: String, + is_dir: bool, + /// Empty children means that the node is either a file or an empty directory. + /// None means that the children have not been loaded. + children: Option>, +} + impl PathElements { pub fn new(files: Vec) -> Self { Self { files } } -} -#[derive(Serialize)] -pub struct PathElement { - name: String, - is_dir: bool, - children: Option>, -} + /// Loads a path recursively nested for up to the provided nesting parameter. + pub async fn load(path: PathBuf, nesting: u32) -> Result { + Self::load_directory(path, nesting).await.map(Self::new) + } -impl PathElement { - async fn from(value: DirEntry, nesting: u32) -> Result { + async fn load_directory( + path: PathBuf, + nesting: u32, + ) -> Result, ProblemDetails> { + match read_dir(path).await { + Err(error) => Err(Self::map_io_error(error)), + Ok(mut value) => { + let mut result = vec![]; + while let Some(next) = value.next_entry().await.map_err(Self::map_io_error)? { + result.push(Self::load_path_element(next, nesting).await?); + } + Ok(result) + } + } + } + + async fn load_children( + path: PathBuf, + nesting: u32, + is_dir: bool, + ) -> Result>, ProblemDetails> { + if nesting == 0 { + Ok(None) + } else if !is_dir { + Ok(Some(vec![])) + } else { + Ok(Some( + // needs to call pin to support recursion in async contexts. + Box::pin(Self::load_directory(path, nesting - 1)).await?, + )) + } + } + + async fn load_path_element( + value: DirEntry, + nesting: u32, + ) -> Result { let metadata = value.metadata().await.unwrap(); debug!("File type: {:?}", value.file_type().await.unwrap()); let is_dir = metadata.is_dir(); @@ -77,11 +108,11 @@ impl PathElement { Ok(PathElement { name: value.file_name().into_string().unwrap(), is_dir, - children: load_children(value.path(), nesting, is_dir).await?, + children: Self::load_children(value.path(), nesting, is_dir).await?, }) } else { error!( - "File <{:?}> is neither a directory nor a file. ", + "File <{:?}> is neither a directory nor a file.", value.file_name() ); Err( @@ -90,18 +121,11 @@ impl PathElement { ) } } -} -async fn load_children( - path: PathBuf, - nesting: u32, - is_dir: bool, -) -> Result>, ProblemDetails> { - if nesting == 0 { - Ok(None) - } else if !is_dir { - Ok(Some(vec![])) - } else { - Ok(Some(Box::pin(load_directory(path, nesting - 1)).await?)) + 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), + } } } diff --git a/casket-backend/src/files/upload.rs b/casket-backend/src/files/upload.rs index c524792..6ec8db6 100644 --- a/casket-backend/src/files/upload.rs +++ b/casket-backend/src/files/upload.rs @@ -53,7 +53,7 @@ async fn handle_single_file_upload<'field_lifetime>( } } -pub async fn save_file( +async fn save_file( path: PathBuf, mut content: impl Stream> + Unpin, ) -> Result<(), Error> {