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}; /// 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, axum::extract::Path(user_id): axum::extract::Path, WithProblemDetails(Query(query)): WithProblemDetails>, ) -> Result, ProblemDetails> { let path = build_system_path(&state.config.files.directory, &user_id, &query.path)?; debug!("Loading path: {:?}", path); 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 } } /// 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) } 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(); if is_dir || metadata.is_file() { Ok(PathElement { name: value.file_name().into_string().unwrap(), is_dir, children: Self::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), ) } } 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), } } }