copy files
This commit is contained in:
57
backend/src/lib/domain/warren/models/file/requests/cp.rs
Normal file
57
backend/src/lib/domain/warren/models/file/requests/cp.rs
Normal file
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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::*;
|
||||
|
||||
@@ -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<FetchWarrenRequest> 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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,6 +39,9 @@ pub trait WarrenMetrics: Clone + Send + Sync + 'static {
|
||||
|
||||
fn record_warren_touch_success(&self) -> impl Future<Output = ()> + Send;
|
||||
fn record_warren_touch_failure(&self) -> impl Future<Output = ()> + Send;
|
||||
|
||||
fn record_warren_cp_success(&self) -> impl Future<Output = ()> + Send;
|
||||
fn record_warren_cp_failure(&self) -> impl Future<Output = ()> + 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<Output = ()> + Send;
|
||||
fn record_touch_failure(&self) -> impl Future<Output = ()> + Send;
|
||||
|
||||
fn record_cp_success(&self) -> impl Future<Output = ()> + Send;
|
||||
fn record_cp_failure(&self) -> impl Future<Output = ()> + 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<Output = ()> + Send;
|
||||
fn record_auth_warren_touch_failure(&self) -> impl Future<Output = ()> + Send;
|
||||
|
||||
fn record_auth_warren_cp_success(&self) -> impl Future<Output = ()> + Send;
|
||||
fn record_auth_warren_cp_failure(&self) -> impl Future<Output = ()> + Send;
|
||||
}
|
||||
|
||||
@@ -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<Output = Result<WarrenTouchResponse, WarrenTouchError>> + Send;
|
||||
fn warren_cp(
|
||||
&self,
|
||||
request: WarrenCpRequest,
|
||||
) -> impl Future<Output = Result<WarrenCpResponse, WarrenCpError>> + Send;
|
||||
}
|
||||
|
||||
pub trait FileSystemService: Clone + Send + Sync + 'static {
|
||||
@@ -114,6 +118,7 @@ pub trait FileSystemService: Clone + Send + Sync + 'static {
|
||||
request: SaveRequest,
|
||||
) -> impl Future<Output = Result<SaveResponse, SaveError>> + Send;
|
||||
fn touch(&self, request: TouchRequest) -> impl Future<Output = Result<(), TouchError>> + Send;
|
||||
fn cp(&self, request: CpRequest) -> impl Future<Output = Result<CpResponse, CpError>> + Send;
|
||||
}
|
||||
|
||||
pub trait AuthService: Clone + Send + Sync + 'static {
|
||||
@@ -252,4 +257,9 @@ pub trait AuthService: Clone + Send + Sync + 'static {
|
||||
request: AuthRequest<WarrenTouchRequest>,
|
||||
warren_service: &WS,
|
||||
) -> impl Future<Output = Result<WarrenTouchResponse, AuthError<WarrenTouchError>>> + Send;
|
||||
fn auth_warren_cp<WS: WarrenService>(
|
||||
&self,
|
||||
request: AuthRequest<WarrenCpRequest>,
|
||||
warren_service: &WS,
|
||||
) -> impl Future<Output = Result<WarrenCpResponse, AuthError<WarrenCpError>>> + Send;
|
||||
}
|
||||
|
||||
@@ -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<Output = ()> + Send;
|
||||
fn warren_cp(&self, response: &WarrenCpResponse) -> impl Future<Output = ()> + Send;
|
||||
}
|
||||
|
||||
pub trait FileSystemNotifier: Clone + Send + Sync + 'static {
|
||||
@@ -57,6 +58,11 @@ pub trait FileSystemNotifier: Clone + Send + Sync + 'static {
|
||||
) -> impl Future<Output = ()> + Send;
|
||||
fn save(&self, path: &AbsoluteFilePath) -> impl Future<Output = ()> + Send;
|
||||
fn touch(&self, path: &AbsoluteFilePath) -> impl Future<Output = ()> + Send;
|
||||
fn cp(
|
||||
&self,
|
||||
path: &AbsoluteFilePath,
|
||||
target_path: &AbsoluteFilePath,
|
||||
) -> impl Future<Output = ()> + 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<Output = ()> + Send;
|
||||
fn auth_warren_cp(
|
||||
&self,
|
||||
user: &User,
|
||||
response: &WarrenCpResponse,
|
||||
) -> impl Future<Output = ()> + Send;
|
||||
}
|
||||
|
||||
@@ -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<Output = Result<SaveResponse, SaveError>> + Send;
|
||||
fn touch(&self, request: TouchRequest) -> impl Future<Output = Result<(), TouchError>> + Send;
|
||||
fn cp(&self, request: CpRequest) -> impl Future<Output = Result<CpResponse, CpError>> + Send;
|
||||
}
|
||||
|
||||
pub trait AuthRepository: Clone + Send + Sync + 'static {
|
||||
|
||||
@@ -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<WS: WarrenService>(
|
||||
&self,
|
||||
request: AuthRequest<WarrenCpRequest>,
|
||||
warren_service: &WS,
|
||||
) -> Result<WarrenCpResponse, AuthError<WarrenCpError>> {
|
||||
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
|
||||
|
||||
@@ -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<CpResponse, CpError> {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<WarrenCpResponse, WarrenCpError> {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<WS: WarrenService, AS: AuthService>() -> Router<AppState<WS, AS>> {
|
||||
Router::new()
|
||||
@@ -42,5 +44,6 @@ pub fn routes<WS: WarrenService, AS: AuthService>() -> Router<AppState<WS, AS>>
|
||||
// 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))
|
||||
}
|
||||
|
||||
80
backend/src/lib/inbound/http/handlers/warrens/warren_cp.rs
Normal file
80
backend/src/lib/inbound/http/handlers/warrens/warren_cp.rs
Normal file
@@ -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<WarrenCpRequest, ParseWarrenCpHttpRequestError> {
|
||||
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<ParseWarrenCpHttpRequestError> 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<WS: WarrenService, AS: AuthService>(
|
||||
State(state): State<AppState<WS, AS>>,
|
||||
SessionIdHeader(session): SessionIdHeader,
|
||||
Json(request): Json<CpWarrenEntryHttpRequestBody>,
|
||||
) -> Result<ApiSuccess<()>, 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)
|
||||
}
|
||||
@@ -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<WarrenMvRequest, ParseWarrenMvHttpRequestError> {
|
||||
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<ParseWarrenMvHttpRequestError> for ApiError {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn warren_move<WS: WarrenService, AS: AuthService>(
|
||||
pub async fn warren_mv<WS: WarrenService, AS: AuthService>(
|
||||
State(state): State<AppState<WS, AS>>,
|
||||
SessionIdHeader(session): SessionIdHeader,
|
||||
Json(request): Json<RenameWarrenEntryHttpRequestBody>,
|
||||
Json(request): Json<MvWarrenEntryHttpRequestBody>,
|
||||
) -> Result<ApiSuccess<()>, ApiError> {
|
||||
let domain_request = AuthRequest::new(session, request.try_into_domain()?);
|
||||
|
||||
@@ -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<CpResponse> {
|
||||
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<CpResponse, CpError> {
|
||||
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
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -96,7 +117,10 @@ const onDrop = onDirectoryEntryDrop(entry);
|
||||
<ContextMenuTrigger>
|
||||
<button
|
||||
:disabled="warrenStore.loading || disabled"
|
||||
class="bg-accent/30 border-border flex w-52 translate-0 flex-row gap-4 overflow-hidden rounded-md border-1 px-4 py-2 select-none"
|
||||
:class="[
|
||||
'bg-accent/30 border-border flex w-52 translate-0 flex-row gap-4 overflow-hidden rounded-md border-1 px-4 py-2 select-none',
|
||||
isCopied && 'border-primary/50 border',
|
||||
]"
|
||||
draggable="true"
|
||||
@dragstart="onDragStart"
|
||||
@drop="onDrop"
|
||||
@@ -124,6 +148,10 @@ const onDrop = onDirectoryEntryDrop(entry);
|
||||
<Icon name="lucide:pencil" />
|
||||
Rename
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem @select="onCopy">
|
||||
<Icon name="lucide:copy" />
|
||||
Copy
|
||||
</ContextMenuItem>
|
||||
|
||||
<ContextMenuSeparator />
|
||||
|
||||
|
||||
@@ -6,11 +6,35 @@ import {
|
||||
ContextMenuItem,
|
||||
} from '@/components/ui/context-menu';
|
||||
|
||||
const dialog = useCreateDirectoryDialog();
|
||||
const warrenStore = useWarrenStore();
|
||||
const copyStore = useCopyStore();
|
||||
const createDirectoryDialog = useCreateDirectoryDialog();
|
||||
|
||||
const pasting = ref<boolean>(false);
|
||||
const validPaste = computed(
|
||||
() =>
|
||||
!pasting.value && copyStore.file != null && warrenStore.current != null
|
||||
);
|
||||
|
||||
const props = defineProps<{
|
||||
class?: string;
|
||||
}>();
|
||||
|
||||
async function onPaste() {
|
||||
if (!validPaste.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
pasting.value = true;
|
||||
|
||||
await pasteFile(copyStore.file!, {
|
||||
warrenId: warrenStore.current!.warrenId,
|
||||
name: copyStore.file!.name,
|
||||
path: warrenStore.current!.path,
|
||||
});
|
||||
|
||||
pasting.value = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -19,7 +43,11 @@ const props = defineProps<{
|
||||
<slot />
|
||||
</ContextMenuTrigger>
|
||||
<ContextMenuContent>
|
||||
<ContextMenuItem @select="dialog.openDialog">
|
||||
<ContextMenuItem @disabled="!validPaste" @select="onPaste">
|
||||
<Icon name="lucide:clipboard-paste" />
|
||||
Paste
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem @select="createDirectoryDialog.openDialog">
|
||||
<Icon name="lucide:folder-plus" />
|
||||
Create directory
|
||||
</ContextMenuItem>
|
||||
|
||||
@@ -335,3 +335,38 @@ export async function moveFile(
|
||||
success: true,
|
||||
};
|
||||
}
|
||||
|
||||
export async function copyFile(
|
||||
warrenId: string,
|
||||
currentPath: string,
|
||||
targetPath: string
|
||||
): Promise<{ success: boolean }> {
|
||||
const { status } = await useFetch(getApiUrl(`warrens/files/cp`), {
|
||||
method: 'POST',
|
||||
headers: getApiHeaders(),
|
||||
body: JSON.stringify({
|
||||
warrenId,
|
||||
path: currentPath,
|
||||
targetPath: targetPath,
|
||||
}),
|
||||
});
|
||||
|
||||
if (status.value !== 'success') {
|
||||
toast.error('Copy', {
|
||||
id: 'COPY_FILE_TOAST',
|
||||
description: `Failed to copy file`,
|
||||
});
|
||||
return { success: false };
|
||||
}
|
||||
|
||||
await refreshNuxtData('current-directory');
|
||||
|
||||
toast.success('Copy', {
|
||||
id: 'COPY_FILE_TOAST',
|
||||
description: `Successfully copied file`,
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
};
|
||||
}
|
||||
|
||||
18
frontend/stores/copy.ts
Normal file
18
frontend/stores/copy.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
export const useCopyStore = defineStore('file-copy', {
|
||||
state: () => ({
|
||||
file: null as { warrenId: string; path: string; name: string } | null,
|
||||
}),
|
||||
actions: {
|
||||
copyFile(warrenId: string, filePath: string, fileName: string) {
|
||||
this.file = {
|
||||
warrenId,
|
||||
path: filePath,
|
||||
name: fileName,
|
||||
};
|
||||
},
|
||||
/** Removes the current file from the "clipboard" */
|
||||
clearFile() {
|
||||
this.file = null;
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -1,4 +1,4 @@
|
||||
import { moveFile } from '~/lib/api/warrens';
|
||||
import { copyFile, moveFile } from '~/lib/api/warrens';
|
||||
import type { DirectoryEntry } from '~/shared/types';
|
||||
|
||||
export function joinPaths(path: string, ...other: string[]): string {
|
||||
@@ -58,3 +58,20 @@ export function onDirectoryEntryDrop(
|
||||
await moveFile(warrenStore.current.warrenId, currentPath, targetPath);
|
||||
};
|
||||
}
|
||||
|
||||
export async function pasteFile(
|
||||
current: { warrenId: string; path: string; name: string },
|
||||
target: { warrenId: string; path: string; name: string }
|
||||
): Promise<boolean> {
|
||||
if (current.warrenId !== target.warrenId) {
|
||||
throw new Error('Cross-warren copies are not supported yet');
|
||||
}
|
||||
|
||||
const { success } = await copyFile(
|
||||
current.warrenId,
|
||||
joinPaths(current.path, current.name),
|
||||
joinPaths(target.path, target.name)
|
||||
);
|
||||
|
||||
return success;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user