feat: folder upload - deel 2
This commit is contained in:
Binary file not shown.
@@ -206,10 +206,16 @@ class UiSmokeGoldenTest(unittest.TestCase):
|
|||||||
self.assertIn('function ensureFolderUploadPicker()', app_js)
|
self.assertIn('function ensureFolderUploadPicker()', app_js)
|
||||||
self.assertIn('function openFolderPicker()', app_js)
|
self.assertIn('function openFolderPicker()', app_js)
|
||||||
self.assertIn('function buildFolderUploadPlan(files, targetPath)', 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('input.setAttribute("webkitdirectory", "")', app_js)
|
||||||
self.assertIn('Folder: ${plan.rootFolderName} (plan only)', app_js)
|
self.assertIn('Folder upload to: ${plan.targetPath}/${plan.rootFolderName}', app_js)
|
||||||
self.assertIn('Folder upload plan ready:', 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('async function handleUploadSelection(event)', app_js)
|
||||||
self.assertIn('uploadElements().input.onchange = handleUploadSelection;', app_js)
|
self.assertIn('uploadElements().input.onchange = handleUploadSelection;', app_js)
|
||||||
self.assertIn('"/api/files/upload"', app_js)
|
self.assertIn('"/api/files/upload"', app_js)
|
||||||
|
|||||||
+140
-3
@@ -70,6 +70,7 @@ let uploadState = {
|
|||||||
conflictResolver: null,
|
conflictResolver: null,
|
||||||
};
|
};
|
||||||
let folderUploadPlanState = {
|
let folderUploadPlanState = {
|
||||||
|
targetPane: "left",
|
||||||
targetPath: "",
|
targetPath: "",
|
||||||
rootFolderName: "",
|
rootFolderName: "",
|
||||||
entries: [],
|
entries: [],
|
||||||
@@ -500,7 +501,7 @@ function showFolderUploadPlan(plan) {
|
|||||||
folderUploadPlanState = plan;
|
folderUploadPlanState = plan;
|
||||||
elements.button.disabled = false;
|
elements.button.disabled = false;
|
||||||
elements.target.textContent = `Upload to: ${plan.targetPath}`;
|
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"}` : ""}`;
|
elements.count.textContent = `${plan.fileCount} file${plan.fileCount === 1 ? "" : "s"}${plan.subfolderCount > 0 ? ` • ${plan.subfolderCount} subfolder${plan.subfolderCount === 1 ? "" : "s"}` : ""}`;
|
||||||
setUploadProgressVisible(true);
|
setUploadProgressVisible(true);
|
||||||
}
|
}
|
||||||
@@ -533,6 +534,7 @@ function openFolderPicker() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
folderUploadPlanState = {
|
folderUploadPlanState = {
|
||||||
|
targetPane: state.activePane,
|
||||||
targetPath: activePaneState().currentPath,
|
targetPath: activePaneState().currentPath,
|
||||||
rootFolderName: "",
|
rootFolderName: "",
|
||||||
entries: [],
|
entries: [],
|
||||||
@@ -610,9 +612,11 @@ function buildFolderUploadPlan(files, targetPath) {
|
|||||||
throw new Error("Folder picker returned multiple roots or invalid relative paths");
|
throw new Error("Folder picker returned multiple roots or invalid relative paths");
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
|
targetPane: folderUploadPlanState.targetPane || state.activePane,
|
||||||
targetPath,
|
targetPath,
|
||||||
rootFolderName,
|
rootFolderName,
|
||||||
entries: plannedEntries.map((entry) => ({
|
entries: plannedEntries.map((entry) => ({
|
||||||
|
file: entry.file,
|
||||||
name: entry.file.name,
|
name: entry.file.name,
|
||||||
size: entry.file.size,
|
size: entry.file.size,
|
||||||
relativePath: entry.relativePath,
|
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 || []);
|
const files = Array.from(event.target.files || []);
|
||||||
event.target.value = "";
|
event.target.value = "";
|
||||||
if (files.length === 0) {
|
if (files.length === 0) {
|
||||||
@@ -636,7 +772,8 @@ function handleFolderSelection(event) {
|
|||||||
}
|
}
|
||||||
showFolderUploadPlan(plan);
|
showFolderUploadPlan(plan);
|
||||||
setError("actions-error", "");
|
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) {
|
} catch (err) {
|
||||||
setUploadProgressVisible(false);
|
setUploadProgressVisible(false);
|
||||||
setActionError("Folder upload", err);
|
setActionError("Folder upload", err);
|
||||||
|
|||||||
Reference in New Issue
Block a user