From 712442b41564dfd22d9b73e0e95d2d08e9bc8fe1 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] 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} /> +