diff --git a/src/lang/ar-ye.json b/src/lang/ar-ye.json index 4a1c09c7ca..77d0cf9575 100644 --- a/src/lang/ar-ye.json +++ b/src/lang/ar-ye.json @@ -831,5 +831,7 @@ "managed": "Managed", "acode service": "Acode Service", "info-terminal-show-scrollbar": "Show the xterm scrollbar beside the terminal.", - "terminal:show scrollbar": "Show Scrollbar" + "terminal:show scrollbar": "Show Scrollbar", + "apply exclusions when copying": "Apply exclusions when copying", + "settings-info-app-use-file-operation-exclusions": "Skip files and folders matching the exclusion patterns during copy and paste operations." } diff --git a/src/lang/be-by.json b/src/lang/be-by.json index 5d92508798..7c12976c30 100644 --- a/src/lang/be-by.json +++ b/src/lang/be-by.json @@ -831,5 +831,7 @@ "managed": "Managed", "acode service": "Acode Service", "info-terminal-show-scrollbar": "Show the xterm scrollbar beside the terminal.", - "terminal:show scrollbar": "Show Scrollbar" + "terminal:show scrollbar": "Show Scrollbar", + "apply exclusions when copying": "Apply exclusions when copying", + "settings-info-app-use-file-operation-exclusions": "Skip files and folders matching the exclusion patterns during copy and paste operations." } diff --git a/src/lang/bn-bd.json b/src/lang/bn-bd.json index d2b354ad3b..f9ab374f3d 100644 --- a/src/lang/bn-bd.json +++ b/src/lang/bn-bd.json @@ -831,5 +831,7 @@ "managed": "Managed", "acode service": "Acode Service", "info-terminal-show-scrollbar": "Show the xterm scrollbar beside the terminal.", - "terminal:show scrollbar": "Show Scrollbar" + "terminal:show scrollbar": "Show Scrollbar", + "apply exclusions when copying": "Apply exclusions when copying", + "settings-info-app-use-file-operation-exclusions": "Skip files and folders matching the exclusion patterns during copy and paste operations." } diff --git a/src/lang/cs-cz.json b/src/lang/cs-cz.json index 79492bb4eb..4021728586 100644 --- a/src/lang/cs-cz.json +++ b/src/lang/cs-cz.json @@ -831,5 +831,7 @@ "managed": "Managed", "acode service": "Acode Service", "info-terminal-show-scrollbar": "Show the xterm scrollbar beside the terminal.", - "terminal:show scrollbar": "Show Scrollbar" + "terminal:show scrollbar": "Show Scrollbar", + "apply exclusions when copying": "Apply exclusions when copying", + "settings-info-app-use-file-operation-exclusions": "Skip files and folders matching the exclusion patterns during copy and paste operations." } diff --git a/src/lang/de-de.json b/src/lang/de-de.json index bd63f3bf04..8e1dc29646 100644 --- a/src/lang/de-de.json +++ b/src/lang/de-de.json @@ -831,5 +831,7 @@ "managed": "Managed", "acode service": "Acode Service", "info-terminal-show-scrollbar": "Show the xterm scrollbar beside the terminal.", - "terminal:show scrollbar": "Show Scrollbar" + "terminal:show scrollbar": "Show Scrollbar", + "apply exclusions when copying": "Apply exclusions when copying", + "settings-info-app-use-file-operation-exclusions": "Skip files and folders matching the exclusion patterns during copy and paste operations." } diff --git a/src/lang/en-us.json b/src/lang/en-us.json index acd7938cd2..33a19d29ec 100644 --- a/src/lang/en-us.json +++ b/src/lang/en-us.json @@ -669,6 +669,8 @@ "settings-info-app-console": "Choose which debug console integration Acode uses.", "settings-info-app-default-file-encoding": "Default encoding when opening or creating files.", "settings-info-app-exclude-folders": "Skip folders and patterns while searching or scanning.", + "apply exclusions when copying": "Apply exclusions when copying", + "settings-info-app-use-file-operation-exclusions": "Skip files and folders matching the exclusion patterns during copy and paste operations.", "settings-info-app-floating-button": "Show the floating quick actions button.", "settings-info-app-font-manager": "Install, manage, or remove app fonts.", "settings-info-app-fullscreen": "Hide the system status bar while using Acode.", diff --git a/src/lang/es-sv.json b/src/lang/es-sv.json index 34f2a3a438..3672aac66e 100644 --- a/src/lang/es-sv.json +++ b/src/lang/es-sv.json @@ -831,5 +831,7 @@ "managed": "Managed", "acode service": "Acode Service", "info-terminal-show-scrollbar": "Show the xterm scrollbar beside the terminal.", - "terminal:show scrollbar": "Show Scrollbar" + "terminal:show scrollbar": "Show Scrollbar", + "apply exclusions when copying": "Apply exclusions when copying", + "settings-info-app-use-file-operation-exclusions": "Skip files and folders matching the exclusion patterns during copy and paste operations." } diff --git a/src/lang/fr-fr.json b/src/lang/fr-fr.json index e1320be03c..16616d1da8 100644 --- a/src/lang/fr-fr.json +++ b/src/lang/fr-fr.json @@ -831,5 +831,7 @@ "managed": "Managed", "acode service": "Acode Service", "info-terminal-show-scrollbar": "Show the xterm scrollbar beside the terminal.", - "terminal:show scrollbar": "Show Scrollbar" + "terminal:show scrollbar": "Show Scrollbar", + "apply exclusions when copying": "Apply exclusions when copying", + "settings-info-app-use-file-operation-exclusions": "Skip files and folders matching the exclusion patterns during copy and paste operations." } diff --git a/src/lang/he-il.json b/src/lang/he-il.json index 9ccb79ea86..d956ab77f9 100644 --- a/src/lang/he-il.json +++ b/src/lang/he-il.json @@ -831,5 +831,7 @@ "managed": "Managed", "acode service": "Acode Service", "info-terminal-show-scrollbar": "Show the xterm scrollbar beside the terminal.", - "terminal:show scrollbar": "Show Scrollbar" + "terminal:show scrollbar": "Show Scrollbar", + "apply exclusions when copying": "Apply exclusions when copying", + "settings-info-app-use-file-operation-exclusions": "Skip files and folders matching the exclusion patterns during copy and paste operations." } diff --git a/src/lang/hi-in.json b/src/lang/hi-in.json index b73bbdb60d..4528b0c059 100644 --- a/src/lang/hi-in.json +++ b/src/lang/hi-in.json @@ -831,5 +831,7 @@ "managed": "Managed", "acode service": "Acode Service", "info-terminal-show-scrollbar": "Show the xterm scrollbar beside the terminal.", - "terminal:show scrollbar": "Show Scrollbar" + "terminal:show scrollbar": "Show Scrollbar", + "apply exclusions when copying": "Apply exclusions when copying", + "settings-info-app-use-file-operation-exclusions": "Skip files and folders matching the exclusion patterns during copy and paste operations." } diff --git a/src/lang/hu-hu.json b/src/lang/hu-hu.json index ce9ac47fb0..1fc1a55fa7 100644 --- a/src/lang/hu-hu.json +++ b/src/lang/hu-hu.json @@ -831,5 +831,7 @@ "managed": "Kezelt", "acode service": "Acode-szolgáltatás", "info-terminal-show-scrollbar": "Az xterm görgetősávjának megjelenítése a terminál mellett.", - "terminal:show scrollbar": "Görgetősáv megjelenítése" + "terminal:show scrollbar": "Görgetősáv megjelenítése", + "apply exclusions when copying": "Apply exclusions when copying", + "settings-info-app-use-file-operation-exclusions": "Skip files and folders matching the exclusion patterns during copy and paste operations." } diff --git a/src/lang/id-id.json b/src/lang/id-id.json index 2763d37f83..10c860a652 100644 --- a/src/lang/id-id.json +++ b/src/lang/id-id.json @@ -831,5 +831,7 @@ "managed": "Dikelola", "acode service": "Layanan Acode", "info-terminal-show-scrollbar": "Show the xterm scrollbar beside the terminal.", - "terminal:show scrollbar": "Show Scrollbar" + "terminal:show scrollbar": "Show Scrollbar", + "apply exclusions when copying": "Apply exclusions when copying", + "settings-info-app-use-file-operation-exclusions": "Skip files and folders matching the exclusion patterns during copy and paste operations." } diff --git a/src/lang/index.d.ts b/src/lang/index.d.ts index 447cc8052b..0302a017f9 100644 --- a/src/lang/index.d.ts +++ b/src/lang/index.d.ts @@ -672,6 +672,8 @@ declare type LangStrings = { "settings-info-app-console": string; "settings-info-app-default-file-encoding": string; "settings-info-app-exclude-folders": string; + "apply exclusions when copying": string; + "settings-info-app-use-file-operation-exclusions": string; "settings-info-app-floating-button": string; "settings-info-app-font-manager": string; "settings-info-app-fullscreen": string; diff --git a/src/lang/ir-fa.json b/src/lang/ir-fa.json index 8c2ac9da59..173b2625d5 100644 --- a/src/lang/ir-fa.json +++ b/src/lang/ir-fa.json @@ -831,5 +831,7 @@ "managed": "Managed", "acode service": "Acode Service", "info-terminal-show-scrollbar": "Show the xterm scrollbar beside the terminal.", - "terminal:show scrollbar": "Show Scrollbar" + "terminal:show scrollbar": "Show Scrollbar", + "apply exclusions when copying": "Apply exclusions when copying", + "settings-info-app-use-file-operation-exclusions": "Skip files and folders matching the exclusion patterns during copy and paste operations." } diff --git a/src/lang/it-it.json b/src/lang/it-it.json index e338789274..7b6ecdc1e1 100644 --- a/src/lang/it-it.json +++ b/src/lang/it-it.json @@ -831,5 +831,7 @@ "managed": "Managed", "acode service": "Acode Service", "info-terminal-show-scrollbar": "Show the xterm scrollbar beside the terminal.", - "terminal:show scrollbar": "Show Scrollbar" + "terminal:show scrollbar": "Show Scrollbar", + "apply exclusions when copying": "Apply exclusions when copying", + "settings-info-app-use-file-operation-exclusions": "Skip files and folders matching the exclusion patterns during copy and paste operations." } diff --git a/src/lang/ja-jp.json b/src/lang/ja-jp.json index c1d28ba356..5d2152ad37 100644 --- a/src/lang/ja-jp.json +++ b/src/lang/ja-jp.json @@ -831,5 +831,7 @@ "managed": "Managed", "acode service": "Acode Service", "info-terminal-show-scrollbar": "Show the xterm scrollbar beside the terminal.", - "terminal:show scrollbar": "Show Scrollbar" + "terminal:show scrollbar": "Show Scrollbar", + "apply exclusions when copying": "Apply exclusions when copying", + "settings-info-app-use-file-operation-exclusions": "Skip files and folders matching the exclusion patterns during copy and paste operations." } diff --git a/src/lang/ko-kr.json b/src/lang/ko-kr.json index 42963a18f6..978e7ac4d4 100644 --- a/src/lang/ko-kr.json +++ b/src/lang/ko-kr.json @@ -831,5 +831,7 @@ "managed": "Managed", "acode service": "Acode Service", "info-terminal-show-scrollbar": "Show the xterm scrollbar beside the terminal.", - "terminal:show scrollbar": "Show Scrollbar" + "terminal:show scrollbar": "Show Scrollbar", + "apply exclusions when copying": "Apply exclusions when copying", + "settings-info-app-use-file-operation-exclusions": "Skip files and folders matching the exclusion patterns during copy and paste operations." } diff --git a/src/lang/ml-in.json b/src/lang/ml-in.json index ee8052f1a3..8424f45d15 100644 --- a/src/lang/ml-in.json +++ b/src/lang/ml-in.json @@ -831,5 +831,7 @@ "managed": "Managed", "acode service": "Acode Service", "info-terminal-show-scrollbar": "Show the xterm scrollbar beside the terminal.", - "terminal:show scrollbar": "Show Scrollbar" + "terminal:show scrollbar": "Show Scrollbar", + "apply exclusions when copying": "Apply exclusions when copying", + "settings-info-app-use-file-operation-exclusions": "Skip files and folders matching the exclusion patterns during copy and paste operations." } diff --git a/src/lang/mm-unicode.json b/src/lang/mm-unicode.json index e74101b9f1..0bbcd285fe 100644 --- a/src/lang/mm-unicode.json +++ b/src/lang/mm-unicode.json @@ -831,5 +831,7 @@ "managed": "Managed", "acode service": "Acode Service", "info-terminal-show-scrollbar": "Show the xterm scrollbar beside the terminal.", - "terminal:show scrollbar": "Show Scrollbar" + "terminal:show scrollbar": "Show Scrollbar", + "apply exclusions when copying": "Apply exclusions when copying", + "settings-info-app-use-file-operation-exclusions": "Skip files and folders matching the exclusion patterns during copy and paste operations." } diff --git a/src/lang/mm-zawgyi.json b/src/lang/mm-zawgyi.json index 7726546895..6ae58e5d73 100644 --- a/src/lang/mm-zawgyi.json +++ b/src/lang/mm-zawgyi.json @@ -831,5 +831,7 @@ "managed": "Managed", "acode service": "Acode Service", "info-terminal-show-scrollbar": "Show the xterm scrollbar beside the terminal.", - "terminal:show scrollbar": "Show Scrollbar" + "terminal:show scrollbar": "Show Scrollbar", + "apply exclusions when copying": "Apply exclusions when copying", + "settings-info-app-use-file-operation-exclusions": "Skip files and folders matching the exclusion patterns during copy and paste operations." } diff --git a/src/lang/pl-pl.json b/src/lang/pl-pl.json index 76f7e05b39..71f0c876e4 100644 --- a/src/lang/pl-pl.json +++ b/src/lang/pl-pl.json @@ -831,5 +831,7 @@ "managed": "Managed", "acode service": "Acode Service", "info-terminal-show-scrollbar": "Show the xterm scrollbar beside the terminal.", - "terminal:show scrollbar": "Show Scrollbar" + "terminal:show scrollbar": "Show Scrollbar", + "apply exclusions when copying": "Apply exclusions when copying", + "settings-info-app-use-file-operation-exclusions": "Skip files and folders matching the exclusion patterns during copy and paste operations." } diff --git a/src/lang/pt-br.json b/src/lang/pt-br.json index facba76b0e..d47cf936db 100644 --- a/src/lang/pt-br.json +++ b/src/lang/pt-br.json @@ -831,5 +831,7 @@ "managed": "Managed", "acode service": "Acode Service", "info-terminal-show-scrollbar": "Show the xterm scrollbar beside the terminal.", - "terminal:show scrollbar": "Show Scrollbar" + "terminal:show scrollbar": "Show Scrollbar", + "apply exclusions when copying": "Apply exclusions when copying", + "settings-info-app-use-file-operation-exclusions": "Skip files and folders matching the exclusion patterns during copy and paste operations." } diff --git a/src/lang/pu-in.json b/src/lang/pu-in.json index 2daa7dee96..5f43ef31fa 100644 --- a/src/lang/pu-in.json +++ b/src/lang/pu-in.json @@ -831,5 +831,7 @@ "managed": "Managed", "acode service": "Acode Service", "info-terminal-show-scrollbar": "Show the xterm scrollbar beside the terminal.", - "terminal:show scrollbar": "Show Scrollbar" + "terminal:show scrollbar": "Show Scrollbar", + "apply exclusions when copying": "Apply exclusions when copying", + "settings-info-app-use-file-operation-exclusions": "Skip files and folders matching the exclusion patterns during copy and paste operations." } diff --git a/src/lang/ru-ru.json b/src/lang/ru-ru.json index 8e101865dc..6e24ed9e9b 100644 --- a/src/lang/ru-ru.json +++ b/src/lang/ru-ru.json @@ -831,5 +831,7 @@ "managed": "Managed", "acode service": "Acode Service", "info-terminal-show-scrollbar": "Show the xterm scrollbar beside the terminal.", - "terminal:show scrollbar": "Show Scrollbar" + "terminal:show scrollbar": "Show Scrollbar", + "apply exclusions when copying": "Apply exclusions when copying", + "settings-info-app-use-file-operation-exclusions": "Skip files and folders matching the exclusion patterns during copy and paste operations." } diff --git a/src/lang/tl-ph.json b/src/lang/tl-ph.json index ddf0162be9..305b015043 100644 --- a/src/lang/tl-ph.json +++ b/src/lang/tl-ph.json @@ -831,5 +831,7 @@ "managed": "Managed", "acode service": "Acode Service", "info-terminal-show-scrollbar": "Show the xterm scrollbar beside the terminal.", - "terminal:show scrollbar": "Show Scrollbar" + "terminal:show scrollbar": "Show Scrollbar", + "apply exclusions when copying": "Apply exclusions when copying", + "settings-info-app-use-file-operation-exclusions": "Skip files and folders matching the exclusion patterns during copy and paste operations." } diff --git a/src/lang/tr-tr.json b/src/lang/tr-tr.json index 6154499ba4..f49180969d 100644 --- a/src/lang/tr-tr.json +++ b/src/lang/tr-tr.json @@ -831,5 +831,7 @@ "managed": "Managed", "acode service": "Acode Service", "info-terminal-show-scrollbar": "Show the xterm scrollbar beside the terminal.", - "terminal:show scrollbar": "Show Scrollbar" + "terminal:show scrollbar": "Show Scrollbar", + "apply exclusions when copying": "Apply exclusions when copying", + "settings-info-app-use-file-operation-exclusions": "Skip files and folders matching the exclusion patterns during copy and paste operations." } diff --git a/src/lang/uk-ua.json b/src/lang/uk-ua.json index 080ea08dd2..2801b4634c 100644 --- a/src/lang/uk-ua.json +++ b/src/lang/uk-ua.json @@ -831,5 +831,7 @@ "managed": "Managed", "acode service": "Acode Service", "info-terminal-show-scrollbar": "Show the xterm scrollbar beside the terminal.", - "terminal:show scrollbar": "Show Scrollbar" + "terminal:show scrollbar": "Show Scrollbar", + "apply exclusions when copying": "Apply exclusions when copying", + "settings-info-app-use-file-operation-exclusions": "Skip files and folders matching the exclusion patterns during copy and paste operations." } diff --git a/src/lang/uz-uz.json b/src/lang/uz-uz.json index 15dc46c84a..f6ac1dbf08 100644 --- a/src/lang/uz-uz.json +++ b/src/lang/uz-uz.json @@ -831,5 +831,7 @@ "managed": "Managed", "acode service": "Acode Service", "info-terminal-show-scrollbar": "Show the xterm scrollbar beside the terminal.", - "terminal:show scrollbar": "Show Scrollbar" + "terminal:show scrollbar": "Show Scrollbar", + "apply exclusions when copying": "Apply exclusions when copying", + "settings-info-app-use-file-operation-exclusions": "Skip files and folders matching the exclusion patterns during copy and paste operations." } diff --git a/src/lang/vi-vn.json b/src/lang/vi-vn.json index 14e338c470..47f030ca3e 100644 --- a/src/lang/vi-vn.json +++ b/src/lang/vi-vn.json @@ -831,5 +831,7 @@ "managed": "Managed", "acode service": "Acode Service", "info-terminal-show-scrollbar": "Show the xterm scrollbar beside the terminal.", - "terminal:show scrollbar": "Show Scrollbar" + "terminal:show scrollbar": "Show Scrollbar", + "apply exclusions when copying": "Apply exclusions when copying", + "settings-info-app-use-file-operation-exclusions": "Skip files and folders matching the exclusion patterns during copy and paste operations." } diff --git a/src/lang/zh-cn.json b/src/lang/zh-cn.json index a517f4ecab..72fdd86287 100644 --- a/src/lang/zh-cn.json +++ b/src/lang/zh-cn.json @@ -831,5 +831,7 @@ "managed": "Managed", "acode service": "Acode Service", "info-terminal-show-scrollbar": "Show the xterm scrollbar beside the terminal.", - "terminal:show scrollbar": "Show Scrollbar" + "terminal:show scrollbar": "Show Scrollbar", + "apply exclusions when copying": "Apply exclusions when copying", + "settings-info-app-use-file-operation-exclusions": "Skip files and folders matching the exclusion patterns during copy and paste operations." } diff --git a/src/lang/zh-hant.json b/src/lang/zh-hant.json index 68e3cb1ba6..4ad73928d2 100644 --- a/src/lang/zh-hant.json +++ b/src/lang/zh-hant.json @@ -831,5 +831,7 @@ "managed": "Managed", "acode service": "Acode Service", "info-terminal-show-scrollbar": "Show the xterm scrollbar beside the terminal.", - "terminal:show scrollbar": "Show Scrollbar" + "terminal:show scrollbar": "Show Scrollbar", + "apply exclusions when copying": "Apply exclusions when copying", + "settings-info-app-use-file-operation-exclusions": "Skip files and folders matching the exclusion patterns during copy and paste operations." } diff --git a/src/lang/zh-tw.json b/src/lang/zh-tw.json index d42f8b7529..581198caa8 100644 --- a/src/lang/zh-tw.json +++ b/src/lang/zh-tw.json @@ -831,5 +831,7 @@ "managed": "Managed", "acode service": "Acode Service", "info-terminal-show-scrollbar": "Show the xterm scrollbar beside the terminal.", - "terminal:show scrollbar": "Show Scrollbar" + "terminal:show scrollbar": "Show Scrollbar", + "apply exclusions when copying": "Apply exclusions when copying", + "settings-info-app-use-file-operation-exclusions": "Skip files and folders matching the exclusion patterns during copy and paste operations." } diff --git a/src/lib/openFolder.js b/src/lib/openFolder.js index 5415f7a824..741b6f583c 100644 --- a/src/lib/openFolder.js +++ b/src/lib/openFolder.js @@ -10,6 +10,7 @@ import confirm from "dialogs/confirm"; import prompt from "dialogs/prompt"; import select from "dialogs/select"; import escapeStringRegexp from "escape-string-regexp"; +import copyEntry from "utils/copyEntry"; import helpers from "utils/helpers"; import Path from "utils/Path"; import Uri from "utils/Uri"; @@ -776,7 +777,19 @@ function execOperation(type, action, url, $target, name) { newUrl = await fs.moveTo(url); } } else { - newUrl = await fs.copyTo(url); + if (appSettings.value.useFileOperationExclusions) { + const result = await copyEntry(clipBoard.url, url, { + excludePatterns: appSettings.value.excludeFolders, + }); + newUrl = result.url; + if (!newUrl) { + toast(strings.skipped); + clearClipboard(); + return; + } + } else { + newUrl = await fs.copyTo(url); + } } stopLoading(); diff --git a/src/lib/settings.js b/src/lib/settings.js index 6728c80b92..7421f3d2cd 100644 --- a/src/lib/settings.js +++ b/src/lib/settings.js @@ -192,6 +192,7 @@ class Settings { 16, 19, 17, 23, 24, 25, 26, 27, 28, 29, 30, 31, ], excludeFolders: this.#excludeFolders, + useFileOperationExclusions: true, defaultFileEncoding: "UTF-8", inlineAutoCompletion: true, colorPreview: true, diff --git a/src/pages/fileBrowser/fileBrowser.js b/src/pages/fileBrowser/fileBrowser.js index 439ac15015..30da2e8f00 100644 --- a/src/pages/fileBrowser/fileBrowser.js +++ b/src/pages/fileBrowser/fileBrowser.js @@ -26,6 +26,7 @@ import mimeTypes from "mime-types"; import mustache from "mustache"; import filesSettings from "settings/filesSettings"; import URLParse from "url-parse"; +import copyEntry from "utils/copyEntry"; import helpers from "utils/helpers"; import Url from "utils/Url"; import _addMenu from "./add-menu.hbs"; @@ -726,54 +727,64 @@ function FileBrowserInclude(mode, info, doesOpenLast = true) { ); let copiedCount = 0; + let skippedCount = 0; try { for (const url of copiedItems) { const fs = fsOperation(url); const stat = await fs.stat(); const name = stat.name || Url.basename(url); - const possibleConflictUrl = Url.join(targetDirUrl, name); - if (stat.isDirectory && isInsideDirectory(url, targetDirUrl)) { - alert( - strings.warning, - strings["cannot paste folder into itself"] || - "Cannot paste a folder into itself", - ); - continue; - } + const result = await copyEntry(url, targetDirUrl, { + name, + stat, + excludePatterns: appSettings.value.useFileOperationExclusions + ? appSettings.value.excludeFolders + : [], + async onBeforeCopy() { + if (stat.isDirectory && isInsideDirectory(url, targetDirUrl)) { + alert( + strings.warning, + strings["cannot paste folder into itself"] || + "Cannot paste a folder into itself", + ); + return false; + } - const doesExist = await fsOperation(possibleConflictUrl).exists(); - if (doesExist) { - if (Url.areSame(url, possibleConflictUrl)) { - continue; - } + const possibleConflictUrl = Url.join(targetDirUrl, name); + if (!(await fsOperation(possibleConflictUrl).exists())) { + return true; + } - const targetStat = await fsOperation(possibleConflictUrl).stat(); - if (stat.isDirectory || targetStat.isDirectory) { - alert( - strings.warning, - strings["folder already exists"] || "Folder already exists", - ); - continue; - } + if (Url.areSame(url, possibleConflictUrl)) return false; - const confirmation = await confirm( - strings.warning, - strings["file already exists force named"] - ? strings["file already exists force named"].replace( - "{name}", - name, - ) - : `"${name}" already exists in this location.`, - ); - if (!confirmation) continue; + const targetFs = fsOperation(possibleConflictUrl); + const targetStat = await targetFs.stat(); + if (stat.isDirectory || targetStat.isDirectory) { + alert( + strings.warning, + strings["folder already exists"] || "Folder already exists", + ); + return false; + } - await fsOperation(possibleConflictUrl).delete(); - } + const confirmation = await confirm( + strings.warning, + strings["file already exists force named"] + ? strings["file already exists force named"].replace( + "{name}", + name, + ) + : `"${name}" already exists in this location.`, + ); + if (!confirmation) return false; - await copyEntry(url, targetDirUrl, name, stat); - copiedCount++; + await targetFs.delete(); + return true; + }, + }); + if (result.url) copiedCount++; + skippedCount += result.skipped; } } catch (err) { helpers.error(err); @@ -781,39 +792,16 @@ function FileBrowserInclude(mode, info, doesOpenLast = true) { if (copiedCount) { toast(strings.success); reload(); + } else if (skippedCount) { + toast(strings.skipped); } + if (copiedCount || skippedCount) copiedItems = []; loadingDialog.destroy(); isPasting = false; updatePasteToggler(); } } - async function copyEntry(sourceUrl, targetDirUrl, name, sourceStat) { - const fs = fsOperation(sourceUrl); - const stat = sourceStat || (await fs.stat()); - const entryName = name || stat.name || Url.basename(sourceUrl); - - if (stat.isDirectory) { - const newDirUrl = - await fsOperation(targetDirUrl).createDirectory(entryName); - const entries = await fs.lsDir(); - - for (const entry of entries) { - await copyEntry( - entry.url, - newDirUrl, - entry.name || Url.basename(entry.url), - entry, - ); - } - - return newDirUrl; - } - - const content = await fs.readFile(); - return fsOperation(targetDirUrl).createFile(entryName, content); - } - function isInsideDirectory(sourceUrl, targetUrl) { let source = Url.parse(sourceUrl).url; let target = Url.parse(targetUrl).url; diff --git a/src/settings/appSettings.js b/src/settings/appSettings.js index d7d5248179..a7f912f74c 100644 --- a/src/settings/appSettings.js +++ b/src/settings/appSettings.js @@ -264,6 +264,17 @@ export default function otherSettings() { info: strings["settings-info-app-exclude-folders"], category: categories.filesSessions, }, + { + key: "useFileOperationExclusions", + text: + strings["apply exclusions when copying"] || + "Apply exclusions when copying", + checkbox: values.useFileOperationExclusions, + info: + strings["settings-info-app-use-file-operation-exclusions"] || + "Skip files and folders matching the exclusion patterns during copy and paste operations.", + category: categories.filesSessions, + }, { key: "defaultFileEncoding", text: strings["default file encoding"], diff --git a/src/utils/copyEntry.js b/src/utils/copyEntry.js new file mode 100644 index 0000000000..b910ba2623 --- /dev/null +++ b/src/utils/copyEntry.js @@ -0,0 +1,58 @@ +import fsOperation from "fileSystem"; +import { isExcludedFileOperationPath } from "./fileOperationExclusions"; +import Url from "./Url"; + +/** + * Recursively copy a file or directory while omitting excluded entries. + * + * @param {string} sourceUrl + * @param {string} targetDirUrl + * @param {object} [options] + * @param {string} [options.name] + * @param {object} [options.stat] + * @param {string[]} [options.excludePatterns] + * @param {(entry: {name: string, stat: object}) => Promise} [options.onBeforeCopy] + * @returns {Promise<{url: string|null, copied: number, skipped: number}>} + */ +export default async function copyEntry( + sourceUrl, + targetDirUrl, + { name, stat: sourceStat, excludePatterns = [], onBeforeCopy } = {}, +) { + if (isExcludedFileOperationPath(sourceUrl, excludePatterns)) { + return { url: null, copied: 0, skipped: 1 }; + } + + const sourceFs = fsOperation(sourceUrl); + const stat = sourceStat || (await sourceFs.stat()); + const entryName = name || stat.name || Url.basename(sourceUrl); + + if ( + typeof onBeforeCopy === "function" && + (await onBeforeCopy({ name: entryName, stat })) === false + ) { + return { url: null, copied: 0, skipped: 0 }; + } + + if (!stat.isDirectory) { + const content = await sourceFs.readFile(); + const url = await fsOperation(targetDirUrl).createFile(entryName, content); + return { url, copied: 1, skipped: 0 }; + } + + const url = await fsOperation(targetDirUrl).createDirectory(entryName); + const result = { url, copied: 1, skipped: 0 }; + const entries = await sourceFs.lsDir(); + + for (const entry of entries) { + const child = await copyEntry(entry.url, url, { + name: entry.name || Url.basename(entry.url), + stat: entry, + excludePatterns, + }); + result.copied += child.copied; + result.skipped += child.skipped; + } + + return result; +} diff --git a/src/utils/fileOperationExclusions.js b/src/utils/fileOperationExclusions.js new file mode 100644 index 0000000000..d59eed13ff --- /dev/null +++ b/src/utils/fileOperationExclusions.js @@ -0,0 +1,41 @@ +import picomatch from "picomatch/posix"; +import Url from "utils/Url"; + +/** + * Test whether a file-system URL matches one of the configured exclusion globs. + * Both the URL path and a directory-style version are checked so recursive + * glob patterns also match the excluded directory itself. + * + * @param {string} url + * @param {string[]} patterns + * @returns {boolean} + */ +export function isExcludedFileOperationPath(url, patterns = []) { + if (!url || !Array.isArray(patterns) || !patterns.length) return false; + + const parsedUrl = Url.parse(url).url; + const pathname = Url.pathname(parsedUrl) || parsedUrl; + const normalizedPath = pathname.replace(/\\/g, "/").replace(/\/+$/, ""); + const relativePath = normalizedPath.replace(/^\/+/, ""); + const candidates = [ + normalizedPath, + `${normalizedPath}/`, + relativePath, + `${relativePath}/`, + ]; + + return patterns.some((pattern) => { + if (typeof pattern !== "string" || !pattern.trim()) return false; + + try { + return candidates.some((candidate) => + picomatch.isMatch(candidate, pattern.trim(), { + matchBase: !pattern.includes("/") && !pattern.includes("\\"), + }), + ); + } catch (error) { + console.warn(`Invalid file exclusion pattern: ${pattern}`, error); + return false; + } + }); +} diff --git a/tests/unit/copyEntry.test.js b/tests/unit/copyEntry.test.js new file mode 100644 index 0000000000..136970d8c8 --- /dev/null +++ b/tests/unit/copyEntry.test.js @@ -0,0 +1,137 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +const fileSystem = vi.hoisted(() => { + const entries = new Map([ + [ + "/source", + { + name: "source", + isDirectory: true, + children: ["/source/src", "/source/node_modules"], + }, + ], + [ + "/source/src", + { + name: "src", + isDirectory: true, + children: ["/source/src/index.js"], + }, + ], + [ + "/source/src/index.js", + { + name: "index.js", + isDirectory: false, + content: "console.log('copied');", + }, + ], + [ + "/source/node_modules", + { + name: "node_modules", + isDirectory: true, + children: ["/source/node_modules/package.json"], + }, + ], + [ + "/source/node_modules/package.json", + { + name: "package.json", + isDirectory: false, + content: "{}", + }, + ], + ]); + + return { + entries, + created: [], + }; +}); + +vi.mock("fileSystem", () => ({ + default(url) { + const entry = fileSystem.entries.get(url); + + return { + async stat() { + return { ...entry, url }; + }, + async lsDir() { + return entry.children.map((childUrl) => ({ + ...fileSystem.entries.get(childUrl), + url: childUrl, + })); + }, + async readFile() { + return entry.content; + }, + async createDirectory(name) { + const createdUrl = `${url}/${name}`; + fileSystem.created.push({ type: "directory", url: createdUrl }); + return createdUrl; + }, + async createFile(name, content) { + const createdUrl = `${url}/${name}`; + fileSystem.created.push({ type: "file", url: createdUrl, content }); + return createdUrl; + }, + }; + }, +})); + +import copyEntry from "utils/copyEntry"; + +describe("copyEntry", () => { + beforeEach(() => { + fileSystem.created.length = 0; + }); + + it("prunes excluded directory subtrees", async () => { + const result = await copyEntry("/source", "/target", { + excludePatterns: ["**/node_modules/**"], + }); + + expect(result).toEqual({ + url: "/target/source", + copied: 3, + skipped: 1, + }); + expect(fileSystem.created).toEqual([ + { type: "directory", url: "/target/source" }, + { type: "directory", url: "/target/source/src" }, + { + type: "file", + url: "/target/source/src/index.js", + content: "console.log('copied');", + }, + ]); + }); + + it("copies excluded paths when no patterns are enabled", async () => { + const result = await copyEntry("/source/node_modules", "/target"); + + expect(result.skipped).toBe(0); + expect(fileSystem.created.map(({ url }) => url)).toEqual([ + "/target/node_modules", + "/target/node_modules/package.json", + ]); + }); + + it("does not prepare or replace the target for an excluded source", async () => { + const onBeforeCopy = vi.fn(); + const result = await copyEntry( + "/source/node_modules/package.json", + "/target", + { + excludePatterns: ["**/node_modules/**"], + onBeforeCopy, + }, + ); + + expect(result).toEqual({ url: null, copied: 0, skipped: 1 }); + expect(onBeforeCopy).not.toHaveBeenCalled(); + expect(fileSystem.created).toEqual([]); + }); +}); diff --git a/tests/unit/fileOperationExclusions.test.js b/tests/unit/fileOperationExclusions.test.js new file mode 100644 index 0000000000..65ec3bc40b --- /dev/null +++ b/tests/unit/fileOperationExclusions.test.js @@ -0,0 +1,77 @@ +import { describe, expect, it } from "vitest"; +import { isExcludedFileOperationPath } from "utils/fileOperationExclusions"; + +const patterns = [ + "**/node_modules/**", + "**/.git/**", + "**/*.egg-info/**", + "*.map", +]; + +describe("isExcludedFileOperationPath", () => { + it("matches an excluded directory itself", () => { + expect( + isExcludedFileOperationPath( + "file:///storage/emulated/0/project/node_modules", + patterns, + ), + ).toBe(true); + }); + + it("matches descendants and file basename patterns", () => { + expect( + isExcludedFileOperationPath( + "file:///storage/emulated/0/project/node_modules/pkg/index.js", + patterns, + ), + ).toBe(true); + expect( + isExcludedFileOperationPath( + "sftp://example.com/project/dist/app.js.map", + patterns, + ), + ).toBe(true); + }); + + it("normalizes Windows separators", () => { + expect( + isExcludedFileOperationPath( + "C:\\project\\package.egg-info", + patterns, + ), + ).toBe(true); + }); + + it("matches paths inside SAF tree URIs", () => { + const safRoot = + "content://com.android.externalstorage.documents/tree/primary%3AProjects"; + + expect( + isExcludedFileOperationPath( + `${safRoot}::primary:Projects/app/node_modules`, + patterns, + ), + ).toBe(true); + expect( + isExcludedFileOperationPath( + `${safRoot}::primary:Projects/app/src/index.js`, + patterns, + ), + ).toBe(false); + }); + + it("keeps paths that do not match an exclusion", () => { + expect( + isExcludedFileOperationPath( + "file:///storage/emulated/0/project/src/index.js", + patterns, + ), + ).toBe(false); + }); + + it("ignores empty and invalid patterns", () => { + expect( + isExcludedFileOperationPath("/project/src/index.js", ["", "[invalid"]), + ).toBe(false); + }); +});