From f81c1b35a80b5a3ecee2a421330d7510cbcdf873 Mon Sep 17 00:00:00 2001 From: brxken128 <77554505+brxken128@users.noreply.github.com> Date: Mon, 16 Jan 2023 17:38:13 +0000 Subject: [PATCH 01/43] basic single file duplication --- core/src/api/files.rs | 11 +++ core/src/object/fs/duplicate.rs | 89 +++++++++++++++++++ core/src/object/fs/mod.rs | 1 + packages/client/src/core.ts | 6 ++ .../explorer/ExplorerContextMenu.tsx | 12 ++- 5 files changed, 118 insertions(+), 1 deletion(-) create mode 100644 core/src/object/fs/duplicate.rs diff --git a/core/src/api/files.rs b/core/src/api/files.rs index d669e5c4a..326725a4f 100644 --- a/core/src/api/files.rs +++ b/core/src/api/files.rs @@ -4,6 +4,7 @@ use crate::{ object::fs::{ decrypt::{FileDecryptorJob, FileDecryptorJobInit}, delete::{FileDeleterJob, FileDeleterJobInit}, + duplicate::{FileDuplicatorJob, FileDuplicatorJobInit}, encrypt::{FileEncryptorJob, FileEncryptorJobInit}, erase::{FileEraserJob, FileEraserJobInit}, }, @@ -122,6 +123,16 @@ pub(crate) fn mount() -> RouterBuilder { library.spawn_job(Job::new(args, FileEraserJob {})).await; invalidate_query!(library, "locations.getExplorerData"); + Ok(()) + }) + }) + .library_mutation("duplicateFiles", |t| { + t(|_, args: FileDuplicatorJobInit, library| async move { + library + .spawn_job(Job::new(args, FileDuplicatorJob {})) + .await; + invalidate_query!(library, "locations.getExplorerData"); + Ok(()) }) }) diff --git a/core/src/object/fs/duplicate.rs b/core/src/object/fs/duplicate.rs new file mode 100644 index 000000000..ef5f44063 --- /dev/null +++ b/core/src/object/fs/duplicate.rs @@ -0,0 +1,89 @@ +use super::{context_menu_fs_info, FsInfo, ObjectType}; +use crate::job::{JobError, JobReportUpdate, JobResult, JobState, StatefulJob, WorkerContext}; +use serde::{Deserialize, Serialize}; +use specta::Type; +use std::{collections::VecDeque, hash::Hash}; + +pub struct FileDuplicatorJob {} + +#[derive(Serialize, Deserialize, Debug)] +pub struct FileDuplicatorJobState {} + +#[derive(Serialize, Deserialize, Hash, Type)] +pub struct FileDuplicatorJobInit { + pub location_id: i32, + pub path_id: i32, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct FileDuplicatorJobStep { + pub fs_info: FsInfo, +} + +const JOB_NAME: &str = "file_duplicator"; + +#[async_trait::async_trait] +impl StatefulJob for FileDuplicatorJob { + type Data = FileDuplicatorJobState; + type Init = FileDuplicatorJobInit; + type Step = FileDuplicatorJobStep; + + fn name(&self) -> &'static str { + JOB_NAME + } + + async fn init(&self, ctx: WorkerContext, state: &mut JobState) -> Result<(), JobError> { + let fs_info = context_menu_fs_info( + &ctx.library_ctx.db, + state.init.location_id, + state.init.path_id, + ) + .await?; + + state.steps = VecDeque::new(); + state.steps.push_back(FileDuplicatorJobStep { fs_info }); + + ctx.progress(vec![JobReportUpdate::TaskCount(state.steps.len())]); + + Ok(()) + } + + async fn execute_step( + &self, + ctx: WorkerContext, + state: &mut JobState, + ) -> Result<(), JobError> { + let step = &state.steps[0]; + let info = &step.fs_info; + + match info.obj_type { + ObjectType::File => { + let mut output_path = info.obj_path.clone(); + output_path.set_file_name( + info.obj_path + .clone() + .file_stem() + .unwrap() + .to_str() + .unwrap() + .to_string() + "-Copy" + "." + + info + .obj_path + .extension() + .map_or_else(|| "", |x| x.to_str().unwrap()), + ); + std::fs::copy(info.obj_path.clone(), output_path) + } + ObjectType::Directory => todo!(), + }?; + + ctx.progress(vec![JobReportUpdate::CompletedTaskCount( + state.step_number + 1, + )]); + Ok(()) + } + + async fn finalize(&self, _ctx: WorkerContext, state: &mut JobState) -> JobResult { + Ok(Some(serde_json::to_value(&state.init)?)) + } +} diff --git a/core/src/object/fs/mod.rs b/core/src/object/fs/mod.rs index a7a94c909..41d255576 100644 --- a/core/src/object/fs/mod.rs +++ b/core/src/object/fs/mod.rs @@ -9,6 +9,7 @@ use crate::{ pub mod decrypt; pub mod delete; +pub mod duplicate; pub mod encrypt; pub mod erase; diff --git a/packages/client/src/core.ts b/packages/client/src/core.ts index cdb2b6b0c..28de78ca2 100644 --- a/packages/client/src/core.ts +++ b/packages/client/src/core.ts @@ -85,6 +85,7 @@ export type Procedures = { | { key: 'files.decryptFiles'; input: LibraryArgs; result: null } | { key: 'files.delete'; input: LibraryArgs; result: null } | { key: 'files.deleteFiles'; input: LibraryArgs; result: null } + | { key: 'files.duplicateFiles'; input: LibraryArgs; result: null } | { key: 'files.encryptFiles'; input: LibraryArgs; result: null } | { key: 'files.eraseFiles'; input: LibraryArgs; result: null } | { key: 'files.setFavorite'; input: LibraryArgs; result: null } @@ -193,6 +194,11 @@ export interface FileDeleterJobInit { path_id: number; } +export interface FileDuplicatorJobInit { + location_id: number; + path_id: number; +} + export interface FileEncryptorJobInit { location_id: number; path_id: number; diff --git a/packages/interface/src/components/explorer/ExplorerContextMenu.tsx b/packages/interface/src/components/explorer/ExplorerContextMenu.tsx index 5fa4272b0..ae0d2a1dc 100644 --- a/packages/interface/src/components/explorer/ExplorerContextMenu.tsx +++ b/packages/interface/src/components/explorer/ExplorerContextMenu.tsx @@ -172,6 +172,8 @@ export function FileItemContextMenu({ ...props }: FileItemContextMenuProps) { const hasMountedKeys = mountedUuids.data !== undefined && mountedUuids.data.length > 0 ? true : false; + const duplicateFiles = useLibraryMutation('files.duplicateFiles'); + return (
@@ -186,7 +188,15 @@ export function FileItemContextMenu({ ...props }: FileItemContextMenuProps) { - + { + expStore.locationId && + props.item.id && + duplicateFiles.mutate({ location_id: expStore.locationId, path_id: props.item.id }); + }} + /> From e86ed4e2127ab381edee1e0b2040c332f5a8c958 Mon Sep 17 00:00:00 2001 From: brxken128 <77554505+brxken128@users.noreply.github.com> Date: Tue, 17 Jan 2023 12:13:25 +0000 Subject: [PATCH 02/43] working dir duplication --- core/src/object/fs/duplicate.rs | 119 ++++++++++++++++++++++++++------ 1 file changed, 97 insertions(+), 22 deletions(-) diff --git a/core/src/object/fs/duplicate.rs b/core/src/object/fs/duplicate.rs index ef5f44063..dacc7f330 100644 --- a/core/src/object/fs/duplicate.rs +++ b/core/src/object/fs/duplicate.rs @@ -2,12 +2,15 @@ use super::{context_menu_fs_info, FsInfo, ObjectType}; use crate::job::{JobError, JobReportUpdate, JobResult, JobState, StatefulJob, WorkerContext}; use serde::{Deserialize, Serialize}; use specta::Type; -use std::{collections::VecDeque, hash::Hash}; +use std::{collections::VecDeque, hash::Hash, path::PathBuf}; pub struct FileDuplicatorJob {} -#[derive(Serialize, Deserialize, Debug)] -pub struct FileDuplicatorJobState {} +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct FileDuplicatorJobState { + pub root_path: PathBuf, + pub root_type: ObjectType, +} #[derive(Serialize, Deserialize, Hash, Type)] pub struct FileDuplicatorJobInit { @@ -15,7 +18,7 @@ pub struct FileDuplicatorJobInit { pub path_id: i32, } -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, Clone)] pub struct FileDuplicatorJobStep { pub fs_info: FsInfo, } @@ -40,6 +43,11 @@ impl StatefulJob for FileDuplicatorJob { ) .await?; + state.data = Some(FileDuplicatorJobState { + root_path: fs_info.obj_path.clone(), + root_type: fs_info.obj_type.clone(), + }); + state.steps = VecDeque::new(); state.steps.push_back(FileDuplicatorJobStep { fs_info }); @@ -53,29 +61,96 @@ impl StatefulJob for FileDuplicatorJob { ctx: WorkerContext, state: &mut JobState, ) -> Result<(), JobError> { - let step = &state.steps[0]; + let step = state.steps[0].clone(); let info = &step.fs_info; + // temporary + let job_state = if let Some(st) = state.data.clone() { + st + } else { + return Err(JobError::CryptoError(sd_crypto::Error::MediaLengthParse)); + }; + + let mut root_path = if job_state.root_type == ObjectType::File { + let mut output_path = info.obj_path.clone(); + output_path.set_file_name( + info.obj_path + .file_stem() + .unwrap() + .to_str() + .unwrap() + .to_string() + "-Copy" + + &info.obj_path.extension().map_or_else( + || String::from(""), + |x| String::from(".") + x.to_str().unwrap(), + ), + ); + output_path + } else { + let mut output_path = job_state.root_path.clone(); + output_path.set_file_name( + output_path + .file_stem() + .unwrap() + .to_str() + .unwrap() + .to_string() + "-Copy", + ); + output_path + }; + match info.obj_type { ObjectType::File => { - let mut output_path = info.obj_path.clone(); - output_path.set_file_name( - info.obj_path - .clone() - .file_stem() - .unwrap() - .to_str() - .unwrap() - .to_string() + "-Copy" + "." - + info - .obj_path - .extension() - .map_or_else(|| "", |x| x.to_str().unwrap()), - ); - std::fs::copy(info.obj_path.clone(), output_path) + let mut path = root_path.clone(); + + if job_state.root_type == ObjectType::Directory { + path.push( + info.obj_path + .strip_prefix(job_state.root_path.clone()) + .unwrap(), + ); + } + + std::fs::copy(info.obj_path.clone(), path.clone())?; } - ObjectType::Directory => todo!(), - }?; + ObjectType::Directory => { + for entry in std::fs::read_dir(info.obj_path.clone())? { + let entry = entry?; + if entry.metadata()?.is_dir() { + let obj_type = ObjectType::Directory; + state.steps.push_back(FileDuplicatorJobStep { + fs_info: FsInfo { + obj_id: None, + obj_name: String::new(), + obj_path: entry.path(), + obj_type, + }, + }); + + let mut path = root_path.clone(); + path.push( + entry + .path() + .strip_prefix(job_state.root_path.clone()) + .unwrap(), + ); + std::fs::create_dir_all(path)?; + } else { + let obj_type = ObjectType::File; + state.steps.push_back(FileDuplicatorJobStep { + fs_info: FsInfo { + obj_id: None, + obj_name: entry.file_name().to_str().unwrap().to_string(), + obj_path: entry.path(), + obj_type, + }, + }); + }; + + ctx.progress(vec![JobReportUpdate::TaskCount(state.steps.len())]); + } + } + }; ctx.progress(vec![JobReportUpdate::CompletedTaskCount( state.step_number + 1, From bd8b6446aad58ae0b3855ed9bb14a0fb44395b31 Mon Sep 17 00:00:00 2001 From: brxken128 <77554505+brxken128@users.noreply.github.com> Date: Tue, 17 Jan 2023 12:40:07 +0000 Subject: [PATCH 03/43] cleanup duplicator job --- core/src/object/fs/duplicate.rs | 90 +++++++++++++++++---------------- 1 file changed, 46 insertions(+), 44 deletions(-) diff --git a/core/src/object/fs/duplicate.rs b/core/src/object/fs/duplicate.rs index dacc7f330..c5e12106d 100644 --- a/core/src/object/fs/duplicate.rs +++ b/core/src/object/fs/duplicate.rs @@ -9,6 +9,7 @@ pub struct FileDuplicatorJob {} #[derive(Serialize, Deserialize, Debug, Clone)] pub struct FileDuplicatorJobState { pub root_path: PathBuf, + pub root_prefix: PathBuf, pub root_type: ObjectType, } @@ -43,8 +44,38 @@ impl StatefulJob for FileDuplicatorJob { ) .await?; + let root_prefix = if fs_info.obj_type == ObjectType::File { + let mut output_path = fs_info.obj_path.clone(); + output_path.set_file_name( + fs_info + .obj_path + .file_stem() + .unwrap() + .to_str() + .unwrap() + .to_string() + "-Copy" + + &fs_info.obj_path.extension().map_or_else( + || String::from(""), + |x| String::from(".") + x.to_str().unwrap(), + ), + ); + output_path + } else { + let mut output_path = fs_info.obj_path.clone(); + output_path.set_file_name( + output_path + .file_stem() + .unwrap() + .to_str() + .unwrap() + .to_string() + "-Copy", + ); + output_path + }; + state.data = Some(FileDuplicatorJobState { root_path: fs_info.obj_path.clone(), + root_prefix, root_type: fs_info.obj_type.clone(), }); @@ -64,44 +95,13 @@ impl StatefulJob for FileDuplicatorJob { let step = state.steps[0].clone(); let info = &step.fs_info; - // temporary - let job_state = if let Some(st) = state.data.clone() { - st - } else { - return Err(JobError::CryptoError(sd_crypto::Error::MediaLengthParse)); - }; - - let mut root_path = if job_state.root_type == ObjectType::File { - let mut output_path = info.obj_path.clone(); - output_path.set_file_name( - info.obj_path - .file_stem() - .unwrap() - .to_str() - .unwrap() - .to_string() + "-Copy" - + &info.obj_path.extension().map_or_else( - || String::from(""), - |x| String::from(".") + x.to_str().unwrap(), - ), - ); - output_path - } else { - let mut output_path = job_state.root_path.clone(); - output_path.set_file_name( - output_path - .file_stem() - .unwrap() - .to_str() - .unwrap() - .to_string() + "-Copy", - ); - output_path - }; + let job_state = state.data.clone().ok_or(JobError::MissingData { + value: String::from("job state"), + })?; match info.obj_type { ObjectType::File => { - let mut path = root_path.clone(); + let mut path = job_state.root_prefix.clone(); if job_state.root_type == ObjectType::Directory { path.push( @@ -126,15 +126,6 @@ impl StatefulJob for FileDuplicatorJob { obj_type, }, }); - - let mut path = root_path.clone(); - path.push( - entry - .path() - .strip_prefix(job_state.root_path.clone()) - .unwrap(), - ); - std::fs::create_dir_all(path)?; } else { let obj_type = ObjectType::File; state.steps.push_back(FileDuplicatorJobStep { @@ -147,6 +138,17 @@ impl StatefulJob for FileDuplicatorJob { }); }; + let mut path_suffix = entry + .path() + .strip_prefix(job_state.root_path.clone()) + .unwrap() + .to_path_buf(); + path_suffix.set_file_name(""); + + let mut path = job_state.root_prefix.clone(); + path.push(path_suffix); + std::fs::create_dir_all(path)?; + ctx.progress(vec![JobReportUpdate::TaskCount(state.steps.len())]); } } From b9f4d59bf4304682b26a33227db3533f366d9f71 Mon Sep 17 00:00:00 2001 From: brxken128 <77554505+brxken128@users.noreply.github.com> Date: Tue, 17 Jan 2023 12:54:30 +0000 Subject: [PATCH 04/43] fully working file duplicator --- core/src/object/fs/duplicate.rs | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/core/src/object/fs/duplicate.rs b/core/src/object/fs/duplicate.rs index c5e12106d..e3e72c61c 100644 --- a/core/src/object/fs/duplicate.rs +++ b/core/src/object/fs/duplicate.rs @@ -126,6 +126,16 @@ impl StatefulJob for FileDuplicatorJob { obj_type, }, }); + + let path_suffix = entry + .path() + .strip_prefix(job_state.root_path.clone()) + .unwrap() + .to_path_buf(); + + let mut path = job_state.root_prefix.clone(); + path.push(path_suffix); + std::fs::create_dir_all(path)?; } else { let obj_type = ObjectType::File; state.steps.push_back(FileDuplicatorJobStep { @@ -138,17 +148,6 @@ impl StatefulJob for FileDuplicatorJob { }); }; - let mut path_suffix = entry - .path() - .strip_prefix(job_state.root_path.clone()) - .unwrap() - .to_path_buf(); - path_suffix.set_file_name(""); - - let mut path = job_state.root_prefix.clone(); - path.push(path_suffix); - std::fs::create_dir_all(path)?; - ctx.progress(vec![JobReportUpdate::TaskCount(state.steps.len())]); } } From 6b5af02b8733b21c03dbe5504ef75fea3034b19a Mon Sep 17 00:00:00 2001 From: brxken128 <77554505+brxken128@users.noreply.github.com> Date: Fri, 20 Jan 2023 13:18:11 +0000 Subject: [PATCH 05/43] add frontend ground work for cut/copy actions --- core/src/api/files.rs | 9 + core/src/object/fs/copy.rs | 167 ++++++++++++++++++ core/src/object/fs/mod.rs | 1 + packages/client/src/core.ts | 8 + .../explorer/ExplorerContextMenu.tsx | 77 ++++++++ .../interface/src/hooks/useExplorerStore.tsx | 13 +- 6 files changed, 274 insertions(+), 1 deletion(-) diff --git a/core/src/api/files.rs b/core/src/api/files.rs index 326725a4f..c4ab092b4 100644 --- a/core/src/api/files.rs +++ b/core/src/api/files.rs @@ -2,6 +2,7 @@ use crate::{ invalidate_query, job::Job, object::fs::{ + copy::{FileCopierJob, FileCopierJobInit}, decrypt::{FileDecryptorJob, FileDecryptorJobInit}, delete::{FileDeleterJob, FileDeleterJobInit}, duplicate::{FileDuplicatorJob, FileDuplicatorJobInit}, @@ -133,6 +134,14 @@ pub(crate) fn mount() -> RouterBuilder { .await; invalidate_query!(library, "locations.getExplorerData"); + Ok(()) + }) + }) + .library_mutation("copyFiles", |t| { + t(|_, args: FileCopierJobInit, library| async move { + library.spawn_job(Job::new(args, FileCopierJob {})).await; + invalidate_query!(library, "locations.getExplorerData"); + Ok(()) }) }) diff --git a/core/src/object/fs/copy.rs b/core/src/object/fs/copy.rs index e69de29bb..cce361b9d 100644 --- a/core/src/object/fs/copy.rs +++ b/core/src/object/fs/copy.rs @@ -0,0 +1,167 @@ +use super::{context_menu_fs_info, FsInfo, ObjectType}; +use crate::job::{JobError, JobReportUpdate, JobResult, JobState, StatefulJob, WorkerContext}; +use serde::{Deserialize, Serialize}; +use specta::Type; +use std::{collections::VecDeque, hash::Hash, path::PathBuf}; + +pub struct FileCopierJob {} + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct FileCopierJobState { + pub root_path: PathBuf, + pub root_prefix: PathBuf, + pub root_type: ObjectType, +} + +#[derive(Serialize, Deserialize, Hash, Type)] +pub struct FileCopierJobInit { + pub source_location_id: i32, + pub source_path_id: i32, + pub target_location_id: i32, + pub target_path: PathBuf, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct FileCopierJobStep { + pub fs_info: FsInfo, +} + +const JOB_NAME: &str = "file_copier"; + +#[async_trait::async_trait] +impl StatefulJob for FileCopierJob { + type Data = FileCopierJobState; + type Init = FileCopierJobInit; + type Step = FileCopierJobStep; + + fn name(&self) -> &'static str { + JOB_NAME + } + + async fn init(&self, ctx: WorkerContext, state: &mut JobState) -> Result<(), JobError> { + // let fs_info = context_menu_fs_info( + // &ctx.library_ctx.db, + // state.init.location_id, + // state.init.path_id, + // ) + // .await?; + + // let root_prefix = if fs_info.obj_type == ObjectType::File { + // let mut output_path = fs_info.obj_path.clone(); + // output_path.set_file_name( + // fs_info + // .obj_path + // .file_stem() + // .unwrap() + // .to_str() + // .unwrap() + // .to_string() + "-Copy" + // + &fs_info.obj_path.extension().map_or_else( + // || String::from(""), + // |x| String::from(".") + x.to_str().unwrap(), + // ), + // ); + // output_path + // } else { + // let mut output_path = fs_info.obj_path.clone(); + // output_path.set_file_name( + // output_path + // .file_stem() + // .unwrap() + // .to_str() + // .unwrap() + // .to_string() + "-Copy", + // ); + // output_path + // }; + + // state.data = Some(FileCopierJobState { + // root_path: fs_info.obj_path.clone(), + // root_prefix, + // root_type: fs_info.obj_type.clone(), + // }); + + // state.steps = VecDeque::new(); + // state.steps.push_back(FileCopierJobStep { fs_info }); + + // ctx.progress(vec![JobReportUpdate::TaskCount(state.steps.len())]); + + Ok(()) + } + + async fn execute_step( + &self, + ctx: WorkerContext, + state: &mut JobState, + ) -> Result<(), JobError> { + // let step = state.steps[0].clone(); + // let info = &step.fs_info; + + // let job_state = state.data.clone().ok_or(JobError::MissingData { + // value: String::from("job state"), + // })?; + + // match info.obj_type { + // ObjectType::File => { + // let mut path = job_state.root_prefix.clone(); + + // if job_state.root_type == ObjectType::Directory { + // path.push( + // info.obj_path + // .strip_prefix(job_state.root_path.clone()) + // .unwrap(), + // ); + // } + + // std::fs::copy(info.obj_path.clone(), path.clone())?; + // } + // ObjectType::Directory => { + // for entry in std::fs::read_dir(info.obj_path.clone())? { + // let entry = entry?; + // if entry.metadata()?.is_dir() { + // let obj_type = ObjectType::Directory; + // state.steps.push_back(FileCopierJobStep { + // fs_info: FsInfo { + // obj_id: None, + // obj_name: String::new(), + // obj_path: entry.path(), + // obj_type, + // }, + // }); + + // let path_suffix = entry + // .path() + // .strip_prefix(job_state.root_path.clone()) + // .unwrap() + // .to_path_buf(); + + // let mut path = job_state.root_prefix.clone(); + // path.push(path_suffix); + // std::fs::create_dir_all(path)?; + // } else { + // let obj_type = ObjectType::File; + // state.steps.push_back(FileCopierJobStep { + // fs_info: FsInfo { + // obj_id: None, + // obj_name: entry.file_name().to_str().unwrap().to_string(), + // obj_path: entry.path(), + // obj_type, + // }, + // }); + // }; + + // ctx.progress(vec![JobReportUpdate::TaskCount(state.steps.len())]); + // } + // } + // }; + + // ctx.progress(vec![JobReportUpdate::CompletedTaskCount( + // state.step_number + 1, + // )]); + Ok(()) + } + + async fn finalize(&self, _ctx: WorkerContext, state: &mut JobState) -> JobResult { + Ok(Some(serde_json::to_value(&state.init)?)) + } +} diff --git a/core/src/object/fs/mod.rs b/core/src/object/fs/mod.rs index 41d255576..b6c629b0a 100644 --- a/core/src/object/fs/mod.rs +++ b/core/src/object/fs/mod.rs @@ -7,6 +7,7 @@ use crate::{ prisma::{file_path, location, PrismaClient}, }; +pub mod copy; pub mod decrypt; pub mod delete; pub mod duplicate; diff --git a/packages/client/src/core.ts b/packages/client/src/core.ts index 28de78ca2..64e8589f9 100644 --- a/packages/client/src/core.ts +++ b/packages/client/src/core.ts @@ -82,6 +82,7 @@ export type Procedures = { | { key: 'tags.list'; input: LibraryArgs; result: Array } | { key: 'volumes.list'; input: never; result: Array }; mutations: + | { key: 'files.copyFiles'; input: LibraryArgs; result: null } | { key: 'files.decryptFiles'; input: LibraryArgs; result: null } | { key: 'files.delete'; input: LibraryArgs; result: null } | { key: 'files.deleteFiles'; input: LibraryArgs; result: null } @@ -181,6 +182,13 @@ export type ExplorerItem = | ({ type: 'Path' } & FilePathWithObject) | ({ type: 'Object' } & ObjectWithFilePaths); +export interface FileCopierJobInit { + source_location_id: number; + source_path_id: number; + target_location_id: number; + target_path: string; +} + export interface FileDecryptorJobInit { location_id: number; path_id: number; diff --git a/packages/interface/src/components/explorer/ExplorerContextMenu.tsx b/packages/interface/src/components/explorer/ExplorerContextMenu.tsx index ae0d2a1dc..5f3915421 100644 --- a/packages/interface/src/components/explorer/ExplorerContextMenu.tsx +++ b/packages/interface/src/components/explorer/ExplorerContextMenu.tsx @@ -1,11 +1,15 @@ import { ArrowBendUpRight, + Clipboard, + Copy, + FileX, Image, LockSimple, LockSimpleOpen, Package, Plus, Repeat, + Scissors, Share, ShieldCheck, TagSimple, @@ -101,6 +105,7 @@ export function ExplorerContextMenu(props: PropsWithChildren) { const generateThumbsForLocation = useLibraryMutation('jobs.generateThumbsForLocation'); const objectValidator = useLibraryMutation('jobs.objectValidator'); const rescanLocation = useLibraryMutation('locations.fullRescan'); + const copyFiles = useLibraryMutation('files.copyFiles'); return (
@@ -131,6 +136,38 @@ export function ExplorerContextMenu(props: PropsWithChildren) { icon={Repeat} /> +