fix(files): save purchased files atomically with rootless ownership #162

Merged
chaum merged 2 commits from fix/filebrowser-purchase-filing into main 2026-09-30 12:58:37 +00:00
Collaborator

Problem and behavior

Purchased files could fail to appear in Files because the backend UID cannot write into FileBrowser's rootless-owned folders. The current FileBrowser API path also checks for name conflicts before opening with truncation, so override=false does not protect concurrent saves. This change publishes complete purchased files through a no-overwrite hard link, using the rootless namespace when host permissions require it.

Changes

  • Bring the branch up to current main while preserving its refund handling, purchase cache and response compatibility.
  • Write to a private, uniquely named temporary file; finish and sync its contents before publishing it. Preserve existing files and choose numbered names when needed.
  • Use the same no-overwrite behavior for direct and rootless writes. ln -T treats an existing directory or dangling symlink as a conflict instead of placing a file inside it.
  • Validate filenames, refuse symlink destination directories, bound collision retries, verify the exact input length, and clean temporary files on ordinary failures.
  • Keep the Files copy optional and retain the durable purchase cache when Files storage is unavailable.

Validation

  • Full isolated backend suite: 1,568 passed, zero failed; four existing hardware/live tests ignored.
  • Production cargo check passed with the existing 16 warnings.
  • Real filesystem tests cover 24 concurrent writes, original-file preservation, directory/symlink conflicts, invalid names, collision exhaustion, permission fallback and temporary-file cleanup.
  • The actual namespace shell script is tested for quoting, ownership/mode, exact bytes and truncated input.
  • Scratch-only tests on the development node passed with real Podman rootless ownership and four concurrent writers; all fixtures were removed.
  • Combined testing with #161: 1,585 passed, zero failed; four existing tests ignored.

Targets the next release after 1.8.21. Signed 1.8.21 artifacts are unchanged.

Review evidence: next-release PR review.

## Problem and behavior Purchased files could fail to appear in Files because the backend UID cannot write into FileBrowser's rootless-owned folders. The current FileBrowser API path also checks for name conflicts before opening with truncation, so `override=false` does not protect concurrent saves. This change publishes complete purchased files through a no-overwrite hard link, using the rootless namespace when host permissions require it. ## Changes - Bring the branch up to current main while preserving its refund handling, purchase cache and response compatibility. - Write to a private, uniquely named temporary file; finish and sync its contents before publishing it. Preserve existing files and choose numbered names when needed. - Use the same no-overwrite behavior for direct and rootless writes. `ln -T` treats an existing directory or dangling symlink as a conflict instead of placing a file inside it. - Validate filenames, refuse symlink destination directories, bound collision retries, verify the exact input length, and clean temporary files on ordinary failures. - Keep the Files copy optional and retain the durable purchase cache when Files storage is unavailable. ## Validation - Full isolated backend suite: 1,568 passed, zero failed; four existing hardware/live tests ignored. - Production `cargo check` passed with the existing 16 warnings. - Real filesystem tests cover 24 concurrent writes, original-file preservation, directory/symlink conflicts, invalid names, collision exhaustion, permission fallback and temporary-file cleanup. - The actual namespace shell script is tested for quoting, ownership/mode, exact bytes and truncated input. - Scratch-only tests on the development node passed with real Podman rootless ownership and four concurrent writers; all fixtures were removed. - Combined testing with #161: 1,585 passed, zero failed; four existing tests ignored. Targets the next release after 1.8.21. Signed 1.8.21 artifacts are unchanged. Review evidence: [next-release PR review](https://source.archipelago-foundation.org/lfg2025/archy/src/branch/main/docs/pr-review-20260930.md).
ssmithx added 1 commit 2026-09-29 22:57:23 +00:00
Every paid download logged "filing into filebrowser/Music/... failed
(non-fatal): Permission denied". The purchase played in-app but never
appeared in Files. FileBrowser's folders belong to its rootless container
range (host uid 100000, mode 755). This service is host uid 1000, outside
that range, so it can read them but not create files in them.

New container::filebrowser::save_new_file:
- Writes directly when the folder allows it.
- Otherwise writes through `podman unshare`, where that uid range is
  ours: to a temp file, then chowned to the folder's owner, set to 0644,
  and hard-linked into place. FileBrowser never sees a partial file and an
  existing file is never replaced. A missing folder is created and given
  its parent's owner. No sudo.
- Keeps the "name (2).ext" de-duplication the RPC did inline.

Checked the unshare script on amishparadise in a scratch folder owned
like FileBrowser's: new folder + file OK, owner/mode right, no clobber,
no temp file left, and the service can read the result.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
chaum added 1 commit 2026-09-30 11:33:15 +00:00
chaum changed title from fix(files): file purchased content into FileBrowser folders again to fix(files): save purchased files atomically with rootless ownership 2026-09-30 11:34:44 +00:00
chaum force-pushed fix/filebrowser-purchase-filing from 0677924a64 to 0677924a64 2026-09-30 11:35:05 +00:00 Compare
chaum merged commit b02ba4100d into main 2026-09-30 12:58:37 +00:00
Sign in to join this conversation.