Skip to content

Commit df6d7a1

Browse files
authored
fix: prevent folder paste loops and improve SAF handling (#1474)
- Add validation to prevent pasting folders into themselves or subdirectories - Implement special Termux SAF handling for cut operations with recursive file/folder moving - Fix createFileStructure to handle nested paths (foo/bar) for file:// URIs
1 parent 03eb3a6 commit df6d7a1

File tree

2 files changed

+100
-7
lines changed

2 files changed

+100
-7
lines changed

src/lib/openFolder.js

Lines changed: 62 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -532,6 +532,27 @@ function execOperation(type, action, url, $target, name) {
532532
return;
533533
}
534534

535+
// Prevent pasting a folder into itself or its subdirectories
536+
if (helpers.isDir(clipBoard.$el.dataset.type)) {
537+
const sourceUrl = Url.parse(clipBoard.url).url;
538+
const targetUrl = Url.parse(url).url;
539+
540+
// Check if trying to paste folder into itself
541+
if (sourceUrl === targetUrl) {
542+
alert(strings.warning, "Cannot paste a folder into itself");
543+
return;
544+
}
545+
546+
// Check if trying to paste folder into one of its subdirectories
547+
if (
548+
targetUrl.startsWith(sourceUrl + "/") ||
549+
targetUrl.startsWith(sourceUrl + "\\")
550+
) {
551+
alert(strings.warning, "Cannot paste a folder into its subdirectory");
552+
return;
553+
}
554+
}
555+
535556
let CASE = "";
536557
const $src = clipBoard.$el;
537558
const srcType = $src.dataset.type;
@@ -559,8 +580,47 @@ function execOperation(type, action, url, $target, name) {
559580
if (!confirmation) return;
560581
}
561582
let newUrl;
562-
if (clipBoard.action === "cut") newUrl = await fs.moveTo(url);
563-
else newUrl = await fs.copyTo(url);
583+
if (clipBoard.action === "cut") {
584+
// Special handling for Termux SAF folders - move manually due to SAF limitations
585+
if (
586+
clipBoard.url.startsWith("content://com.termux.documents/tree/") &&
587+
IS_DIR
588+
) {
589+
const moveRecursively = async (sourceUrl, targetParentUrl) => {
590+
const sourceFs = fsOperation(sourceUrl);
591+
const sourceName = Url.basename(sourceUrl);
592+
const targetUrl = Url.join(targetParentUrl, sourceName);
593+
594+
// Create target folder
595+
await fsOperation(targetParentUrl).createDirectory(sourceName);
596+
597+
// Get all entries in source folder
598+
const entries = await sourceFs.lsDir();
599+
600+
// Move all files and folders recursively
601+
for (const entry of entries) {
602+
if (entry.isDirectory) {
603+
await moveRecursively(entry.url, targetUrl);
604+
} else {
605+
const fileContent = await fsOperation(entry.url).readFile();
606+
const fileName = entry.name || Url.basename(entry.url);
607+
await fsOperation(targetUrl).createFile(fileName, fileContent);
608+
await fsOperation(entry.url).delete();
609+
}
610+
}
611+
612+
// Delete the now-empty source folder
613+
await sourceFs.delete();
614+
return targetUrl;
615+
};
616+
617+
newUrl = await moveRecursively(clipBoard.url, url);
618+
} else {
619+
newUrl = await fs.moveTo(url);
620+
}
621+
} else {
622+
newUrl = await fs.copyTo(url);
623+
}
564624
const { name: newName } = await fsOperation(newUrl).stat();
565625
stopLoading();
566626
/**

src/utils/helpers.js

Lines changed: 38 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -396,12 +396,45 @@ export default {
396396
currentUri.includes("com.termux.documents")
397397
)
398398
) {
399-
if (isFile) {
400-
uri = await fsOperation(uri).createFile(pathString);
401-
} else {
402-
uri = await fsOperation(uri).createDirectory(pathString);
399+
// Handle nested paths for regular file:// URIs
400+
const pathParts = pathString.split("/").filter(Boolean);
401+
let currentPath = uri;
402+
let firstCreatedPath = null;
403+
let firstCreatedType = null;
404+
405+
for (let i = 0; i < pathParts.length; i++) {
406+
const isLastPart = i === pathParts.length - 1;
407+
const partName = pathParts[i];
408+
const newPath = Url.join(currentPath, partName);
409+
410+
if (isLastPart && isFile) {
411+
// Create file if it's the last part and we're creating a file
412+
if (!(await fsOperation(newPath).exists())) {
413+
await fsOperation(currentPath).createFile(partName);
414+
if (firstCreatedPath === null) {
415+
firstCreatedPath = newPath;
416+
firstCreatedType = "file";
417+
}
418+
}
419+
} else {
420+
// Create directory for intermediate parts or when creating a folder
421+
if (!(await fsOperation(newPath).exists())) {
422+
await fsOperation(currentPath).createDirectory(partName);
423+
if (firstCreatedPath === null) {
424+
firstCreatedPath = newPath;
425+
firstCreatedType = "folder";
426+
}
427+
}
428+
}
429+
currentPath = newPath;
403430
}
404-
return { uri: uri, type: isFile ? "file" : "folder" };
431+
432+
return {
433+
uri: firstCreatedPath || Url.join(uri, pathParts[0]),
434+
type:
435+
firstCreatedType ||
436+
(isFile && pathParts.length === 1 ? "file" : "folder"),
437+
};
405438
}
406439

407440
for (let i = 0; i < parts.length; i++) {

0 commit comments

Comments
 (0)