Skip to content

Fix directory downloads failing with "object not found" - #59

Merged
dentifrag merged 7 commits into
mainfrom
copilot/download-directories-fix
Aug 25, 2026
Merged

Fix directory downloads failing with "object not found"#59
dentifrag merged 7 commits into
mainfrom
copilot/download-directories-fix

Conversation

Copilot AI commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

What this fixes

Directory downloads were completely broken. Every file in a folder download failed.

client.listRecursiveFiles() wraps rclone's operations/list. Its returned entry.Path is relative to the fs root and includes the listed remote prefix:

fs=<root>, remote="media/movies", recurse:true
  -> Path = "media/movies/a.mkv"
  -> Path = "media/movies/sub/b.mkv"

expandDownload assumed the opposite, that Path was relative to the listed directory. It paired srcFs: "<job>:media/movies" with srcRemote: "media/movies/a.mkv", doubling the prefix. Verified against rclone v1.74.4, operations/copyfile with that pairing returns:

{"error": "object not found"}

downloadManager turns that rejection into status: 'error', so users saw every file in a folder download fail. Only a root-level download (dirPath === '') worked, by accident.

The fix

  • Pass the fs root as srcFs and keep the full entry.Path as srcRemote, so the pair resolves. Strip the prefix only when building the destination path, preserving the on-disk layout.
  • Normalize the directory path once, before listing, and use it for both the listing call and the containment prefix. rclone returns an empty list (not an error) for a redundant or trailing separator, so media//movies previously hit the files.length === 0 early return and silently downloaded nothing.
  • Add a containment guard: if any returned entry falls outside the requested directory, delete the temporary remote and throw before anything is enqueued. Fails closed.

Normalizing before the listing call also removes a latent trap: rclone echoes a leading slash back in Path, but filter(Boolean) strips it from the prefix, so a /-prefixed dirPath would have false-rejected legitimate entries.

Tests

test/downloadRoute.test.ts previously mocked listRecursiveFiles with directory-relative paths (a.mkv), encoding the wrong assumption. That is why the bug survived. The mocks now use real rclone shape (media/movies/a.mkv), plus new cases for the containment guard and for redundant, trailing, and leading separators.

Verification

Probed rclone v1.74.4 directly rather than relying on the mocks:

remote passed to operations/list Files returned
media//movies (pre-fix, raw) 0
media/movies (post-fix, normalized) 2

operations/copyfile with the new srcFs/srcRemote pairing succeeds and lands the file at the expected out/movies/a.mkv.

Full suite green: typecheck, lint, format:check, 189 tests, build.

Copilot AI and others added 4 commits August 8, 2026 15:06
Co-authored-by: dentifrag <65632734+dentifrag@users.noreply.github.com>
Co-authored-by: dentifrag <65632734+dentifrag@users.noreply.github.com>
Co-authored-by: dentifrag <65632734+dentifrag@users.noreply.github.com>
Co-authored-by: dentifrag <65632734+dentifrag@users.noreply.github.com>
@dentifrag dentifrag changed the title Harden directory path normalization Fix directory downloads failing with "object not found" Aug 25, 2026
@dentifrag
dentifrag requested a lite review from Copilot August 25, 2026 20:14

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Changes recommended

The new relativePath slicing can allow .. segments or absolute paths in entry.Path to escape the intended destination subdirectory unless additional validation/sanitization is added before enqueuing transfers.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

Fixes folder (directory) downloads failing with “object not found” by correcting how rclone list results are converted into copy jobs, normalizing directory paths before listing, and adding a fail-closed containment guard. This aligns expandDownload’s assumptions with rclone’s operations/list output shape and prevents silent no-op downloads for malformed directory paths.

Changes:

  • Adjusted expandDownload to use srcFs: <jobRemote>: with full entry.Path as srcRemote, while stripping the directory prefix only for destination layout.
  • Normalized dirPath before calling listRecursiveFiles, fixing redundant/trailing/leading separator cases.
  • Updated tests to match rclone’s real returned Path shape and added coverage for normalization + containment guard behavior.
File summaries
File Description
src/server/routes/downloads.ts Fixes directory expansion logic (src/dst pairing, normalization, containment guard).
test/downloadRoute.test.ts Updates mocks to rclone-shaped paths and adds new test cases for normalization and containment rejection.
CHANGELOG.md Documents the folder download fix and the redundant-separator no-op behavior.
Review details
  • Files reviewed: 3/3 changed files
  • Comments generated: 1
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/server/routes/downloads.ts

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟢 Approval recommended

The changes align with the described rclone behavior, add fail-closed safety checks, and include targeted test coverage for the regression and key edge cases.

Review details
  • Files reviewed: 3/3 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

@dentifrag
dentifrag marked this pull request as ready for review August 25, 2026 20:42
@dentifrag
dentifrag merged commit bf4c81b into main Aug 25, 2026
2 checks passed
@dentifrag
dentifrag deleted the copilot/download-directories-fix branch August 25, 2026 20:42
@dentifrag dentifrag mentioned this pull request Aug 25, 2026
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