From 287dddb7b37b73e4074b2ffdf68c2ba2913513f1 Mon Sep 17 00:00:00 2001 From: kodi Date: Sat, 14 Mar 2026 06:57:18 +0100 Subject: [PATCH] feat: folder upload - deel 2 --- .../test_ui_smoke_golden.cpython-313.pyc | Bin 25811 -> 26477 bytes .../tests/golden/test_ui_smoke_golden.py | 12 +- webui/html/app.js | 143 +++++++++++++++++- 3 files changed, 149 insertions(+), 6 deletions(-) diff --git a/webui/backend/tests/golden/__pycache__/test_ui_smoke_golden.cpython-313.pyc b/webui/backend/tests/golden/__pycache__/test_ui_smoke_golden.cpython-313.pyc index 84688b6b98401ab6e7d2aa090807e95e71ceee13..18946876005887bc96d7489a6c90a91b3d20b25a 100644 GIT binary patch delta 847 zcmcb7lJV_1M!wIyyj%=G5d33HW}eSRJ_9wT9a5Ve)L3~Z|J4+k9BF)H@>*Mw$v*`I zzyL0dIOmdPJ96`7}mLg=YL+Q3d3BFZ>@@;qB*<{41AnW3zc4*=y@!8&G#vVrM2 zq3mFKZYT$so)^jqrss!pf$0UI++ccPC=ZBcTr^qBUYB)o@RH!AlVj{vITr^n3tno; zl*d0g!Cnz4z8omN!Csbe#pDmd3M`@g!7D)wp-_R~Ri;d!kYQj@U`S`sT)kPvL4aLH zIjuA=xg;|`Pa!QoCndGWC9^0sxg@`+G9a-eqgbOLCoxa6${?}0GA~(S@&PwdZ@2>2 zip=5?pbDTm%_<#;3bK>?O4N;>|3Ra|0 zl5eG;QVp_JuOzW3J+%bnj#~Z6{qAD*ReFi#iJ2t|i3OQKsfDGf#U&a_0sg@uN;(Qk z`anVbw9K5;V*T9gl*}TaSap>(L_Nf4H=ra;8^UNEh19%~qDnoGRvm@>vecsTqRf(1 zP3tNN6o*+U6r>iV792dg?cSD zMF;3J6ouKD1qG=oP*uec-sUsz&HRjOHV1^?XJ)hB5q5>cW%9f@2~n2=X;(OWZ*cJR z^LFxH5OY2Nl<}E7KSp9QSCjyo+X=QS9R8CTqcqryFO*ha;i%c16#bc&7223LWJGm~a delta 445 zcmaERj`8wIM!wIyyj%=GaO>NaOi!D9<)_s zWCiP+5(=TGhO$jIuv2Da2a8S*lRpS6u!QmlF9tD$LIr}C zm@REt^OaF8Cax>#DMMv0K8t4i2wiq diff --git a/webui/backend/tests/golden/test_ui_smoke_golden.py b/webui/backend/tests/golden/test_ui_smoke_golden.py index fc9923e..c6ba17a 100644 --- a/webui/backend/tests/golden/test_ui_smoke_golden.py +++ b/webui/backend/tests/golden/test_ui_smoke_golden.py @@ -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) diff --git a/webui/html/app.js b/webui/html/app.js index 8a438db..5b52c24 100644 --- a/webui/html/app.js +++ b/webui/html/app.js @@ -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);