From ea09b9c4703bee5119839b06f3ea615f47f27bf9 Mon Sep 17 00:00:00 2001 From: 409 <409dev@protonmail.com> Date: Wed, 30 Jul 2025 23:35:30 +0200 Subject: [PATCH] copy files --- .../domain/warren/models/file/requests/cp.rs | 57 +++++++++++++ .../domain/warren/models/file/requests/mod.rs | 2 + .../domain/warren/models/warren/requests.rs | 68 ++++++++++++++++ .../src/lib/domain/warren/ports/metrics.rs | 9 +++ backend/src/lib/domain/warren/ports/mod.rs | 26 ++++-- .../src/lib/domain/warren/ports/notifier.rs | 15 +++- .../src/lib/domain/warren/ports/repository.rs | 7 +- backend/src/lib/domain/warren/service/auth.rs | 55 +++++++++++-- .../lib/domain/warren/service/file_system.rs | 21 ++++- .../src/lib/domain/warren/service/warren.rs | 29 ++++++- .../lib/inbound/http/handlers/warrens/mod.rs | 9 ++- .../http/handlers/warrens/warren_cp.rs | 80 +++++++++++++++++++ .../warrens/{warren_move.rs => warren_mv.rs} | 8 +- backend/src/lib/outbound/file_system.rs | 30 +++++-- .../src/lib/outbound/metrics_debug_logger.rs | 21 +++++ .../src/lib/outbound/notifier_debug_logger.rs | 27 ++++++- frontend/components/DirectoryEntry.vue | 30 ++++++- .../components/DirectoryListContextMenu.vue | 32 +++++++- frontend/lib/api/warrens.ts | 35 ++++++++ frontend/stores/copy.ts | 18 +++++ frontend/utils/files.ts | 19 ++++- 21 files changed, 552 insertions(+), 46 deletions(-) create mode 100644 backend/src/lib/domain/warren/models/file/requests/cp.rs create mode 100644 backend/src/lib/inbound/http/handlers/warrens/warren_cp.rs rename backend/src/lib/inbound/http/handlers/warrens/{warren_move.rs => warren_mv.rs} (91%) create mode 100644 frontend/stores/copy.ts diff --git a/backend/src/lib/domain/warren/models/file/requests/cp.rs b/backend/src/lib/domain/warren/models/file/requests/cp.rs new file mode 100644 index 0000000..786cbaf --- /dev/null +++ b/backend/src/lib/domain/warren/models/file/requests/cp.rs @@ -0,0 +1,57 @@ +use thiserror::Error; + +use crate::domain::warren::models::file::AbsoluteFilePath; + +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct CpRequest { + path: AbsoluteFilePath, + target_path: AbsoluteFilePath, +} + +impl CpRequest { + pub fn new(path: AbsoluteFilePath, target_path: AbsoluteFilePath) -> Self { + Self { path, target_path } + } + + pub fn path(&self) -> &AbsoluteFilePath { + &self.path + } + + pub fn target_path(&self) -> &AbsoluteFilePath { + &self.target_path + } + + pub fn into_paths(self) -> (AbsoluteFilePath, AbsoluteFilePath) { + (self.path, self.target_path) + } +} + +#[derive(Debug, Error)] +pub enum CpError { + #[error("The file does not exist")] + NotFound, + #[error("The target path already exists")] + AlreadyExists, + #[error(transparent)] + Unknown(#[from] anyhow::Error), +} + +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct CpResponse { + path: AbsoluteFilePath, + target_path: AbsoluteFilePath, +} + +impl CpResponse { + pub fn new(path: AbsoluteFilePath, target_path: AbsoluteFilePath) -> Self { + Self { path, target_path } + } + + pub fn path(&self) -> &AbsoluteFilePath { + &self.path + } + + pub fn target_path(&self) -> &AbsoluteFilePath { + &self.target_path + } +} diff --git a/backend/src/lib/domain/warren/models/file/requests/mod.rs b/backend/src/lib/domain/warren/models/file/requests/mod.rs index e919807..9aed375 100644 --- a/backend/src/lib/domain/warren/models/file/requests/mod.rs +++ b/backend/src/lib/domain/warren/models/file/requests/mod.rs @@ -1,4 +1,5 @@ mod cat; +mod cp; mod ls; mod mkdir; mod mv; @@ -7,6 +8,7 @@ mod save; mod touch; pub use cat::*; +pub use cp::*; pub use ls::*; pub use mkdir::*; pub use mv::*; diff --git a/backend/src/lib/domain/warren/models/warren/requests.rs b/backend/src/lib/domain/warren/models/warren/requests.rs index 5119901..2cbb393 100644 --- a/backend/src/lib/domain/warren/models/warren/requests.rs +++ b/backend/src/lib/domain/warren/models/warren/requests.rs @@ -4,6 +4,9 @@ use futures_util::StreamExt; use thiserror::Error; use uuid::Uuid; +use crate::domain::warren::models::file::CpError; +use crate::domain::warren::models::file::CpRequest; +use crate::domain::warren::models::file::CpResponse; use crate::domain::warren::models::file::LsResponse; use crate::domain::warren::models::file::SaveResponse; use crate::domain::warren::models::file::{ @@ -673,3 +676,68 @@ impl WarrenTouchResponse { &self.path } } + +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct WarrenCpRequest { + warren_id: Uuid, + base: CpRequest, +} + +impl WarrenCpRequest { + pub fn new(warren_id: Uuid, base: CpRequest) -> Self { + Self { warren_id, base } + } + + pub fn warren_id(&self) -> &Uuid { + &self.warren_id + } + + pub fn base(&self) -> &CpRequest { + &self.base + } + + pub fn build_fs_request(self, warren: &Warren) -> CpRequest { + let (base_path, base_target_path) = self.base.into_paths(); + + let path = warren.path().clone().join(&base_path.to_relative()); + let target_path = warren.path().clone().join(&base_target_path.to_relative()); + + CpRequest::new(path, target_path) + } +} + +impl Into for &WarrenCpRequest { + fn into(self) -> FetchWarrenRequest { + FetchWarrenRequest::new(self.warren_id) + } +} + +#[derive(Debug, Error)] +pub enum WarrenCpError { + #[error(transparent)] + FetchWarren(#[from] FetchWarrenError), + #[error(transparent)] + FileSystem(#[from] CpError), + #[error(transparent)] + Unknown(#[from] anyhow::Error), +} + +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct WarrenCpResponse { + warren: Warren, + base: CpResponse, +} + +impl WarrenCpResponse { + pub fn new(warren: Warren, base: CpResponse) -> Self { + Self { warren, base } + } + + pub fn warren(&self) -> &Warren { + &self.warren + } + + pub fn base(&self) -> &CpResponse { + &self.base + } +} diff --git a/backend/src/lib/domain/warren/ports/metrics.rs b/backend/src/lib/domain/warren/ports/metrics.rs index b347780..edf121e 100644 --- a/backend/src/lib/domain/warren/ports/metrics.rs +++ b/backend/src/lib/domain/warren/ports/metrics.rs @@ -39,6 +39,9 @@ pub trait WarrenMetrics: Clone + Send + Sync + 'static { fn record_warren_touch_success(&self) -> impl Future + Send; fn record_warren_touch_failure(&self) -> impl Future + Send; + + fn record_warren_cp_success(&self) -> impl Future + Send; + fn record_warren_cp_failure(&self) -> impl Future + Send; } pub trait FileSystemMetrics: Clone + Send + Sync + 'static { @@ -62,6 +65,9 @@ pub trait FileSystemMetrics: Clone + Send + Sync + 'static { fn record_touch_success(&self) -> impl Future + Send; fn record_touch_failure(&self) -> impl Future + Send; + + fn record_cp_success(&self) -> impl Future + Send; + fn record_cp_failure(&self) -> impl Future + Send; } pub trait AuthMetrics: Clone + Send + Sync + 'static { @@ -139,4 +145,7 @@ pub trait AuthMetrics: Clone + Send + Sync + 'static { fn record_auth_warren_touch_success(&self) -> impl Future + Send; fn record_auth_warren_touch_failure(&self) -> impl Future + Send; + + fn record_auth_warren_cp_success(&self) -> impl Future + Send; + fn record_auth_warren_cp_failure(&self) -> impl Future + Send; } diff --git a/backend/src/lib/domain/warren/ports/mod.rs b/backend/src/lib/domain/warren/ports/mod.rs index 133f950..9437434 100644 --- a/backend/src/lib/domain/warren/ports/mod.rs +++ b/backend/src/lib/domain/warren/ports/mod.rs @@ -15,9 +15,9 @@ use super::models::{ }, }, file::{ - CatError, CatRequest, FileStream, LsError, LsRequest, LsResponse, MkdirError, MkdirRequest, - MvError, MvRequest, RmError, RmRequest, SaveError, SaveRequest, SaveResponse, TouchError, - TouchRequest, + CatError, CatRequest, CpError, CpRequest, CpResponse, FileStream, LsError, LsRequest, + LsResponse, MkdirError, MkdirRequest, MvError, MvRequest, RmError, RmRequest, SaveError, + SaveRequest, SaveResponse, TouchError, TouchRequest, }, user::{ CreateUserError, CreateUserRequest, DeleteUserError, DeleteUserRequest, EditUserError, @@ -37,11 +37,11 @@ use super::models::{ CreateWarrenError, CreateWarrenRequest, DeleteWarrenError, DeleteWarrenRequest, EditWarrenError, EditWarrenRequest, FetchWarrenError, FetchWarrenRequest, FetchWarrensError, FetchWarrensRequest, ListWarrensError, ListWarrensRequest, Warren, - WarrenCatError, WarrenCatRequest, WarrenLsError, WarrenLsRequest, WarrenLsResponse, - WarrenMkdirError, WarrenMkdirRequest, WarrenMkdirResponse, WarrenMvError, WarrenMvRequest, - WarrenMvResponse, WarrenRmError, WarrenRmRequest, WarrenRmResponse, WarrenSaveError, - WarrenSaveRequest, WarrenSaveResponse, WarrenTouchError, WarrenTouchRequest, - WarrenTouchResponse, + WarrenCatError, WarrenCatRequest, WarrenCpError, WarrenCpRequest, WarrenCpResponse, + WarrenLsError, WarrenLsRequest, WarrenLsResponse, WarrenMkdirError, WarrenMkdirRequest, + WarrenMkdirResponse, WarrenMvError, WarrenMvRequest, WarrenMvResponse, WarrenRmError, + WarrenRmRequest, WarrenRmResponse, WarrenSaveError, WarrenSaveRequest, WarrenSaveResponse, + WarrenTouchError, WarrenTouchRequest, WarrenTouchResponse, }, }; @@ -100,6 +100,10 @@ pub trait WarrenService: Clone + Send + Sync + 'static { &self, request: WarrenTouchRequest, ) -> impl Future> + Send; + fn warren_cp( + &self, + request: WarrenCpRequest, + ) -> impl Future> + Send; } pub trait FileSystemService: Clone + Send + Sync + 'static { @@ -114,6 +118,7 @@ pub trait FileSystemService: Clone + Send + Sync + 'static { request: SaveRequest, ) -> impl Future> + Send; fn touch(&self, request: TouchRequest) -> impl Future> + Send; + fn cp(&self, request: CpRequest) -> impl Future> + Send; } pub trait AuthService: Clone + Send + Sync + 'static { @@ -252,4 +257,9 @@ pub trait AuthService: Clone + Send + Sync + 'static { request: AuthRequest, warren_service: &WS, ) -> impl Future>> + Send; + fn auth_warren_cp( + &self, + request: AuthRequest, + warren_service: &WS, + ) -> impl Future>> + Send; } diff --git a/backend/src/lib/domain/warren/ports/notifier.rs b/backend/src/lib/domain/warren/ports/notifier.rs index d024b4e..ad66ed4 100644 --- a/backend/src/lib/domain/warren/ports/notifier.rs +++ b/backend/src/lib/domain/warren/ports/notifier.rs @@ -6,8 +6,8 @@ use crate::domain::warren::models::{ user::{ListAllUsersAndWarrensResponse, LoginUserResponse, User}, user_warren::UserWarren, warren::{ - Warren, WarrenLsResponse, WarrenMkdirResponse, WarrenMvResponse, WarrenRmResponse, - WarrenSaveResponse, WarrenTouchResponse, + Warren, WarrenCpResponse, WarrenLsResponse, WarrenMkdirResponse, WarrenMvResponse, + WarrenRmResponse, WarrenSaveResponse, WarrenTouchResponse, }, }; @@ -43,6 +43,7 @@ pub trait WarrenNotifier: Clone + Send + Sync + 'static { warren: &Warren, path: &AbsoluteFilePath, ) -> impl Future + Send; + fn warren_cp(&self, response: &WarrenCpResponse) -> impl Future + Send; } pub trait FileSystemNotifier: Clone + Send + Sync + 'static { @@ -57,6 +58,11 @@ pub trait FileSystemNotifier: Clone + Send + Sync + 'static { ) -> impl Future + Send; fn save(&self, path: &AbsoluteFilePath) -> impl Future + Send; fn touch(&self, path: &AbsoluteFilePath) -> impl Future + Send; + fn cp( + &self, + path: &AbsoluteFilePath, + target_path: &AbsoluteFilePath, + ) -> impl Future + Send; } pub trait AuthNotifier: Clone + Send + Sync + 'static { @@ -160,4 +166,9 @@ pub trait AuthNotifier: Clone + Send + Sync + 'static { user: &User, response: &WarrenTouchResponse, ) -> impl Future + Send; + fn auth_warren_cp( + &self, + user: &User, + response: &WarrenCpResponse, + ) -> impl Future + Send; } diff --git a/backend/src/lib/domain/warren/ports/repository.rs b/backend/src/lib/domain/warren/ports/repository.rs index 911fb2e..1e10375 100644 --- a/backend/src/lib/domain/warren/ports/repository.rs +++ b/backend/src/lib/domain/warren/ports/repository.rs @@ -7,9 +7,9 @@ use crate::domain::warren::models::{ }, }, file::{ - CatError, CatRequest, FileStream, LsError, LsRequest, LsResponse, MkdirError, MkdirRequest, - MvError, MvRequest, RmError, RmRequest, SaveError, SaveRequest, SaveResponse, TouchError, - TouchRequest, + CatError, CatRequest, CpError, CpRequest, CpResponse, FileStream, LsError, LsRequest, + LsResponse, MkdirError, MkdirRequest, MvError, MvRequest, RmError, RmRequest, SaveError, + SaveRequest, SaveResponse, TouchError, TouchRequest, }, user::{ CreateUserError, CreateUserRequest, DeleteUserError, DeleteUserRequest, EditUserError, @@ -77,6 +77,7 @@ pub trait FileSystemRepository: Clone + Send + Sync + 'static { request: SaveRequest, ) -> impl Future> + Send; fn touch(&self, request: TouchRequest) -> impl Future> + Send; + fn cp(&self, request: CpRequest) -> impl Future> + Send; } pub trait AuthRepository: Clone + Send + Sync + 'static { diff --git a/backend/src/lib/domain/warren/service/auth.rs b/backend/src/lib/domain/warren/service/auth.rs index 2f2c836..f48fa38 100644 --- a/backend/src/lib/domain/warren/service/auth.rs +++ b/backend/src/lib/domain/warren/service/auth.rs @@ -28,12 +28,12 @@ use crate::{ warren::{ CreateWarrenError, CreateWarrenRequest, DeleteWarrenError, DeleteWarrenRequest, EditWarrenError, EditWarrenRequest, FetchWarrenError, FetchWarrenRequest, - FetchWarrensRequest, Warren, WarrenCatError, WarrenCatRequest, WarrenLsError, - WarrenLsRequest, WarrenLsResponse, WarrenMkdirError, WarrenMkdirRequest, - WarrenMkdirResponse, WarrenMvError, WarrenMvRequest, WarrenMvResponse, - WarrenRmError, WarrenRmRequest, WarrenRmResponse, WarrenSaveError, - WarrenSaveRequest, WarrenSaveResponse, WarrenTouchError, WarrenTouchRequest, - WarrenTouchResponse, + FetchWarrensRequest, Warren, WarrenCatError, WarrenCatRequest, WarrenCpError, + WarrenCpRequest, WarrenCpResponse, WarrenLsError, WarrenLsRequest, + WarrenLsResponse, WarrenMkdirError, WarrenMkdirRequest, WarrenMkdirResponse, + WarrenMvError, WarrenMvRequest, WarrenMvResponse, WarrenRmError, WarrenRmRequest, + WarrenRmResponse, WarrenSaveError, WarrenSaveRequest, WarrenSaveResponse, + WarrenTouchError, WarrenTouchRequest, WarrenTouchResponse, }, }, ports::{AuthMetrics, AuthNotifier, AuthRepository, AuthService, WarrenService}, @@ -839,12 +839,51 @@ where .map_err(AuthError::Custom); if let Ok(response) = result.as_ref() { - self.metrics.record_auth_warren_save_success().await; + self.metrics.record_auth_warren_touch_success().await; self.notifier .auth_warren_touch(session_response.user(), response) .await; } else { - self.metrics.record_auth_warren_save_failure().await; + self.metrics.record_auth_warren_touch_failure().await; + } + + result + } + + async fn auth_warren_cp( + &self, + request: AuthRequest, + warren_service: &WS, + ) -> Result> { + let session_response = self.fetch_auth_session((&request).into()).await?; + + let request = request.into_value(); + + let user_warren = self + .repository + .fetch_user_warren(FetchUserWarrenRequest::new( + session_response.user().id().clone(), + request.warren_id().clone(), + )) + .await?; + + // TODO: Maybe create a separate permission for this + if !user_warren.can_modify_files() { + return Err(AuthError::InsufficientPermissions); + } + + let result = warren_service + .warren_cp(request) + .await + .map_err(AuthError::Custom); + + if let Ok(response) = result.as_ref() { + self.metrics.record_auth_warren_cp_success().await; + self.notifier + .auth_warren_cp(session_response.user(), response) + .await; + } else { + self.metrics.record_auth_warren_cp_failure().await; } result diff --git a/backend/src/lib/domain/warren/service/file_system.rs b/backend/src/lib/domain/warren/service/file_system.rs index e8a388d..e8147f5 100644 --- a/backend/src/lib/domain/warren/service/file_system.rs +++ b/backend/src/lib/domain/warren/service/file_system.rs @@ -1,8 +1,8 @@ use crate::domain::warren::{ models::file::{ - CatError, CatRequest, FileStream, LsError, LsRequest, LsResponse, MkdirError, MkdirRequest, - MvError, MvRequest, RmError, RmRequest, SaveError, SaveRequest, SaveResponse, TouchError, - TouchRequest, + CatError, CatRequest, CpError, CpRequest, CpResponse, FileStream, LsError, LsRequest, + LsResponse, MkdirError, MkdirRequest, MvError, MvRequest, RmError, RmRequest, SaveError, + SaveRequest, SaveResponse, TouchError, TouchRequest, }, ports::{FileSystemMetrics, FileSystemNotifier, FileSystemRepository, FileSystemService}, }; @@ -137,4 +137,19 @@ where result } + + async fn cp(&self, request: CpRequest) -> Result { + let path = request.path().clone(); + let target_path = request.target_path().clone(); + let result = self.repository.cp(request).await; + + if result.is_ok() { + self.metrics.record_cp_success().await; + self.notifier.cp(&path, &target_path).await; + } else { + self.metrics.record_cp_failure().await; + } + + result + } } diff --git a/backend/src/lib/domain/warren/service/warren.rs b/backend/src/lib/domain/warren/service/warren.rs index bde4585..94167fe 100644 --- a/backend/src/lib/domain/warren/service/warren.rs +++ b/backend/src/lib/domain/warren/service/warren.rs @@ -4,10 +4,10 @@ use crate::domain::warren::{ warren::{ CreateWarrenError, CreateWarrenRequest, DeleteWarrenError, DeleteWarrenRequest, EditWarrenError, EditWarrenRequest, FetchWarrensError, FetchWarrensRequest, - ListWarrensError, ListWarrensRequest, WarrenCatError, WarrenCatRequest, - WarrenLsResponse, WarrenMkdirResponse, WarrenMvError, WarrenMvRequest, - WarrenMvResponse, WarrenRmRequest, WarrenRmResponse, WarrenSaveResponse, - WarrenTouchError, WarrenTouchRequest, WarrenTouchResponse, + ListWarrensError, ListWarrensRequest, WarrenCatError, WarrenCatRequest, WarrenCpError, + WarrenCpRequest, WarrenCpResponse, WarrenLsResponse, WarrenMkdirResponse, + WarrenMvError, WarrenMvRequest, WarrenMvResponse, WarrenRmRequest, WarrenRmResponse, + WarrenSaveResponse, WarrenTouchError, WarrenTouchRequest, WarrenTouchResponse, }, }, ports::FileSystemService, @@ -314,4 +314,25 @@ where result } + + async fn warren_cp(&self, request: WarrenCpRequest) -> Result { + let warren = self.repository.fetch_warren((&request).into()).await?; + + let cp_request = request.build_fs_request(&warren); + let result = self + .fs_service + .cp(cp_request) + .await + .map(|base| WarrenCpResponse::new(warren, base)) + .map_err(Into::into); + + if let Ok(response) = result.as_ref() { + self.metrics.record_warren_cp_success().await; + self.notifier.warren_cp(response).await; + } else { + self.metrics.record_warren_cp_failure().await; + } + + result + } } diff --git a/backend/src/lib/inbound/http/handlers/warrens/mod.rs b/backend/src/lib/inbound/http/handlers/warrens/mod.rs index 4e637be..cbcab8c 100644 --- a/backend/src/lib/inbound/http/handlers/warrens/mod.rs +++ b/backend/src/lib/inbound/http/handlers/warrens/mod.rs @@ -2,9 +2,10 @@ mod fetch_warren; mod list_warrens; mod upload_warren_files; mod warren_cat; +mod warren_cp; mod warren_ls; mod warren_mkdir; -mod warren_move; +mod warren_mv; mod warren_rm; use axum::{ @@ -27,7 +28,8 @@ use warren_rm::warren_rm; use upload_warren_files::warren_save; use warren_cat::fetch_file; -use warren_move::warren_move; +use warren_cp::warren_cp; +use warren_mv::warren_mv; pub fn routes() -> Router> { Router::new() @@ -42,5 +44,6 @@ pub fn routes() -> Router> // 10737418240 bytes = 10GB post(warren_save).route_layer(DefaultBodyLimit::max(10737418240)), ) - .route("/files/mv", post(warren_move)) + .route("/files/mv", post(warren_mv)) + .route("/files/cp", post(warren_cp)) } diff --git a/backend/src/lib/inbound/http/handlers/warrens/warren_cp.rs b/backend/src/lib/inbound/http/handlers/warrens/warren_cp.rs new file mode 100644 index 0000000..5b9b202 --- /dev/null +++ b/backend/src/lib/inbound/http/handlers/warrens/warren_cp.rs @@ -0,0 +1,80 @@ +use axum::{Json, extract::State, http::StatusCode}; +use serde::Deserialize; +use thiserror::Error; +use uuid::Uuid; + +use crate::{ + domain::warren::{ + models::{ + auth_session::AuthRequest, + file::{AbsoluteFilePath, AbsoluteFilePathError, CpRequest, FilePath, FilePathError}, + warren::WarrenCpRequest, + }, + ports::{AuthService, WarrenService}, + }, + inbound::http::{ + AppState, + handlers::extractors::SessionIdHeader, + responses::{ApiError, ApiSuccess}, + }, +}; + +#[derive(Debug, Clone, PartialEq, Eq, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CpWarrenEntryHttpRequestBody { + warren_id: Uuid, + path: String, + target_path: String, +} + +#[derive(Debug, Clone, Error)] +pub enum ParseWarrenCpHttpRequestError { + #[error(transparent)] + FilePath(#[from] FilePathError), + #[error(transparent)] + AbsoluteFilePath(#[from] AbsoluteFilePathError), +} + +impl CpWarrenEntryHttpRequestBody { + fn try_into_domain(self) -> Result { + let path: AbsoluteFilePath = FilePath::new(&self.path)?.try_into()?; + let target_path: AbsoluteFilePath = FilePath::new(&self.target_path)?.try_into()?; + + Ok(WarrenCpRequest::new( + self.warren_id, + CpRequest::new(path, target_path), + )) + } +} + +impl From for ApiError { + fn from(value: ParseWarrenCpHttpRequestError) -> Self { + match value { + ParseWarrenCpHttpRequestError::FilePath(err) => match err { + FilePathError::InvalidPath => { + ApiError::BadRequest("The file path must be valid".to_string()) + } + }, + ParseWarrenCpHttpRequestError::AbsoluteFilePath(err) => match err { + AbsoluteFilePathError::NotAbsolute => { + ApiError::BadRequest("The file path must be absolute".to_string()) + } + }, + } + } +} + +pub async fn warren_cp( + State(state): State>, + SessionIdHeader(session): SessionIdHeader, + Json(request): Json, +) -> Result, ApiError> { + let domain_request = AuthRequest::new(session, request.try_into_domain()?); + + state + .auth_service + .auth_warren_cp(domain_request, state.warren_service.as_ref()) + .await + .map(|_| ApiSuccess::new(StatusCode::OK, ())) + .map_err(ApiError::from) +} diff --git a/backend/src/lib/inbound/http/handlers/warrens/warren_move.rs b/backend/src/lib/inbound/http/handlers/warrens/warren_mv.rs similarity index 91% rename from backend/src/lib/inbound/http/handlers/warrens/warren_move.rs rename to backend/src/lib/inbound/http/handlers/warrens/warren_mv.rs index 8c2313b..219cf43 100644 --- a/backend/src/lib/inbound/http/handlers/warrens/warren_move.rs +++ b/backend/src/lib/inbound/http/handlers/warrens/warren_mv.rs @@ -21,7 +21,7 @@ use crate::{ #[derive(Debug, Clone, PartialEq, Eq, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct RenameWarrenEntryHttpRequestBody { +pub struct MvWarrenEntryHttpRequestBody { warren_id: Uuid, path: String, target_path: String, @@ -35,7 +35,7 @@ pub enum ParseWarrenMvHttpRequestError { AbsoluteFilePath(#[from] AbsoluteFilePathError), } -impl RenameWarrenEntryHttpRequestBody { +impl MvWarrenEntryHttpRequestBody { fn try_into_domain(self) -> Result { let path: AbsoluteFilePath = FilePath::new(&self.path)?.try_into()?; let target_path: AbsoluteFilePath = FilePath::new(&self.target_path)?.try_into()?; @@ -64,10 +64,10 @@ impl From for ApiError { } } -pub async fn warren_move( +pub async fn warren_mv( State(state): State>, SessionIdHeader(session): SessionIdHeader, - Json(request): Json, + Json(request): Json, ) -> Result, ApiError> { let domain_request = AuthRequest::new(session, request.try_into_domain()?); diff --git a/backend/src/lib/outbound/file_system.rs b/backend/src/lib/outbound/file_system.rs index 3179f9a..418ec03 100644 --- a/backend/src/lib/outbound/file_system.rs +++ b/backend/src/lib/outbound/file_system.rs @@ -14,10 +14,10 @@ use crate::{ domain::warren::{ models::{ file::{ - AbsoluteFilePath, CatError, CatRequest, File, FileMimeType, FileName, FilePath, - FileStream, FileType, LsError, LsRequest, LsResponse, MkdirError, MkdirRequest, - MvError, MvRequest, RelativeFilePath, RmError, RmRequest, SaveError, SaveRequest, - SaveResponse, TouchError, TouchRequest, + AbsoluteFilePath, CatError, CatRequest, CpError, CpRequest, CpResponse, File, + FileMimeType, FileName, FilePath, FileStream, FileType, LsError, LsRequest, + LsResponse, MkdirError, MkdirRequest, MvError, MvRequest, RelativeFilePath, + RmError, RmRequest, SaveError, SaveRequest, SaveResponse, TouchError, TouchRequest, }, warren::UploadFileStream, }, @@ -242,7 +242,20 @@ impl FileSystem { async fn touch(&self, path: &AbsoluteFilePath) -> io::Result<()> { let path = self.get_target_path(path); - tokio::fs::File::create(&path).await.map(|_| ()) + fs::File::create(&path).await.map(|_| ()) + } + + async fn cp( + &self, + path: AbsoluteFilePath, + target_path: AbsoluteFilePath, + ) -> io::Result { + let fs_current_path = self.get_target_path(&path); + let fs_target_path = self.get_target_path(&target_path); + + fs::copy(fs_current_path, fs_target_path).await?; + + Ok(CpResponse::new(path, target_path)) } } @@ -313,6 +326,13 @@ impl FileSystemRepository for FileSystem { let (path, mut stream) = request.unpack(); Ok(self.save(&path, &mut stream).await.map(SaveResponse::new)?) } + + async fn cp(&self, request: CpRequest) -> Result { + let (path, target_path) = request.into_paths(); + self.cp(path, target_path) + .await + .map_err(|e| CpError::Unknown(e.into())) + } } // TODO: Use `DirEntry::metadata` once `target=x86_64-unknown-linux-musl` updates from musl 1.2.3 to 1.2.5 diff --git a/backend/src/lib/outbound/metrics_debug_logger.rs b/backend/src/lib/outbound/metrics_debug_logger.rs index a390b4d..24fac57 100644 --- a/backend/src/lib/outbound/metrics_debug_logger.rs +++ b/backend/src/lib/outbound/metrics_debug_logger.rs @@ -100,6 +100,13 @@ impl WarrenMetrics for MetricsDebugLogger { async fn record_warren_touch_failure(&self) { tracing::debug!("[Metrics] Warren entry touch failed"); } + + async fn record_warren_cp_success(&self) { + tracing::debug!("[Metrics] Warren entry cp succeeded"); + } + async fn record_warren_cp_failure(&self) { + tracing::debug!("[Metrics] Warren entry cp failed"); + } } impl FileSystemMetrics for MetricsDebugLogger { @@ -151,6 +158,13 @@ impl FileSystemMetrics for MetricsDebugLogger { async fn record_touch_failure(&self) { tracing::debug!("[Metrics] Touch failed"); } + + async fn record_cp_success(&self) { + tracing::debug!("[Metrics] Cp succeeded"); + } + async fn record_cp_failure(&self) { + tracing::debug!("[Metrics] Cp failed"); + } } impl AuthMetrics for MetricsDebugLogger { @@ -328,4 +342,11 @@ impl AuthMetrics for MetricsDebugLogger { async fn record_auth_warren_touch_failure(&self) { tracing::debug!("[Metrics] Auth warren touch failed"); } + + async fn record_auth_warren_cp_success(&self) { + tracing::debug!("[Metrics] Auth warren cp succeeded"); + } + async fn record_auth_warren_cp_failure(&self) { + tracing::debug!("[Metrics] Auth warren cp failed"); + } } diff --git a/backend/src/lib/outbound/notifier_debug_logger.rs b/backend/src/lib/outbound/notifier_debug_logger.rs index 8e812a1..69667b7 100644 --- a/backend/src/lib/outbound/notifier_debug_logger.rs +++ b/backend/src/lib/outbound/notifier_debug_logger.rs @@ -7,8 +7,8 @@ use crate::domain::warren::{ user::{ListAllUsersAndWarrensResponse, LoginUserResponse, User}, user_warren::UserWarren, warren::{ - Warren, WarrenLsResponse, WarrenMkdirResponse, WarrenMvResponse, WarrenRmResponse, - WarrenSaveResponse, WarrenTouchResponse, + Warren, WarrenCpResponse, WarrenLsResponse, WarrenMkdirResponse, WarrenMvResponse, + WarrenRmResponse, WarrenSaveResponse, WarrenTouchResponse, }, }, ports::{AuthNotifier, FileSystemNotifier, WarrenNotifier}, @@ -98,6 +98,15 @@ impl WarrenNotifier for NotifierDebugLogger { warren.name() ); } + + async fn warren_cp(&self, response: &WarrenCpResponse) { + tracing::debug!( + "[Notifier] Copied file {} to {} in warren {}", + response.base().path(), + response.base().target_path(), + response.warren().name() + ); + } } impl FileSystemNotifier for NotifierDebugLogger { @@ -128,6 +137,10 @@ impl FileSystemNotifier for NotifierDebugLogger { async fn touch(&self, path: &AbsoluteFilePath) { tracing::debug!("[Notifier] Touched file {}", path); } + + async fn cp(&self, path: &AbsoluteFilePath, target_path: &AbsoluteFilePath) { + tracing::debug!("[Notifier] Copied file {} to {}", path, target_path); + } } impl AuthNotifier for NotifierDebugLogger { @@ -330,4 +343,14 @@ impl AuthNotifier for NotifierDebugLogger { user.id() ) } + + async fn auth_warren_cp(&self, user: &User, response: &WarrenCpResponse) { + tracing::debug!( + "[Notifier] Copied file {} to {} in warren {} for authenticated user {}", + response.base().path(), + response.base().target_path(), + response.warren().name(), + user.id() + ) + } } diff --git a/frontend/components/DirectoryEntry.vue b/frontend/components/DirectoryEntry.vue index cc62c66..e63932c 100644 --- a/frontend/components/DirectoryEntry.vue +++ b/frontend/components/DirectoryEntry.vue @@ -14,6 +14,7 @@ import { import type { DirectoryEntry } from '#shared/types'; const warrenStore = useWarrenStore(); +const copyStore = useCopyStore(); const renameDialog = useRenameDirectoryDialog(); const { entry, disabled } = defineProps<{ @@ -22,6 +23,14 @@ const { entry, disabled } = defineProps<{ }>(); const deleting = ref(false); +const isCopied = computed( + () => + warrenStore.current != null && + copyStore.file != null && + warrenStore.current.warrenId === copyStore.file.warrenId && + warrenStore.current.path === copyStore.file.path && + entry.name === copyStore.file.name +); async function submitDelete(force: boolean = false) { if (warrenStore.current == null) { @@ -89,6 +98,18 @@ function onDragStart(e: DragEvent) { } const onDrop = onDirectoryEntryDrop(entry); + +function onCopy() { + if (warrenStore.current == null) { + return; + } + + copyStore.copyFile( + warrenStore.current.warrenId, + warrenStore.current.path, + entry.name + ); +}