feat: folder upload - deel 2

This commit is contained in:
kodi
2026-03-14 06:57:18 +01:00
parent e2e206573d
commit 287dddb7b3
3 changed files with 149 additions and 6 deletions
@@ -206,10 +206,16 @@ class UiSmokeGoldenTest(unittest.TestCase):
self.assertIn('function ensureFolderUploadPicker()', app_js)
self.assertIn('function openFolderPicker()', app_js)
self.assertIn('function buildFolderUploadPlan(files, targetPath)', app_js)
self.assertIn('function handleFolderSelection(event)', app_js)
self.assertIn('function folderDirectoryPaths(plan)', app_js)
self.assertIn('async function ensureFolderDirectoryExists(path)', app_js)
self.assertIn('async function executeFolderUploadPlan(plan)', app_js)
self.assertIn('async function handleFolderSelection(event)', app_js)
self.assertIn('input.setAttribute("webkitdirectory", "")', app_js)
self.assertIn('Folder: ${plan.rootFolderName} (plan only)', app_js)
self.assertIn('Folder upload plan ready:', app_js)
self.assertIn('Folder upload to: ${plan.targetPath}/${plan.rootFolderName}', app_js)
self.assertIn('await apiRequest("POST", "/api/files/mkdir", {', app_js)
self.assertIn('await uploadFileRequest(targetPath, entry.file, overwrite);', app_js)
self.assertIn('Folder upload: preparing', app_js)
self.assertIn('Folder upload: ${uploadState.successfulCount} uploaded, ${uploadState.skippedCount} skipped', app_js)
self.assertIn('async function handleUploadSelection(event)', app_js)
self.assertIn('uploadElements().input.onchange = handleUploadSelection;', app_js)
self.assertIn('"/api/files/upload"', app_js)
+140 -3
View File
@@ -70,6 +70,7 @@ let uploadState = {
conflictResolver: null,
};
let folderUploadPlanState = {
targetPane: "left",
targetPath: "",
rootFolderName: "",
entries: [],
@@ -500,7 +501,7 @@ function showFolderUploadPlan(plan) {
folderUploadPlanState = plan;
elements.button.disabled = false;
elements.target.textContent = `Upload to: ${plan.targetPath}`;
elements.currentFile.textContent = `Folder: ${plan.rootFolderName} (plan only)`;
elements.currentFile.textContent = `Folder: ${plan.rootFolderName}`;
elements.count.textContent = `${plan.fileCount} file${plan.fileCount === 1 ? "" : "s"}${plan.subfolderCount > 0 ? `${plan.subfolderCount} subfolder${plan.subfolderCount === 1 ? "" : "s"}` : ""}`;
setUploadProgressVisible(true);
}
@@ -533,6 +534,7 @@ function openFolderPicker() {
return;
}
folderUploadPlanState = {
targetPane: state.activePane,
targetPath: activePaneState().currentPath,
rootFolderName: "",
entries: [],
@@ -610,9 +612,11 @@ function buildFolderUploadPlan(files, targetPath) {
throw new Error("Folder picker returned multiple roots or invalid relative paths");
}
return {
targetPane: folderUploadPlanState.targetPane || state.activePane,
targetPath,
rootFolderName,
entries: plannedEntries.map((entry) => ({
file: entry.file,
name: entry.file.name,
size: entry.file.size,
relativePath: entry.relativePath,
@@ -622,7 +626,139 @@ function buildFolderUploadPlan(files, targetPath) {
};
}
function handleFolderSelection(event) {
function folderDirectoryPaths(plan) {
const paths = new Set([`${plan.targetPath}/${plan.rootFolderName}`]);
plan.entries.forEach((entry) => {
const parts = entry.relativePath.split("/").filter(Boolean);
if (parts.length <= 1) {
return;
}
let current = `${plan.targetPath}/${plan.rootFolderName}`;
for (let index = 0; index < parts.length - 1; index += 1) {
current = `${current}/${parts[index]}`;
paths.add(current);
}
});
return Array.from(paths).sort((left, right) => left.split("/").length - right.split("/").length);
}
async function ensureFolderDirectoryExists(path) {
const segments = path.split("/");
const name = segments.pop();
const parentPath = segments.join("/");
try {
await apiRequest("POST", "/api/files/mkdir", {
parent_path: parentPath,
name,
});
return;
} catch (err) {
if (err.code !== "already_exists") {
throw err;
}
}
await apiRequest("GET", `/api/browse?${new URLSearchParams({ path }).toString()}`);
}
function updateFolderUploadProgress(plan, currentName, index) {
const elements = uploadElements();
elements.target.textContent = `Folder upload to: ${plan.targetPath}/${plan.rootFolderName}`;
elements.currentFile.textContent = currentName ? `Current file: ${currentName}` : `Folder: ${plan.rootFolderName}`;
elements.count.textContent = `${Math.min(index + 1, plan.fileCount)}/${plan.fileCount} files`;
elements.button.disabled = true;
setUploadProgressVisible(true);
}
async function executeFolderUploadPlan(plan) {
uploadState.active = true;
uploadState.targetPath = `${plan.targetPath}/${plan.rootFolderName}`;
uploadState.overwriteAll = false;
uploadState.skipAll = false;
uploadState.successfulCount = 0;
uploadState.skippedCount = 0;
uploadState.cancelled = false;
uploadState.files = plan.entries.map((entry) => entry.file);
uploadState.index = 0;
setError("actions-error", "");
showFolderUploadPlan(plan);
try {
const directories = folderDirectoryPaths(plan);
for (const directoryPath of directories) {
await ensureFolderDirectoryExists(directoryPath);
}
outer:
for (let index = 0; index < plan.entries.length; index += 1) {
const entry = plan.entries[index];
const relativeParts = entry.relativePath.split("/").filter(Boolean);
const fileName = relativeParts[relativeParts.length - 1];
const parentSegments = relativeParts.slice(0, -1);
const targetPath = parentSegments.length
? `${plan.targetPath}/${plan.rootFolderName}/${parentSegments.join("/")}`
: `${plan.targetPath}/${plan.rootFolderName}`;
uploadState.index = index;
updateFolderUploadProgress(plan, entry.relativePath, index);
let overwrite = uploadState.overwriteAll;
while (true) {
try {
await uploadFileRequest(targetPath, entry.file, overwrite);
uploadState.successfulCount += 1;
break;
} catch (err) {
if (err.code !== "already_exists") {
throw err;
}
if (uploadState.skipAll) {
uploadState.skippedCount += 1;
break;
}
const choice = await promptUploadConflict(fileName, targetPath, err.message);
if (choice === "overwrite") {
overwrite = true;
continue;
}
if (choice === "overwrite_all") {
uploadState.overwriteAll = true;
overwrite = true;
continue;
}
if (choice === "skip") {
uploadState.skippedCount += 1;
break;
}
if (choice === "skip_all") {
uploadState.skipAll = true;
uploadState.skippedCount += 1;
break;
}
uploadState.cancelled = true;
break outer;
}
}
}
if (uploadState.successfulCount > 0 || uploadState.skippedCount > 0) {
await loadBrowsePane(plan.targetPane);
}
if (uploadState.cancelled) {
setStatus(`Folder upload: ${uploadState.successfulCount}/${plan.fileCount} file${plan.fileCount === 1 ? "" : "s"} uploaded before cancel`);
} else if (uploadState.skippedCount > 0) {
setStatus(`Folder upload: ${uploadState.successfulCount} uploaded, ${uploadState.skippedCount} skipped`);
} else {
setStatus(`Folder upload: ${uploadState.successfulCount} file${uploadState.successfulCount === 1 ? "" : "s"} uploaded`);
}
} catch (err) {
if (uploadState.successfulCount > 0 || uploadState.skippedCount > 0) {
await loadBrowsePane(plan.targetPane);
}
setActionError("Folder upload", err);
} finally {
resetUploadProgress();
}
}
async function handleFolderSelection(event) {
const files = Array.from(event.target.files || []);
event.target.value = "";
if (files.length === 0) {
@@ -636,7 +772,8 @@ function handleFolderSelection(event) {
}
showFolderUploadPlan(plan);
setError("actions-error", "");
setStatus(`Folder upload plan ready: ${plan.rootFolderName} (${plan.fileCount} file${plan.fileCount === 1 ? "" : "s"})`);
setStatus(`Folder upload: preparing ${plan.rootFolderName} (${plan.fileCount} file${plan.fileCount === 1 ? "" : "s"})`);
await executeFolderUploadPlan(plan);
} catch (err) {
setUploadProgressVisible(false);
setActionError("Folder upload", err);