diff --git a/backend/src/lib/domain/warren/models/file/requests/ls.rs b/backend/src/lib/domain/warren/models/file/requests/ls.rs index 3c70a58..6486079 100644 --- a/backend/src/lib/domain/warren/models/file/requests/ls.rs +++ b/backend/src/lib/domain/warren/models/file/requests/ls.rs @@ -1,21 +1,29 @@ use thiserror::Error; -use crate::domain::warren::models::file::AbsoluteFilePath; +use crate::domain::warren::models::file::{AbsoluteFilePath, File}; #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct LsRequest { path: AbsoluteFilePath, + include_parent: bool, } impl LsRequest { - pub fn new(path: AbsoluteFilePath) -> Self { - Self { path } + pub fn new(path: AbsoluteFilePath, include_parent: bool) -> Self { + Self { + path, + include_parent, + } } pub fn path(&self) -> &AbsoluteFilePath { &self.path } + pub fn include_parent(&self) -> bool { + self.include_parent + } + pub fn into_path(self) -> AbsoluteFilePath { self.path } @@ -28,3 +36,23 @@ pub enum LsError { #[error(transparent)] Unknown(#[from] anyhow::Error), } + +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct LsResponse { + files: Vec, + parent: Option, +} + +impl LsResponse { + pub fn new(files: Vec, parent: Option) -> Self { + Self { files, parent } + } + + pub fn files(&self) -> &Vec { + &self.files + } + + pub fn parent(&self) -> Option<&File> { + self.parent.as_ref() + } +} diff --git a/backend/src/lib/domain/warren/models/warren/requests.rs b/backend/src/lib/domain/warren/models/warren/requests.rs index 01d8e56..5119901 100644 --- a/backend/src/lib/domain/warren/models/warren/requests.rs +++ b/backend/src/lib/domain/warren/models/warren/requests.rs @@ -4,11 +4,11 @@ use futures_util::StreamExt; use thiserror::Error; use uuid::Uuid; +use crate::domain::warren::models::file::LsResponse; use crate::domain::warren::models::file::SaveResponse; use crate::domain::warren::models::file::{ - AbsoluteFilePath, CatError, CatRequest, File, FileName, LsError, LsRequest, MkdirError, - MkdirRequest, MvError, MvRequest, RmError, RmRequest, SaveError, SaveRequest, TouchError, - TouchRequest, + AbsoluteFilePath, CatError, CatRequest, FileName, LsError, LsRequest, MkdirError, MkdirRequest, + MvError, MvRequest, RmError, RmRequest, SaveError, SaveRequest, TouchError, TouchRequest, }; use super::{Warren, WarrenName}; @@ -77,12 +77,13 @@ impl WarrenLsRequest { } pub fn build_fs_request(self, warren: &Warren) -> LsRequest { + let include_parent = self.base.include_parent(); let path = warren .path() .clone() .join(&self.base.into_path().to_relative()); - LsRequest::new(path) + LsRequest::new(path, include_parent) } } @@ -96,16 +97,12 @@ impl Into for WarrenLsRequest { pub struct WarrenLsResponse { warren: Warren, path: AbsoluteFilePath, - files: Vec, + base: LsResponse, } impl WarrenLsResponse { - pub fn new(warren: Warren, path: AbsoluteFilePath, files: Vec) -> Self { - Self { - warren, - path, - files, - } + pub fn new(warren: Warren, path: AbsoluteFilePath, base: LsResponse) -> Self { + Self { warren, path, base } } pub fn warren(&self) -> &Warren { @@ -116,8 +113,8 @@ impl WarrenLsResponse { &self.path } - pub fn files(&self) -> &Vec { - &self.files + pub fn base(&self) -> &LsResponse { + &self.base } } diff --git a/backend/src/lib/domain/warren/ports/mod.rs b/backend/src/lib/domain/warren/ports/mod.rs index eb5a6e4..133f950 100644 --- a/backend/src/lib/domain/warren/ports/mod.rs +++ b/backend/src/lib/domain/warren/ports/mod.rs @@ -15,7 +15,7 @@ use super::models::{ }, }, file::{ - CatError, CatRequest, File, FileStream, LsError, LsRequest, MkdirError, MkdirRequest, + CatError, CatRequest, FileStream, LsError, LsRequest, LsResponse, MkdirError, MkdirRequest, MvError, MvRequest, RmError, RmRequest, SaveError, SaveRequest, SaveResponse, TouchError, TouchRequest, }, @@ -103,7 +103,7 @@ pub trait WarrenService: Clone + Send + Sync + 'static { } pub trait FileSystemService: Clone + Send + Sync + 'static { - fn ls(&self, request: LsRequest) -> impl Future, LsError>> + Send; + fn ls(&self, request: LsRequest) -> impl Future> + Send; fn cat(&self, request: CatRequest) -> impl Future> + Send; fn mkdir(&self, request: MkdirRequest) -> impl Future> + Send; diff --git a/backend/src/lib/domain/warren/ports/notifier.rs b/backend/src/lib/domain/warren/ports/notifier.rs index e984f07..d024b4e 100644 --- a/backend/src/lib/domain/warren/ports/notifier.rs +++ b/backend/src/lib/domain/warren/ports/notifier.rs @@ -2,7 +2,7 @@ use uuid::Uuid; use crate::domain::warren::models::{ auth_session::requests::FetchAuthSessionResponse, - file::{AbsoluteFilePath, File}, + file::{AbsoluteFilePath, LsResponse}, user::{ListAllUsersAndWarrensResponse, LoginUserResponse, User}, user_warren::UserWarren, warren::{ @@ -46,7 +46,7 @@ pub trait WarrenNotifier: Clone + Send + Sync + 'static { } pub trait FileSystemNotifier: Clone + Send + Sync + 'static { - fn ls(&self, files: &Vec) -> impl Future + Send; + fn ls(&self, response: &LsResponse) -> impl Future + Send; fn cat(&self, path: &AbsoluteFilePath) -> impl Future + Send; fn mkdir(&self, path: &AbsoluteFilePath) -> impl Future + Send; fn rm(&self, path: &AbsoluteFilePath) -> impl Future + Send; diff --git a/backend/src/lib/domain/warren/ports/repository.rs b/backend/src/lib/domain/warren/ports/repository.rs index 5ae904d..911fb2e 100644 --- a/backend/src/lib/domain/warren/ports/repository.rs +++ b/backend/src/lib/domain/warren/ports/repository.rs @@ -7,7 +7,7 @@ use crate::domain::warren::models::{ }, }, file::{ - CatError, CatRequest, File, FileStream, LsError, LsRequest, MkdirError, MkdirRequest, + CatError, CatRequest, FileStream, LsError, LsRequest, LsResponse, MkdirError, MkdirRequest, MvError, MvRequest, RmError, RmRequest, SaveError, SaveRequest, SaveResponse, TouchError, TouchRequest, }, @@ -66,7 +66,7 @@ pub trait WarrenRepository: Clone + Send + Sync + 'static { } pub trait FileSystemRepository: Clone + Send + Sync + 'static { - fn ls(&self, request: LsRequest) -> impl Future, LsError>> + Send; + fn ls(&self, request: LsRequest) -> impl Future> + Send; fn cat(&self, request: CatRequest) -> impl Future> + Send; fn mkdir(&self, request: MkdirRequest) -> impl Future> + Send; diff --git a/backend/src/lib/domain/warren/service/file_system.rs b/backend/src/lib/domain/warren/service/file_system.rs index eea4bcb..e8a388d 100644 --- a/backend/src/lib/domain/warren/service/file_system.rs +++ b/backend/src/lib/domain/warren/service/file_system.rs @@ -1,6 +1,6 @@ use crate::domain::warren::{ models::file::{ - CatError, CatRequest, File, FileStream, LsError, LsRequest, MkdirError, MkdirRequest, + CatError, CatRequest, FileStream, LsError, LsRequest, LsResponse, MkdirError, MkdirRequest, MvError, MvRequest, RmError, RmRequest, SaveError, SaveRequest, SaveResponse, TouchError, TouchRequest, }, @@ -40,7 +40,7 @@ where M: FileSystemMetrics, N: FileSystemNotifier, { - async fn ls(&self, request: LsRequest) -> Result, LsError> { + async fn ls(&self, request: LsRequest) -> Result { let result = self.repository.ls(request).await; if let Ok(files) = result.as_ref() { diff --git a/backend/src/lib/inbound/http/handlers/warrens/warren_ls.rs b/backend/src/lib/inbound/http/handlers/warrens/warren_ls.rs index c7fec2a..5feecd5 100644 --- a/backend/src/lib/inbound/http/handlers/warrens/warren_ls.rs +++ b/backend/src/lib/inbound/http/handlers/warrens/warren_ls.rs @@ -11,7 +11,7 @@ use crate::{ AbsoluteFilePathError, File, FileMimeType, FilePath, FilePathError, FileType, LsRequest, }, - warren::WarrenLsRequest, + warren::{WarrenLsRequest, WarrenLsResponse}, }, ports::{AuthService, WarrenService}, }, @@ -41,7 +41,10 @@ impl WarrenLsHttpRequestBody { fn try_into_domain(self) -> Result { let path = FilePath::new(&self.path)?.try_into()?; - Ok(WarrenLsRequest::new(self.warren_id, LsRequest::new(path))) + Ok(WarrenLsRequest::new( + self.warren_id, + LsRequest::new(path, &self.path != "/"), + )) } } @@ -75,6 +78,7 @@ pub struct WarrenFileElement { #[serde(rename_all = "camelCase")] pub struct ListWarrenFilesResponseData { files: Vec, + parent: Option, } impl From<&File> for WarrenFileElement { @@ -88,10 +92,16 @@ impl From<&File> for WarrenFileElement { } } -impl From<&Vec> for ListWarrenFilesResponseData { - fn from(value: &Vec) -> Self { +impl From for ListWarrenFilesResponseData { + fn from(value: WarrenLsResponse) -> Self { Self { - files: value.iter().map(WarrenFileElement::from).collect(), + files: value + .base() + .files() + .iter() + .map(WarrenFileElement::from) + .collect(), + parent: value.base().parent().map(WarrenFileElement::from), } } } @@ -107,6 +117,6 @@ pub async fn list_warren_files( .auth_service .auth_warren_ls(domain_request, state.warren_service.as_ref()) .await - .map(|response| ApiSuccess::new(StatusCode::OK, response.files().into())) + .map(|response| ApiSuccess::new(StatusCode::OK, response.into())) .map_err(ApiError::from) } diff --git a/backend/src/lib/outbound/file_system.rs b/backend/src/lib/outbound/file_system.rs index dfd2ba3..3179f9a 100644 --- a/backend/src/lib/outbound/file_system.rs +++ b/backend/src/lib/outbound/file_system.rs @@ -15,8 +15,8 @@ use crate::{ models::{ file::{ AbsoluteFilePath, CatError, CatRequest, File, FileMimeType, FileName, FilePath, - FileStream, FileType, LsError, LsRequest, MkdirError, MkdirRequest, MvError, - MvRequest, RelativeFilePath, RmError, RmRequest, SaveError, SaveRequest, + FileStream, FileType, LsError, LsRequest, LsResponse, MkdirError, MkdirRequest, + MvError, MvRequest, RelativeFilePath, RmError, RmRequest, SaveError, SaveRequest, SaveResponse, TouchError, TouchRequest, }, warren::UploadFileStream, @@ -75,13 +75,38 @@ impl FileSystem { self.base_directory.join(&path.as_relative()) } - async fn get_all_files(&self, absolute_path: &AbsoluteFilePath) -> anyhow::Result> { - let directory_path = self.get_target_path(absolute_path); - - let mut dir = fs::read_dir(&directory_path).await?; + async fn get_all_files( + &self, + absolute_path: &AbsoluteFilePath, + include_parent: bool, + ) -> anyhow::Result { + let dir_path = self.get_target_path(absolute_path).as_ref().to_path_buf(); let mut files = Vec::new(); + let parent = if include_parent { + let dir_name = FileName::new( + &dir_path + .file_name() + .context("Failed to get directory name")? + .to_owned() + .into_string() + .ok() + .context("Failed to get directory name")?, + )?; + + Some(File::new( + dir_name, + FileType::Directory, + None, + get_btime(&dir_path), + )) + } else { + None + }; + + let mut dir = fs::read_dir(&dir_path).await?; + while let Ok(Some(entry)) = dir.next_entry().await { let name = entry .file_name() @@ -100,18 +125,7 @@ impl FileSystem { } }; - // TODO: Use `DirEntry::metadata` once `target=x86_64-unknown-linux-musl` updates from musl 1.2.3 to 1.2.5 - // https://github.com/rust-lang/rust/pull/142682 - let created_at = unsafe { - statx( - std::os::fd::BorrowedFd::borrow_raw(-100), - entry.path(), - rustix::fs::AtFlags::empty(), - rustix::fs::StatxFlags::BTIME, - ) - } - .ok() - .map(|statx| statx.stx_btime.tv_sec as u64); + let created_at = get_btime(entry.path()); let mime_type = match file_type { FileType::File => FileMimeType::from_name(&name), @@ -126,7 +140,7 @@ impl FileSystem { )); } - Ok(files) + Ok(LsResponse::new(files, parent)) } /// Actually created a directory in the underlying file system @@ -233,13 +247,16 @@ impl FileSystem { } impl FileSystemRepository for FileSystem { - async fn ls(&self, request: LsRequest) -> Result, LsError> { - let files = self.get_all_files(request.path()).await.map_err(|err| { - anyhow!(err).context(format!( - "Failed to get the files at path: {}", - request.path() - )) - })?; + async fn ls(&self, request: LsRequest) -> Result { + let files = self + .get_all_files(request.path(), request.include_parent()) + .await + .map_err(|err| { + anyhow!(err).context(format!( + "Failed to get the files at path: {}", + request.path() + )) + })?; Ok(files) } @@ -297,3 +314,21 @@ impl FileSystemRepository for FileSystem { Ok(self.save(&path, &mut stream).await.map(SaveResponse::new)?) } } + +// TODO: Use `DirEntry::metadata` once `target=x86_64-unknown-linux-musl` updates from musl 1.2.3 to 1.2.5 +// https://github.com/rust-lang/rust/pull/142682 +fn get_btime

(path: P) -> Option +where + P: rustix::path::Arg, +{ + unsafe { + statx( + std::os::fd::BorrowedFd::borrow_raw(-100), + path, + rustix::fs::AtFlags::empty(), + rustix::fs::StatxFlags::BTIME, + ) + } + .ok() + .map(|statx| statx.stx_btime.tv_sec as u64) +} diff --git a/backend/src/lib/outbound/notifier_debug_logger.rs b/backend/src/lib/outbound/notifier_debug_logger.rs index ae986ff..8e812a1 100644 --- a/backend/src/lib/outbound/notifier_debug_logger.rs +++ b/backend/src/lib/outbound/notifier_debug_logger.rs @@ -3,7 +3,7 @@ use uuid::Uuid; use crate::domain::warren::{ models::{ auth_session::requests::FetchAuthSessionResponse, - file::{AbsoluteFilePath, File}, + file::{AbsoluteFilePath, LsResponse}, user::{ListAllUsersAndWarrensResponse, LoginUserResponse, User}, user_warren::UserWarren, warren::{ @@ -57,7 +57,7 @@ impl WarrenNotifier for NotifierDebugLogger { async fn warren_ls(&self, response: &WarrenLsResponse) { tracing::debug!( "[Notifier] Listed {} file(s) in warren {}", - response.files().len(), + response.base().files().len(), response.warren().name() ); } @@ -101,8 +101,8 @@ impl WarrenNotifier for NotifierDebugLogger { } impl FileSystemNotifier for NotifierDebugLogger { - async fn ls(&self, files: &Vec) { - tracing::debug!("[Notifier] Listed {} file(s)", files.len()); + async fn ls(&self, response: &LsResponse) { + tracing::debug!("[Notifier] Listed {} file(s)", response.files().len()); } async fn cat(&self, path: &AbsoluteFilePath) { @@ -279,7 +279,7 @@ impl AuthNotifier for NotifierDebugLogger { async fn auth_warren_ls(&self, user: &User, response: &WarrenLsResponse) { tracing::debug!( "[Notifier] Listed {} file(s) in warren {} for authenticated user {}", - response.files().len(), + response.base().files().len(), response.warren().name(), user.id() ); diff --git a/frontend/components/DirectoryBackEntry.vue b/frontend/components/DirectoryBackEntry.vue new file mode 100644 index 0000000..8a4f8b4 --- /dev/null +++ b/frontend/components/DirectoryBackEntry.vue @@ -0,0 +1,38 @@ + + + diff --git a/frontend/components/DirectoryEntry.vue b/frontend/components/DirectoryEntry.vue index a3b27ae..413518f 100644 --- a/frontend/components/DirectoryEntry.vue +++ b/frontend/components/DirectoryEntry.vue @@ -89,28 +89,7 @@ function onDragStart(e: DragEvent) { e.dataTransfer.dropEffect = 'move'; } -async function onDrop(e: DragEvent) { - if (e.dataTransfer == null || warrenStore.current == null) { - return; - } - - if (entry.fileType !== 'directory') { - return; - } - - const fileName = e.dataTransfer.getData('application/warren'); - - if (entry.name === fileName) { - return; - } - - await moveFile( - warrenStore.current.warrenId, - warrenStore.current.path, - fileName, - `${warrenStore.current.path}/${entry.name}` - ); -} +const onDrop = onDirectoryEntryDrop(entry);