From 090986a4b92e83643cc4566ec87d026f8b7de375 Mon Sep 17 00:00:00 2001 From: Raunak Raj <71929976+bajrangCoder@users.noreply.github.com> Date: Fri, 31 Jul 2026 08:50:19 +0530 Subject: [PATCH 1/3] feat(files): skip excluded paths during copy operations --- src/lib/openFolder.js | 15 ++- src/lib/settings.js | 1 + src/pages/fileBrowser/fileBrowser.js | 42 +++---- src/settings/appSettings.js | 11 ++ src/utils/copyEntry.js | 50 +++++++++ src/utils/fileOperationExclusions.js | 41 +++++++ tests/unit/copyEntry.test.js | 121 +++++++++++++++++++++ tests/unit/fileOperationExclusions.test.js | 77 +++++++++++++ 8 files changed, 329 insertions(+), 29 deletions(-) create mode 100644 src/utils/copyEntry.js create mode 100644 src/utils/fileOperationExclusions.js create mode 100644 tests/unit/copyEntry.test.js create mode 100644 tests/unit/fileOperationExclusions.test.js diff --git a/src/lib/openFolder.js b/src/lib/openFolder.js index 5415f7a82..741b6f583 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 6728c80b9..7421f3d2c 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 439ac1501..ac826e378 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,6 +727,7 @@ function FileBrowserInclude(mode, info, doesOpenLast = true) { ); let copiedCount = 0; + let skippedCount = 0; try { for (const url of copiedItems) { @@ -772,8 +774,15 @@ function FileBrowserInclude(mode, info, doesOpenLast = true) { await fsOperation(possibleConflictUrl).delete(); } - await copyEntry(url, targetDirUrl, name, stat); - copiedCount++; + const result = await copyEntry(url, targetDirUrl, { + name, + stat, + excludePatterns: appSettings.value.useFileOperationExclusions + ? appSettings.value.excludeFolders + : [], + }); + if (result.url) copiedCount++; + skippedCount += result.skipped; } } catch (err) { helpers.error(err); @@ -781,39 +790,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 d7d524817..a7f912f74 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 000000000..3f9864611 --- /dev/null +++ b/src/utils/copyEntry.js @@ -0,0 +1,50 @@ +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] + * @returns {Promise<{url: string|null, copied: number, skipped: number}>} + */ +export default async function copyEntry( + sourceUrl, + targetDirUrl, + { name, stat: sourceStat, excludePatterns = [] } = {}, +) { + 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 (!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 000000000..d59eed13f --- /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 000000000..9ebcd42fb --- /dev/null +++ b/tests/unit/copyEntry.test.js @@ -0,0 +1,121 @@ +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", + ]); + }); +}); diff --git a/tests/unit/fileOperationExclusions.test.js b/tests/unit/fileOperationExclusions.test.js new file mode 100644 index 000000000..65ec3bc40 --- /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); + }); +}); From 02bb151d7eb1e64be0e951a5f03095d23c4c3b0d Mon Sep 17 00:00:00 2001 From: Raunak Raj <71929976+bajrangCoder@users.noreply.github.com> Date: Fri, 31 Jul 2026 08:50:35 +0530 Subject: [PATCH 2/3] new translation keys --- src/lang/ar-ye.json | 4 +++- src/lang/be-by.json | 4 +++- src/lang/bn-bd.json | 4 +++- src/lang/cs-cz.json | 4 +++- src/lang/de-de.json | 4 +++- src/lang/en-us.json | 2 ++ src/lang/es-sv.json | 4 +++- src/lang/fr-fr.json | 4 +++- src/lang/he-il.json | 4 +++- src/lang/hi-in.json | 4 +++- src/lang/hu-hu.json | 4 +++- src/lang/id-id.json | 4 +++- src/lang/index.d.ts | 2 ++ src/lang/ir-fa.json | 4 +++- src/lang/it-it.json | 4 +++- src/lang/ja-jp.json | 4 +++- src/lang/ko-kr.json | 4 +++- src/lang/ml-in.json | 4 +++- src/lang/mm-unicode.json | 4 +++- src/lang/mm-zawgyi.json | 4 +++- src/lang/pl-pl.json | 4 +++- src/lang/pt-br.json | 4 +++- src/lang/pu-in.json | 4 +++- src/lang/ru-ru.json | 4 +++- src/lang/tl-ph.json | 4 +++- src/lang/tr-tr.json | 4 +++- src/lang/uk-ua.json | 4 +++- src/lang/uz-uz.json | 4 +++- src/lang/vi-vn.json | 4 +++- src/lang/zh-cn.json | 4 +++- src/lang/zh-hant.json | 4 +++- src/lang/zh-tw.json | 4 +++- 32 files changed, 94 insertions(+), 30 deletions(-) diff --git a/src/lang/ar-ye.json b/src/lang/ar-ye.json index 4a1c09c7c..77d0cf957 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 5d9250879..7c12976c3 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 d2b354ad3..f9ab374f3 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 79492bb4e..402172858 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 bd63f3bf0..8e1dc2964 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 acd7938cd..33a19d29e 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 34f2a3a43..3672aac66 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 e1320be03..16616d1da 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 9ccb79ea8..d956ab77f 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 b73bbdb60..4528b0c05 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 ce9ac47fb..1fc1a55fa 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 2763d37f8..10c860a65 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 447cc8052..0302a017f 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 8c2ac9da5..173b2625d 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 e33878927..7b6ecdc1e 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 c1d28ba35..5d2152ad3 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 42963a18f..978e7ac4d 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 ee8052f1a..8424f45d1 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 e74101b9f..0bbcd285f 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 772654689..6ae58e5d7 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 76f7e05b3..71f0c876e 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 facba76b0..d47cf936d 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 2daa7dee9..5f43ef31f 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 8e101865d..6e24ed9e9 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 ddf0162be..305b01504 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 6154499ba..f49180969 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 080ea08dd..2801b4634 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 15dc46c84..f6ac1dbf0 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 14e338c47..47f030ca3 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 a517f4eca..72fdd8628 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 68e3cb1ba..4ad73928d 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 d42f8b752..581198caa 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." } From f1e5808d61a8ae5310259a306222f09386c57346 Mon Sep 17 00:00:00 2001 From: Raunak Raj <71929976+bajrangCoder@users.noreply.github.com> Date: Fri, 31 Jul 2026 08:58:02 +0530 Subject: [PATCH 3/3] fix the conflict case --- src/pages/fileBrowser/fileBrowser.js | 80 ++++++++++++++-------------- src/utils/copyEntry.js | 10 +++- tests/unit/copyEntry.test.js | 16 ++++++ 3 files changed, 66 insertions(+), 40 deletions(-) diff --git a/src/pages/fileBrowser/fileBrowser.js b/src/pages/fileBrowser/fileBrowser.js index ac826e378..30da2e8f0 100644 --- a/src/pages/fileBrowser/fileBrowser.js +++ b/src/pages/fileBrowser/fileBrowser.js @@ -734,45 +734,6 @@ function FileBrowserInclude(mode, info, doesOpenLast = true) { 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 doesExist = await fsOperation(possibleConflictUrl).exists(); - if (doesExist) { - if (Url.areSame(url, possibleConflictUrl)) { - continue; - } - - const targetStat = await fsOperation(possibleConflictUrl).stat(); - if (stat.isDirectory || targetStat.isDirectory) { - alert( - strings.warning, - strings["folder already exists"] || "Folder already exists", - ); - continue; - } - - 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; - - await fsOperation(possibleConflictUrl).delete(); - } const result = await copyEntry(url, targetDirUrl, { name, @@ -780,6 +741,47 @@ function FileBrowserInclude(mode, info, doesOpenLast = true) { 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 possibleConflictUrl = Url.join(targetDirUrl, name); + if (!(await fsOperation(possibleConflictUrl).exists())) { + return true; + } + + if (Url.areSame(url, possibleConflictUrl)) return false; + + 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; + } + + 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 targetFs.delete(); + return true; + }, }); if (result.url) copiedCount++; skippedCount += result.skipped; diff --git a/src/utils/copyEntry.js b/src/utils/copyEntry.js index 3f9864611..b910ba262 100644 --- a/src/utils/copyEntry.js +++ b/src/utils/copyEntry.js @@ -11,12 +11,13 @@ import Url from "./Url"; * @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 = [] } = {}, + { name, stat: sourceStat, excludePatterns = [], onBeforeCopy } = {}, ) { if (isExcludedFileOperationPath(sourceUrl, excludePatterns)) { return { url: null, copied: 0, skipped: 1 }; @@ -26,6 +27,13 @@ export default async function copyEntry( 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); diff --git a/tests/unit/copyEntry.test.js b/tests/unit/copyEntry.test.js index 9ebcd42fb..136970d8c 100644 --- a/tests/unit/copyEntry.test.js +++ b/tests/unit/copyEntry.test.js @@ -118,4 +118,20 @@ describe("copyEntry", () => { "/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([]); + }); });