Skip to content

Conversation

@sosweetham
Copy link
Member

@sosweetham sosweetham commented Jan 29, 2026

Description of change

Fixes download button on file manager and introduces client side zipped bulk downloads

Issue Number

n/a

Type of change

  • Update (a change which updates existing functionality)
  • Fix (a change which fixes an issue)

How the change has been tested

manual

Change checklist

  • I have ensured that the CI Checks pass locally
  • I have removed any unnecessary logic
  • My code is well documented
  • I have signed my commits
  • My code follows the pattern of the application
  • I have self reviewed my code

Summary by CodeRabbit

  • New Features

    • Multi-file selection with checkboxes and selection controls ("Select All", "Clear Selection") that reset on navigation/view change
    • Batch download that packages selected files into a single ZIP with progress and collision-safe naming
    • Detailed Download Progress modal with per-file statuses, overall progress, cancel/retry actions, and toast notifications
    • Preview modal integrates with the download flow for direct downloads
  • Chores

    • Ignore build artifacts directory (.svelte-kit) in version control

✏️ Tip: You can customize this high-level summary in your review settings.

@sosweetham sosweetham requested a review from coodos as a code owner January 29, 2026 13:42
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 29, 2026

📝 Walkthrough

Walkthrough

Adds multi-file selection and batched downloads to the file manager: checkbox selection UI, batched fetch + per-file and overall progress, JSZip-based ZIP assembly and browser-triggered ZIP download; also adds jszip dependency and ignores .svelte-kit.

Changes

Cohort / File(s) Summary
Dependency
platforms/file-manager/package.json
Added runtime dependency jszip ^3.10.1 for assembling ZIP archives.
Multi-file download & UI
platforms/file-manager/src/routes/(protected)/files/+page.svelte
Large feature: checkbox-based multi-select, select-all/clear, download orchestration with batching, per-file and overall progress/status, JSZip assembly, Download Progress modal UI, preview→downloadUrl integration, error handling, toasts, and selection-clearing on navigation/view changes.
VCS Ignore
platforms/file-manager/.gitignore
Added .svelte-kit to ignore list.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant FileManager as File Manager
    participant JSZip
    participant Browser as Browser

    User->>FileManager: Select multiple files
    User->>FileManager: Click "Download Selected"
    FileManager->>FileManager: Open Progress Modal (preparing)
    loop For each batch
        FileManager->>FileManager: Start parallel fetches for batch
        FileManager->>FileManager: Update per-file progress/status
    end
    FileManager->>JSZip: Add downloaded files to ZIP
    JSZip-->>FileManager: Return ZIP blob
    FileManager->>Browser: Trigger ZIP download (saveAs)
    Browser->>User: Deliver ZIP file
    User->>FileManager: Close modal
    FileManager->>FileManager: Clear selection & cleanup
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Suggested reviewers

  • coodos

Poem

🐰 I hop through folders, gathering bits in a zip,

Batch by batch I hum, each file taking a sip,
I stitch them with a whisker, wrap them up just right,
Click—down they tumble—carrots crunch, code takes flight! 🥕📦

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title is related to the main change but is overly vague; 'Fix/file manager download button' partially describes the changeset but omits the significant feature of multi-file bulk downloads.
Description check ✅ Passed The pull request description is substantially complete, following the template with all required sections filled out (change description, issue number, type of change, testing method, and completed checklist).
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/file-manager-download-button

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@platforms/file-manager/src/routes/`(protected)/files/+page.svelte:
- Around line 1055-1058: The loop adding files to the JSZip instance overwrites
entries when filenames collide; update the block that iterates over
downloadedBlobs and calls zip.file(name, blob) to track used filenames (e.g., a
Set or Map) and, when a name is already present, generate a unique variant by
appending a numeric suffix like " (1)", " (2)" to the base name before calling
zip.file; ensure the uniqueness logic preserves file extensions and increments
the suffix until an unused filename is found so no downloadedBlob is lost.
- Around line 1013-1044: The current use of Promise.all on batchPromises will
abort the entire batch if any single file fails; change the logic to use
Promise.allSettled on batchPromises, then iterate the settled results to push
only fulfilled items into downloadedBlobs and mark corresponding entries in
downloadedFiles/downloadProgress as "done" for fulfilled and "error" for
rejected (update the downloadedFiles status union to include "error"). Ensure
you still update currentFile/currentFileIndex/overallProgress for each file
processed and surface/log per-file errors from the rejected results rather than
throwing to abort the whole operation.
🧹 Nitpick comments (1)
platforms/file-manager/src/routes/(protected)/files/+page.svelte (1)

1109-1112: Cancel does not abort in-flight downloads.

The cancelDownload function hides the modal and resets state, but any in-flight fetch requests continue running in the background. This could waste bandwidth and potentially cause unexpected behavior if the user starts a new download.

Consider using AbortController to properly cancel pending fetch requests.

💡 Sketch of AbortController approach
// Add at module level
let downloadAbortController: AbortController | null = null;

async function downloadSelectedFiles() {
    // ... existing setup ...
    
    downloadAbortController = new AbortController();
    
    // In the fetch call:
    const response = await fetch(url, { signal: downloadAbortController.signal });
    
    // ...
}

function cancelDownload() {
    downloadAbortController?.abort();
    downloadAbortController = null;
    showDownloadModal = false;
    resetDownloadProgress();
}

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
platforms/file-manager/src/routes/(protected)/files/+page.svelte (1)

1443-1516: ⚠️ Potential issue | 🟡 Minor

Add accessible labels to the new checkboxes.
Without labels, screen readers won’t announce what each checkbox does. Adding aria-label keeps the selection UX accessible.

💡 Proposed fix
                                 <input
                                     type="checkbox"
                                     checked={allFilesSelected &&
                                         selectableFiles.length > 0}
                                     disabled={selectableFiles.length === 0}
                                     onclick={toggleSelectAll}
                                     class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 cursor-pointer disabled:cursor-not-allowed disabled:opacity-50"
                                     title={allFilesSelected
                                         ? "Deselect all"
                                         : "Select all files"}
+                                    aria-label="Select all files"
                                 />
                                         <input
                                             type="checkbox"
                                             checked={selectedFileIds.has(
                                                 item.id,
                                             )}
                                             onclick={(e) =>
                                                 toggleFileSelection(item.id, e)}
                                             class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 cursor-pointer"
                                             title="Select for download"
+                                            aria-label={`Select ${item.displayName || item.name} for download`}
                                         />
🤖 Fix all issues with AI agents
In `@platforms/file-manager/src/routes/`(protected)/files/+page.svelte:
- Around line 985-1004: The selection can become stale: before proceeding in the
download block (where selectableFiles, selectedFileIds and filesToDownload are
used), check if filesToDownload.length === 0 and if so call clearSelection(),
show a short info toast (e.g., "No selected files found"), and return early to
avoid the misleading "All 0 file(s) failed" flow; add this guard immediately
before the single-file and zip-download logic.
- Around line 1194-1204: The immediate call to URL.revokeObjectURL(zipUrl) after
link.click() can cancel downloads in some browsers; delay revocation and cleanup
to ensure the download starts first. Change the flow around the zipUrl/link
handling in the download block so after appending link, calling link.click(),
and removing it, you call URL.revokeObjectURL(zipUrl) inside a short setTimeout
(e.g., 100–500ms) or listen for the link’s click/navigation completion before
revoking; update the code that creates zipUrl, link, link.click(), and
URL.revokeObjectURL to use the delayed revocation approach.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@platforms/file-manager/src/routes/`(protected)/files/+page.svelte:
- Around line 1148-1177: The ZIP entry names are used verbatim which allows path
traversal or invalid paths; before de-duplicating and adding to zip, sanitize
the incoming filename in the getUniqueFilename flow: in the getUniqueFilename
function (and/or before calling it for each entry from downloadedBlobs)
normalize the name by stripping any directory components and drive letters,
remove or replace path separators and any ".." segments, trim leading
dots/slashes, and if the result is empty produce a safe fallback name (e.g.,
"file"). Then run your uniqueness logic (usedFilenames, getUniqueFilename) and
call zip.file(uniqueName, blob) with the sanitized, validated uniqueName to
prevent embedding unintended paths.

Comment on lines +1148 to +1177
// Add all successfully downloaded files to zip with unique filenames
const usedFilenames = new Set<string>();

function getUniqueFilename(originalName: string): string {
if (!usedFilenames.has(originalName)) {
usedFilenames.add(originalName);
return originalName;
}

// Split name into base and extension
const lastDotIndex = originalName.lastIndexOf('.');
const hasExtension = lastDotIndex > 0 && lastDotIndex < originalName.length - 1;
const baseName = hasExtension ? originalName.slice(0, lastDotIndex) : originalName;
const extension = hasExtension ? originalName.slice(lastDotIndex) : '';

// Find a unique name by incrementing suffix
let counter = 1;
let uniqueName = `${baseName} (${counter})${extension}`;
while (usedFilenames.has(uniqueName)) {
counter++;
uniqueName = `${baseName} (${counter})${extension}`;
}

usedFilenames.add(uniqueName);
return uniqueName;
}

for (const { name, blob } of downloadedBlobs) {
const uniqueName = getUniqueFilename(name);
zip.file(uniqueName, blob);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Sanitize ZIP entry names to prevent path traversal or invalid paths.
File names are used verbatim when creating ZIP entries. If a shared file name contains path separators or .., the resulting ZIP can embed unintended paths. Sanitize before de-duplication.

🔧 Suggested fix
             // Add all successfully downloaded files to zip with unique filenames
             const usedFilenames = new Set<string>();
 
+            function sanitizeFilename(originalName: string): string {
+                const sanitized = originalName
+                    .replace(/[\\/]/g, "_")
+                    .replace(/\.\.+/g, ".")
+                    .trim();
+                return sanitized || "file";
+            }
+
             function getUniqueFilename(originalName: string): string {
                 if (!usedFilenames.has(originalName)) {
                     usedFilenames.add(originalName);
                     return originalName;
@@
             }
             
             for (const { name, blob } of downloadedBlobs) {
-                const uniqueName = getUniqueFilename(name);
+                const safeName = sanitizeFilename(name);
+                const uniqueName = getUniqueFilename(safeName);
                 zip.file(uniqueName, blob);
             }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Add all successfully downloaded files to zip with unique filenames
const usedFilenames = new Set<string>();
function getUniqueFilename(originalName: string): string {
if (!usedFilenames.has(originalName)) {
usedFilenames.add(originalName);
return originalName;
}
// Split name into base and extension
const lastDotIndex = originalName.lastIndexOf('.');
const hasExtension = lastDotIndex > 0 && lastDotIndex < originalName.length - 1;
const baseName = hasExtension ? originalName.slice(0, lastDotIndex) : originalName;
const extension = hasExtension ? originalName.slice(lastDotIndex) : '';
// Find a unique name by incrementing suffix
let counter = 1;
let uniqueName = `${baseName} (${counter})${extension}`;
while (usedFilenames.has(uniqueName)) {
counter++;
uniqueName = `${baseName} (${counter})${extension}`;
}
usedFilenames.add(uniqueName);
return uniqueName;
}
for (const { name, blob } of downloadedBlobs) {
const uniqueName = getUniqueFilename(name);
zip.file(uniqueName, blob);
// Add all successfully downloaded files to zip with unique filenames
const usedFilenames = new Set<string>();
function sanitizeFilename(originalName: string): string {
const sanitized = originalName
.replace(/[\\/]/g, "_")
.replace(/\.\.+/g, ".")
.trim();
return sanitized || "file";
}
function getUniqueFilename(originalName: string): string {
if (!usedFilenames.has(originalName)) {
usedFilenames.add(originalName);
return originalName;
}
// Split name into base and extension
const lastDotIndex = originalName.lastIndexOf('.');
const hasExtension = lastDotIndex > 0 && lastDotIndex < originalName.length - 1;
const baseName = hasExtension ? originalName.slice(0, lastDotIndex) : originalName;
const extension = hasExtension ? originalName.slice(lastDotIndex) : '';
// Find a unique name by incrementing suffix
let counter = 1;
let uniqueName = `${baseName} (${counter})${extension}`;
while (usedFilenames.has(uniqueName)) {
counter++;
uniqueName = `${baseName} (${counter})${extension}`;
}
usedFilenames.add(uniqueName);
return uniqueName;
}
for (const { name, blob } of downloadedBlobs) {
const safeName = sanitizeFilename(name);
const uniqueName = getUniqueFilename(safeName);
zip.file(uniqueName, blob);
}
🤖 Prompt for AI Agents
In `@platforms/file-manager/src/routes/`(protected)/files/+page.svelte around
lines 1148 - 1177, The ZIP entry names are used verbatim which allows path
traversal or invalid paths; before de-duplicating and adding to zip, sanitize
the incoming filename in the getUniqueFilename flow: in the getUniqueFilename
function (and/or before calling it for each entry from downloadedBlobs)
normalize the name by stripping any directory components and drive letters,
remove or replace path separators and any ".." segments, trim leading
dots/slashes, and if the result is empty produce a safe fallback name (e.g.,
"file"). Then run your uniqueness logic (usedFilenames, getUniqueFilename) and
call zip.file(uniqueName, blob) with the sanitized, validated uniqueName to
prevent embedding unintended paths.

@coodos coodos merged commit 8778466 into main Jan 30, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants