diff --git a/src-server/api/copyHandler.go b/src-server/api/copyHandler.go index 59cf9a4..f40266a 100644 --- a/src-server/api/copyHandler.go +++ b/src-server/api/copyHandler.go @@ -29,6 +29,15 @@ func CopyHandler(fsOperateEventCenter *fsOperateEventCenter.FsOperateEventCenter newPath := filepath.Join(staticPath, req.NewPath, filepath.Base(decodedPath)) eventId := fsOperateEventCenter.Push(func() error { + // 检查源路径是文件还是目录 + fileInfo, err := os.Stat(decodedPath) + if err != nil { + return err + } + + if fileInfo.IsDir() { + return copyDir(decodedPath, newPath) + } return copyFile(decodedPath, newPath) }) c.JSON(http.StatusOK, gin.H{"eventId": eventId}) @@ -55,3 +64,43 @@ func copyFile(src, dst string) error { return destFile.Sync() } + +// 复制目录及其内容 +func copyDir(src, dst string) error { + // 获取源目录信息 + srcInfo, err := os.Stat(src) + if err != nil { + return err + } + + // 创建目标目录 + if err = os.MkdirAll(dst, srcInfo.Mode()); err != nil { + return err + } + + // 读取源目录内容 + entries, err := os.ReadDir(src) + if err != nil { + return err + } + + // 遍历并复制每个条目 + for _, entry := range entries { + srcPath := filepath.Join(src, entry.Name()) + dstPath := filepath.Join(dst, entry.Name()) + + if entry.IsDir() { + // 递归复制子目录 + if err = copyDir(srcPath, dstPath); err != nil { + return err + } + } else { + // 复制文件 + if err = copyFile(srcPath, dstPath); err != nil { + return err + } + } + } + + return nil +} diff --git a/src/class/VirtualDirectory.ts b/src/class/VirtualDirectory.ts index 4446bfa..6888875 100644 --- a/src/class/VirtualDirectory.ts +++ b/src/class/VirtualDirectory.ts @@ -62,6 +62,13 @@ export abstract class VirtualFileBase { * @returns {Promise} 移动是否成功 */ abstract moveTo(targetDir: VirtualDirectoryBase): Promise; + + /** + * 复制文件到目标目录 + * @param {VirtualDirectoryBase} targetDir 目标目录 + * @returns {Promise} 复制是否成功 + */ + abstract copyTo(targetDir: VirtualDirectoryBase): Promise; } export abstract class VirtualDirectoryBase { @@ -193,6 +200,13 @@ export abstract class VirtualDirectoryBase { * @returns {Promise} 移动是否成功 */ abstract mv(targetDir: VirtualDirectoryBase): Promise; + + /** + * 复制目录到目标目录 + * @param {VirtualDirectoryBase} targetDir 目标目录 + * @returns {Promise} 复制是否成功 + */ + abstract copy(targetDir: VirtualDirectoryBase): Promise; } export abstract class VirtualFile extends VirtualFileBase {} diff --git a/src/class/VirtualRemoteDirectory.ts b/src/class/VirtualRemoteDirectory.ts index 8030819..e357036 100644 --- a/src/class/VirtualRemoteDirectory.ts +++ b/src/class/VirtualRemoteDirectory.ts @@ -107,6 +107,34 @@ export class VirtualRemoteFile extends VirtualFileBase { return false; } } + + async copyTo(targetDir: VirtualDirectoryBase): Promise { + // 检查目标目录是否已存在同名文件 + const existingFile = targetDir.files.find(file => file.name === this.name); + if (existingFile) { + // 存在同名文件,拒绝操作并显示提示 + console.error(`目标目录已存在同名文件: ${this.name}`); + alert(`复制失败:目标目录已存在同名文件"${this.name}"`); + return false; + } + + try { + const { response } = miaoFetchApi.copy(this, targetDir); + const id = (await response).eventId; + + const eventResult = await pollEventUntilComplete(id); + const success = eventResult.status === 'success'; + + if (success) { + await targetDir.update(); // 更新目标目录 + } + + return success; + } catch (error) { + console.error('复制文件失败:', error); + return false; + } + } } export class VirtualRemoteDirectory extends VirtualDirectoryBase { @@ -248,8 +276,7 @@ export class VirtualRemoteDirectory extends VirtualDirectoryBase { } async mv(targetDir: VirtualDirectoryBase): Promise { - // 不能移动到自己或自己的子目录 - if (targetDir === this || this.getParents.includes(targetDir as VirtualDirectoryBase)) { + if (targetDir === this || targetDir.getParents.includes(this)) { return false; } @@ -276,4 +303,39 @@ export class VirtualRemoteDirectory extends VirtualDirectoryBase { return false; } } + + async copy(targetDir: VirtualDirectoryBase): Promise { + if (targetDir === this || targetDir.getParents.includes(this)) { + return false; + } + + // 检查目标目录是否已存在同名目录 + const existingDir = targetDir.directories.find(dir => dir.name === this.name); + if (existingDir) { + // 存在同名目录,拒绝操作并显示提示 + console.error(`目标位置已存在同名目录: ${this.name}`); + alert(`复制失败:目标位置已存在同名目录"${this.name}"`); + return false; + } + + console.log(this.parent, targetDir, this) + + try { + const { response } = miaoFetchApi.copy(this, targetDir); + const id = (await response).eventId; + + const eventResult = await pollEventUntilComplete(id); + const success = eventResult.status === 'success'; + + if (success) { + // 不需要更新源目录,因为复制不会改变源目录 + await targetDir.update(); // 更新目标目录 + } + + return success; + } catch (error) { + console.error('复制目录失败:', error); + return false; + } + } } diff --git a/src/config.ts b/src/config.ts index 10fab78..c327f90 100644 --- a/src/config.ts +++ b/src/config.ts @@ -15,6 +15,7 @@ export default { rename: '/rename', mkdir: '/mkdir', cut: '/cut', + copy: '/copy', addresses: '/addresses' }, enableUpload: true diff --git a/src/hooks/useMiaoFetchApi.ts b/src/hooks/useMiaoFetchApi.ts index fb56aea..fb254a1 100644 --- a/src/hooks/useMiaoFetchApi.ts +++ b/src/hooks/useMiaoFetchApi.ts @@ -9,7 +9,8 @@ import { renameOption, cutOption, mkdirOption, - getFileOption + getFileOption, + copyOption } from '@/types/fetch' import Config from '@/config' import axios, { AxiosRequestConfig, AxiosResponse } from 'axios' @@ -270,6 +271,35 @@ const miaoFetchApi = { } }, + /** + * 复制文件或文件夹 + */ + copy( + target: VirtualFile | VirtualDirectory, + to: VirtualDirectory, + option?: copyOption + ) { + const newPath = to.path + const { baseUrl, api } = Config + const url = `${baseUrl}${api.copy}${target.path}` + const retry = option?.retry ?? 0 + const { miaoFetch } = useMiaoFetch({ + retry + }) + return { + response: new Promise<{ + eventId: number + }>(async (resolve) => { + const response = await miaoFetch({ + url, + method: 'post', + data: { newPath } + }) + resolve(response.data) + }) + } + }, + /** * 查询服务端操作事件的进展 */ diff --git a/src/plugins/copyPasteVirtualItem/copyPaste.ts b/src/plugins/copyPasteVirtualItem/copyPaste.ts new file mode 100644 index 0000000..f714e75 --- /dev/null +++ b/src/plugins/copyPasteVirtualItem/copyPaste.ts @@ -0,0 +1,174 @@ +import { VirtualDirectory, VirtualFile } from '@/class/VirtualDirectory' +import { usePluginHooksType } from '@/types/type' + +// 用于存储复制的项目 +let copiedItems: { + files: VirtualFile[], + directories: VirtualDirectory[], + isCut: boolean +} = { + files: [], + directories: [], + isCut: false +} + +// 复制或剪切虚拟项目 +const copyItems = (vDirs: VirtualDirectory[], vFiles: VirtualFile[], hooks: usePluginHooksType, isCut = false) => { + const { globalAlertTip } = hooks + + // 保存复制的项目 + copiedItems = { + files: [...vFiles], + directories: [...vDirs], + isCut + } + + // 生成消息文本 + const fileCount = vFiles.length + const dirCount = vDirs.length + let messageText = '' + + if (fileCount > 0 && dirCount > 0) { + messageText = `已${isCut ? '剪切' : '复制'} ${fileCount}个文件和${dirCount}个文件夹` + } else if (fileCount > 0) { + messageText = `已${isCut ? '剪切' : '复制'} ${fileCount}个文件` + } else if (dirCount > 0) { + messageText = `已${isCut ? '剪切' : '复制'} ${dirCount}个文件夹` + } + + // 显示提示 + globalAlertTip(messageText, { + type: 'info', + timeout: 2000 + }) +} + +// 粘贴虚拟项目 +const pasteItems = async (vDirs: VirtualDirectory[], _vFiles: VirtualFile[], hooks: usePluginHooksType) => { + const { globalAlertTip, getRootVDirectory } = hooks + + // 检查目标目录 + if (vDirs.length !== 1) { + globalAlertTip('请选择一个目标文件夹进行粘贴', { + type: 'error', + timeout: 2000 + }) + return + } + + // 检查是否有复制的项目 + if (copiedItems.files.length === 0 && copiedItems.directories.length === 0) { + globalAlertTip('没有可粘贴的项目', { + type: 'info', + timeout: 2000 + }) + return + } + + const targetDir = vDirs[0] + let successCount = 0 + let failCount = 0 + + // 处理粘贴文件 + for (const file of copiedItems.files) { + try { + if (copiedItems.isCut) { + // 剪切操作 + const success = await file.moveTo(targetDir) + if (success) { + successCount++ + } else { + failCount++ + } + } else { + // 复制操作 + const success = await file.copyTo(targetDir) + if (success) { + successCount++ + } else { + failCount++ + } + } + } catch (err) { + console.error('粘贴文件失败:', err) + failCount++ + } + } + + // 处理粘贴目录 + for (const dir of copiedItems.directories) { + try { + if (copiedItems.isCut) { + // 目录移动 - 使用mv方法而不是moveTo + const success = await dir.mv(targetDir) + if (success) { + successCount++ + } else { + failCount++ + } + } else { + // 目录复制 + const success = await dir.copy(targetDir) + if (success) { + successCount++ + } else { + failCount++ + } + } + } catch (err) { + console.error('粘贴目录失败:', err) + failCount++ + } + } + + // 如果是剪切操作,粘贴后清空复制的项目 + if (copiedItems.isCut && successCount > 0) { + copiedItems = { + files: [], + directories: [], + isCut: false + } + } + + // 显示操作结果 + if (successCount > 0 && failCount === 0) { + globalAlertTip(`成功${copiedItems.isCut ? '移动' : '复制'}了${successCount}个项目`, { + type: 'success', + timeout: 2000 + }) + } else if (successCount > 0 && failCount > 0) { + globalAlertTip(`${successCount}个项目成功,${failCount}个项目失败`, { + type: 'info', + timeout: 2000 + }) + } else { + globalAlertTip('操作失败', { + type: 'error', + timeout: 2000 + }) + } + + // 更新目录 + try { + // 使用update方法更新根目录 + const rootDir = getRootVDirectory(); + await rootDir.update(); + } catch (err) { + console.error('更新目录失败:', err); + } +} + +// 复制功能 +export const copy = (vDirs: VirtualDirectory[], vFiles: VirtualFile[], hooks: usePluginHooksType) => { + copyItems(vDirs, vFiles, hooks, false) +} + +// 剪切功能 +export const cut = (vDirs: VirtualDirectory[], vFiles: VirtualFile[], hooks: usePluginHooksType) => { + copyItems(vDirs, vFiles, hooks, true) +} + +// 粘贴功能 +export const paste = (vDirs: VirtualDirectory[], vFiles: VirtualFile[], hooks: usePluginHooksType) => { + pasteItems(vDirs, vFiles, hooks) +} \ No newline at end of file diff --git a/src/plugins/copyPasteVirtualItem/export.ts b/src/plugins/copyPasteVirtualItem/export.ts new file mode 100644 index 0000000..ecdee02 --- /dev/null +++ b/src/plugins/copyPasteVirtualItem/export.ts @@ -0,0 +1,60 @@ +import { Plugin, PluginGroup } from '@/class/PluginCenter' +import { copy, cut, paste } from './copyPaste' +import { + CopyOutline, + CutOutline, + DocumentAttachOutline +} from '@vicons/ionicons5' +import { PluginExportType } from '@/types/type' +import { shallowRef } from 'vue' + +// 复制插件配置 +export const copyPluginConfig: Plugin = { + name: '复制', + icon: shallowRef(CopyOutline), + filter: (vDirs, vFiles) => { + // 至少选择了一个文件或文件夹 + return vDirs.length > 0 || vFiles.length > 0 + }, + disable: false, + group: [PluginGroup.default], + func: copy, + priority: -10 +} + +// 剪切插件配置 +export const cutPluginConfig: Plugin = { + name: '剪切', + icon: shallowRef(CutOutline), + filter: (vDirs, vFiles) => { + // 至少选择了一个文件或文件夹 + return vDirs.length > 0 || vFiles.length > 0 + }, + disable: false, + group: [PluginGroup.default], + func: cut, + priority: -10 +} + +// 粘贴插件配置 +export const pastePluginConfig: Plugin = { + name: '粘贴', + icon: shallowRef(DocumentAttachOutline), + filter: (vDirs, vFiles) => { + // 只选择了一个目录作为粘贴目标 + return vDirs.length === 1 && vFiles.length === 0 + }, + disable: false, + group: [PluginGroup.default], + func: paste, + priority: -10 +} + +// 导出为默认插件类型 +export const type = PluginExportType.default + +// 插件键值 +export const key = 'miaoCopy' + +// 导出默认的复制插件 +export default copyPluginConfig \ No newline at end of file diff --git a/src/plugins/copyPasteVirtualItem/exportCut.ts b/src/plugins/copyPasteVirtualItem/exportCut.ts new file mode 100644 index 0000000..689d93f --- /dev/null +++ b/src/plugins/copyPasteVirtualItem/exportCut.ts @@ -0,0 +1,11 @@ +import { cutPluginConfig } from './export' +import { PluginExportType } from '@/types/type' + +// 导出为默认插件类型 +export const type = PluginExportType.default + +// 插件键值 +export const key = 'miaoCut' + +// 导出剪切插件 +export default cutPluginConfig \ No newline at end of file diff --git a/src/plugins/copyPasteVirtualItem/exportPaste.ts b/src/plugins/copyPasteVirtualItem/exportPaste.ts new file mode 100644 index 0000000..84fb5fc --- /dev/null +++ b/src/plugins/copyPasteVirtualItem/exportPaste.ts @@ -0,0 +1,11 @@ +import { pastePluginConfig } from './export' +import { PluginExportType } from '@/types/type' + +// 导出为默认插件类型 +export const type = PluginExportType.default + +// 插件键值 +export const key = 'miaoPaste' + +// 导出粘贴插件 +export default pastePluginConfig \ No newline at end of file diff --git a/src/plugins/miaoDirectory/miaoDirectory.vue b/src/plugins/miaoDirectory/miaoDirectory.vue index 7dba531..ba8daa3 100644 --- a/src/plugins/miaoDirectory/miaoDirectory.vue +++ b/src/plugins/miaoDirectory/miaoDirectory.vue @@ -510,7 +510,7 @@ const handleDropFiles = async (files: File[]) => { } const handleDropVDirectory = async (vDirs: VirtualDirectory[]) => { - // 不能移到自己或自己的子文件夹内 - 这部分逻辑已经移到类的实现中 + console.log(vDirs, currentDirectory.value) for (let dir of vDirs) { (async () => { const success = await dir.mv(currentDirectory.value); diff --git a/src/types/fetch.ts b/src/types/fetch.ts index a247758..f5f63c3 100644 --- a/src/types/fetch.ts +++ b/src/types/fetch.ts @@ -35,5 +35,6 @@ export type uploadOption = { export type deleteOption = {} & miaoFetchConfig export type renameOption = {} & miaoFetchConfig export type cutOption = {} & miaoFetchConfig +export type copyOption = {} & miaoFetchConfig export type mkdirOption = {} & miaoFetchConfig export type queryOption = {} & miaoFetchConfig diff --git a/vite.config.ts b/vite.config.ts index bcb1605..ce3fdea 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -40,7 +40,7 @@ export default defineConfig({ }, server: { proxy: { - '^/(get|upload|delete|query|rename|cut|mkdir|addresses)': { + '^/(get|upload|delete|query|rename|cut|copy|mkdir|addresses)': { target: 'http://127.0.0.1:17705', changeOrigin: true, rewrite: (path) => path