diff --git a/frontend/src/api/files.ts b/frontend/src/api/files.ts index e16b75cd..ed2de8ee 100644 --- a/frontend/src/api/files.ts +++ b/frontend/src/api/files.ts @@ -177,9 +177,12 @@ function moveCopy( for (const item of items) { const from = item.from; const to = encodeURIComponent(removePrefix(item.to ?? "")); + const finalOverwrite = + item.overwrite == undefined ? overwrite : item.overwrite; + const finalRename = item.rename == undefined ? rename : item.rename; const url = `${from}?action=${ copy ? "copy" : "rename" - }&destination=${to}&override=${overwrite}&rename=${rename}`; + }&destination=${to}&override=${finalOverwrite}&rename=${finalRename}`; promises.push(resourceAction(url, "PATCH")); } layoutStore.closeHovers(); diff --git a/frontend/src/components/files/ListingItem.vue b/frontend/src/components/files/ListingItem.vue index d75c2f98..ce23c482 100644 --- a/frontend/src/components/files/ListingItem.vue +++ b/frontend/src/components/files/ListingItem.vue @@ -178,6 +178,10 @@ const drop = async (event: Event) => { from: fileStore.req?.items[i].url, to: props.url + encodeURIComponent(fileStore.req?.items[i].name), name: fileStore.req?.items[i].name, + size: fileStore.req?.items[i].size, + modified: fileStore.req?.items[i].modified, + overwrite: false, + rename: false, }); } } @@ -189,7 +193,7 @@ const drop = async (event: Event) => { const path = el.__vue__.url; const baseItems = (await api.fetch(path)).items; - const action = (overwrite: boolean, rename: boolean) => { + const action = (overwrite?: boolean, rename?: boolean) => { api .move(items, overwrite, rename) .then(() => { @@ -200,26 +204,35 @@ const drop = async (event: Event) => { const conflict = upload.checkConflict(items, baseItems); - let overwrite = false; - let rename = false; - - if (conflict) { + if (conflict.length > 0) { layoutStore.showHover({ - prompt: "replace-rename", - confirm: (event: Event, option: any) => { - overwrite = option == "overwrite"; - rename = option == "rename"; - + prompt: "resolve-conflict", + props: { + conflict: conflict, + }, + confirm: (event: Event, result: Array) => { event.preventDefault(); layoutStore.closeHovers(); - action(overwrite, rename); + for (let i = result.length - 1; i >= 0; i--) { + const item = result[i]; + if (item.checked.length == 2) { + items[item.index].rename = true; + } else if (item.checked.length == 1 && item.checked[0] == "origin") { + items[item.index].overwrite = true; + } else { + items.splice(item.index, 1); + } + } + if (items.length > 0) { + action(); + } }, }); return; } - action(overwrite, rename); + action(false, false); }; const itemClick = (event: Event | KeyboardEvent) => { diff --git a/frontend/src/components/prompts/Copy.vue b/frontend/src/components/prompts/Copy.vue index 09040e0a..85810f30 100644 --- a/frontend/src/components/prompts/Copy.vue +++ b/frontend/src/components/prompts/Copy.vue @@ -91,6 +91,10 @@ export default { from: this.req.items[item].url, to: this.dest + encodeURIComponent(this.req.items[item].name), name: this.req.items[item].name, + size: this.req.items[item].size, + modified: this.req.items[item].modified, + overwrite: false, + rename: this.$route.path === this.dest, }); } @@ -118,36 +122,41 @@ export default { }); }; - if (this.$route.path === this.dest) { - this.closeHovers(); - action(false, true); - - return; - } - const dstItems = (await api.fetch(this.dest)).items; const conflict = upload.checkConflict(items, dstItems); - let overwrite = false; - let rename = false; - - if (conflict) { + if (conflict.length > 0) { this.showHover({ - prompt: "replace-rename", - confirm: (event, option) => { - overwrite = option == "overwrite"; - rename = option == "rename"; - + prompt: "resolve-conflict", + props: { + conflict: conflict, + }, + confirm: (event, result) => { event.preventDefault(); this.closeHovers(); - action(overwrite, rename); + for (let i = result.length - 1; i >= 0; i--) { + const item = result[i]; + if (item.checked.length == 2) { + items[item.index].rename = true; + } else if ( + item.checked.length == 1 && + item.checked[0] == "origin" + ) { + items[item.index].overwrite = true; + } else { + items.splice(item.index, 1); + } + } + if (items.length > 0) { + action(); + } }, }); return; } - action(overwrite, rename); + action(false, false); }, }, }; diff --git a/frontend/src/components/prompts/Move.vue b/frontend/src/components/prompts/Move.vue index 0fec8679..36a92469 100644 --- a/frontend/src/components/prompts/Move.vue +++ b/frontend/src/components/prompts/Move.vue @@ -97,6 +97,10 @@ export default { from: this.req.items[item].url, to: this.dest + encodeURIComponent(this.req.items[item].name), name: this.req.items[item].name, + size: this.req.items[item].size, + modified: this.req.items[item].modified, + overwrite: false, + rename: false, }); } @@ -121,26 +125,39 @@ export default { const dstItems = (await api.fetch(this.dest)).items; const conflict = upload.checkConflict(items, dstItems); - let overwrite = false; - let rename = false; - - if (conflict) { + if (conflict.length > 0) { this.showHover({ - prompt: "replace-rename", - confirm: (event, option) => { - overwrite = option == "overwrite"; - rename = option == "rename"; - + prompt: "resolve-conflict", + props: { + conflict: conflict, + files: items, + }, + confirm: (event, result) => { event.preventDefault(); this.closeHovers(); - action(overwrite, rename); + for (let i = result.length - 1; i >= 0; i--) { + const item = result[i]; + if (item.checked.length == 2) { + items[item.index].rename = true; + } else if ( + item.checked.length == 1 && + item.checked[0] == "origin" + ) { + items[item.index].overwrite = true; + } else { + items.splice(item.index, 1); + } + } + if (items.length > 0) { + action(); + } }, }); return; } - action(overwrite, rename); + action(false, false); }, }, }; diff --git a/frontend/src/components/prompts/Prompts.vue b/frontend/src/components/prompts/Prompts.vue index 1cfdbfb2..791d3ca9 100644 --- a/frontend/src/components/prompts/Prompts.vue +++ b/frontend/src/components/prompts/Prompts.vue @@ -23,11 +23,11 @@ import Copy from "./Copy.vue"; import NewFile from "./NewFile.vue"; import NewDir from "./NewDir.vue"; import Replace from "./Replace.vue"; -import ReplaceRename from "./ReplaceRename.vue"; import Share from "./Share.vue"; import ShareDelete from "./ShareDelete.vue"; import Upload from "./Upload.vue"; import DiscardEditorChanges from "./DiscardEditorChanges.vue"; +import ResolveConflict from "./ResolveConflict.vue"; const layoutStore = useLayoutStore(); @@ -44,12 +44,12 @@ const components = new Map([ ["newDir", NewDir], ["download", Download], ["replace", Replace], - ["replace-rename", ReplaceRename], ["share", Share], ["upload", Upload], ["share-delete", ShareDelete], ["deleteUser", DeleteUser], ["discardEditorChanges", DiscardEditorChanges], + ["resolve-conflict", ResolveConflict], ]); const modal = computed(() => { diff --git a/frontend/src/components/prompts/ReplaceRename.vue b/frontend/src/components/prompts/ReplaceRename.vue deleted file mode 100644 index 1d49d735..00000000 --- a/frontend/src/components/prompts/ReplaceRename.vue +++ /dev/null @@ -1,57 +0,0 @@ - - - diff --git a/frontend/src/components/prompts/ResolveConflict.vue b/frontend/src/components/prompts/ResolveConflict.vue new file mode 100644 index 00000000..e7bf9c74 --- /dev/null +++ b/frontend/src/components/prompts/ResolveConflict.vue @@ -0,0 +1,307 @@ + + + + diff --git a/frontend/src/components/prompts/Upload.vue b/frontend/src/components/prompts/Upload.vue index 75f7951f..19b1fbb1 100644 --- a/frontend/src/components/prompts/Upload.vue +++ b/frontend/src/components/prompts/Upload.vue @@ -69,18 +69,29 @@ const uploadInput = (event: Event) => { const path = route.path.endsWith("/") ? route.path : route.path + "/"; const conflict = upload.checkConflict(uploadFiles, fileStore.req!.items); - if (conflict) { + if (conflict.length > 0) { layoutStore.showHover({ - prompt: "replace", - action: (event: Event) => { - event.preventDefault(); - layoutStore.closeHovers(); - upload.handleFiles(uploadFiles, path, false); + prompt: "resolve-conflict", + props: { + conflict: conflict, + isUploadAction: true, }, - confirm: (event: Event) => { + confirm: (event: Event, result: Array) => { event.preventDefault(); layoutStore.closeHovers(); - upload.handleFiles(uploadFiles, path, true); + for (let i = result.length - 1; i >= 0; i--) { + const item = result[i]; + if (item.checked.length == 2) { + continue; + } else if (item.checked.length == 1 && item.checked[0] == "origin") { + uploadFiles[item.index].overwrite = true; + } else { + uploadFiles.splice(item.index, 1); + } + } + if (uploadFiles.length > 0) { + upload.handleFiles(uploadFiles, path); + } }, }); diff --git a/frontend/src/i18n/en.json b/frontend/src/i18n/en.json index 54b30b22..77d74d9f 100644 --- a/frontend/src/i18n/en.json +++ b/frontend/src/i18n/en.json @@ -48,7 +48,11 @@ "saveChanges": "Save changes", "editAsText": "Edit as Text", "increaseFontSize": "Increase font size", - "decreaseFontSize": "Decrease font size" + "decreaseFontSize": "Decrease font size", + "overrideAll": "Replace all files in destination folder", + "skipAll": "Skip all conflicting files", + "renameAll": "Rename all files (create a copy)", + "singleDecision": "Decide for each conflicting file" }, "download": { "downloadFile": "Download File", @@ -161,7 +165,17 @@ "uploadMessage": "Select an option to upload.", "optionalPassword": "Optional password", "resolution": "Resolution", - "discardEditorChanges": "Are you sure you wish to discard the changes you've made?" + "discardEditorChanges": "Are you sure you wish to discard the changes you've made?", + "replaceOrSkip": "Replace or skip files", + "resolveConflict": "Which files do you want to keep?", + "singleConflictResolve": "If you select both versions, a number will be added to the name of the copied file.", + "fastConflictResolve": "The destination folder there are {count} files with same name.", + "uploadingFiles": "Uploading files", + "filesInOrigin": "Files in origin", + "filesInDest": "Files in destination", + "override": "Overwrite", + "skip": "Skip", + "forbiddenError": "Forbidden Error" }, "search": { "images": "Images", diff --git a/frontend/src/types/file.d.ts b/frontend/src/types/file.d.ts index 6b9b6372..e6511b39 100644 --- a/frontend/src/types/file.d.ts +++ b/frontend/src/types/file.d.ts @@ -52,6 +52,8 @@ type DownloadFormat = interface ClipItem { from: string; name: string; + size?: number; + modified?: string; } interface BreadCrumb { @@ -59,6 +61,19 @@ interface BreadCrumb { url: string; } +interface ConflictingItem { + lastModified: number | string | undefined; + size: number | undefined; +} + +interface ConflictingResource { + index: number; + name: string; + origin: ConflictingItem; + dest: ConflictingItem; + checked: Array<"origin" | "dest">; +} + interface CsvData { headers: string[]; rows: string[][]; diff --git a/frontend/src/types/upload.d.ts b/frontend/src/types/upload.d.ts index 4bad9e06..5e5716ac 100644 --- a/frontend/src/types/upload.d.ts +++ b/frontend/src/types/upload.d.ts @@ -17,6 +17,7 @@ interface UploadEntry { isDir: boolean; fullPath?: string; file?: File; + overwrite?: boolean; } type UploadList = UploadEntry[]; diff --git a/frontend/src/utils/upload.ts b/frontend/src/utils/upload.ts index e951cb43..a5a62b1d 100644 --- a/frontend/src/utils/upload.ts +++ b/frontend/src/utils/upload.ts @@ -3,16 +3,24 @@ import { useUploadStore } from "@/stores/upload"; import url from "@/utils/url"; export function checkConflict( - files: UploadList, + files: UploadList | Array, dest: ResourceItem[] -): boolean { +): ConflictingResource[] { if (typeof dest === "undefined" || dest === null) { dest = []; } + const conflictingFiles: ConflictingResource[] = []; const folder_upload = files[0].fullPath !== undefined; - const names = new Set(); + function getFile(name: string): ResourceItem | null { + for (const item of dest) { + if (item.name == name) return item; + } + + return null; + } + for (let i = 0; i < files.length; i++) { const file = files[i]; let name = file.name; @@ -24,10 +32,25 @@ export function checkConflict( } } - names.add(name); + const item = getFile(name); + if (item != null) { + conflictingFiles.push({ + index: i, + name: item.path, + origin: { + lastModified: file.modified || file.file?.lastModified, + size: file.size, + }, + dest: { + lastModified: item.modified, + size: item.size, + }, + checked: ["origin"], + }); + } } - return dest.some((d) => names.has(d.name)); + return conflictingFiles; } export function scanFiles(dt: DataTransfer): Promise { @@ -146,6 +169,12 @@ export function handleFiles( const type = file.isDir ? "dir" : detectType((file.file as File).type); - uploadStore.upload(path, file.name, file.file ?? null, overwrite, type); + uploadStore.upload( + path, + file.name, + file.file ?? null, + file.overwrite || overwrite, + type + ); } } diff --git a/frontend/src/views/files/FileListing.vue b/frontend/src/views/files/FileListing.vue index 0a86b359..f612ab1d 100644 --- a/frontend/src/views/files/FileListing.vue +++ b/frontend/src/views/files/FileListing.vue @@ -628,6 +628,8 @@ const copyCut = (event: Event | KeyboardEvent): void => { items.push({ from: fileStore.req.items[i].url, name: fileStore.req.items[i].name, + size: fileStore.req.items[i].size, + modified: fileStore.req.items[i].modified, }); } @@ -651,7 +653,15 @@ const paste = (event: Event) => { for (const item of clipboardStore.items) { const from = item.from.endsWith("/") ? item.from.slice(0, -1) : item.from; const to = route.path + encodeURIComponent(item.name); - items.push({ from, to, name: item.name }); + items.push({ + from, + to, + name: item.name, + size: item.size, + modified: item.modified, + overwrite: false, + rename: clipboardStore.path == route.path, + }); } if (items.length === 0) { @@ -660,7 +670,7 @@ const paste = (event: Event) => { const preselect = removePrefix(route.path) + items[0].name; - let action = (overwrite: boolean, rename: boolean) => { + let action = (overwrite?: boolean, rename?: boolean) => { api .copy(items, overwrite, rename) .then(() => { @@ -683,34 +693,37 @@ const paste = (event: Event) => { }; } - if (clipboardStore.path == route.path) { - action(false, true); - - return; - } - const conflict = upload.checkConflict(items, fileStore.req!.items); - let overwrite = false; - let rename = false; - - if (conflict) { + if (conflict.length > 0) { layoutStore.showHover({ - prompt: "replace-rename", - confirm: (event: Event, option: string) => { - overwrite = option == "overwrite"; - rename = option == "rename"; - + prompt: "resolve-conflict", + props: { + conflict: conflict, + }, + confirm: (event: Event, result: Array) => { event.preventDefault(); layoutStore.closeHovers(); - action(overwrite, rename); + for (let i = result.length - 1; i >= 0; i--) { + const item = result[i]; + if (item.checked.length == 2) { + items[item.index].rename = true; + } else if (item.checked.length == 1 && item.checked[0] == "origin") { + items[item.index].overwrite = true; + } else { + items.splice(item.index, 1); + } + } + if (items.length > 0) { + action(); + } }, }); return; } - action(overwrite, rename); + action(false, false); }; const columnsResize = () => { @@ -806,20 +819,30 @@ const drop = async (event: DragEvent) => { const preselect = removePrefix(path) + (files[0].fullPath || files[0].name); - if (conflict) { + if (conflict.length > 0) { layoutStore.showHover({ - prompt: "replace", - action: (event: Event) => { - event.preventDefault(); - layoutStore.closeHovers(); - upload.handleFiles(files, path, false); - fileStore.preselect = preselect; + prompt: "resolve-conflict", + props: { + conflict: conflict, + isUploadAction: true, }, - confirm: (event: Event) => { + confirm: (event: Event, result: Array) => { event.preventDefault(); layoutStore.closeHovers(); - upload.handleFiles(files, path, true); - fileStore.preselect = preselect; + for (let i = result.length - 1; i >= 0; i--) { + const item = result[i]; + if (item.checked.length == 2) { + continue; + } else if (item.checked.length == 1 && item.checked[0] == "origin") { + files[item.index].overwrite = true; + } else { + files.splice(item.index, 1); + } + } + if (files.length > 0) { + upload.handleFiles(files, path, true); + fileStore.preselect = preselect; + } }, }); @@ -852,18 +875,29 @@ const uploadInput = (event: Event) => { const path = route.path.endsWith("/") ? route.path : route.path + "/"; const conflict = upload.checkConflict(uploadFiles, fileStore.req!.items); - if (conflict) { + if (conflict.length > 0) { layoutStore.showHover({ - prompt: "replace", - action: (event: Event) => { - event.preventDefault(); - layoutStore.closeHovers(); - upload.handleFiles(uploadFiles, path, false); + prompt: "resolve-conflict", + props: { + conflict: conflict, + isUploadAction: true, }, - confirm: (event: Event) => { + confirm: (event: Event, result: Array) => { event.preventDefault(); layoutStore.closeHovers(); - upload.handleFiles(uploadFiles, path, true); + for (let i = result.length - 1; i >= 0; i--) { + const item = result[i]; + if (item.checked.length == 2) { + continue; + } else if (item.checked.length == 1 && item.checked[0] == "origin") { + uploadFiles[item.index].overwrite = true; + } else { + uploadFiles.splice(item.index, 1); + } + } + if (uploadFiles.length > 0) { + upload.handleFiles(uploadFiles, path, true); + } }, });