Skip to content

feat(cloud-sync): read-only WebDAV browsing (PROPFIND) for the rom library - #3937

Closed
fmustafayaman wants to merge 1 commit into
rommapp:feat/retroarch-cloud-syncfrom
fmustafayaman:feat/webdav-rom-browser
Closed

feat(cloud-sync): read-only WebDAV browsing (PROPFIND) for the rom library#3937
fmustafayaman wants to merge 1 commit into
rommapp:feat/retroarch-cloud-syncfrom
fmustafayaman:feat/webdav-rom-browser

Conversation

@fmustafayaman

Copy link
Copy Markdown

Summary

Adds read-only WebDAV directory browsing (PROPFIND, plus a fake LOCK/UNLOCK handshake some clients require) to the same /api/cloud-sync WebDAV surface RetroArch's Cloud Sync uses. This is unrelated to RetroArch's own sync traffic -- verified against its source, RetroArch's Cloud Sync client never issues PROPFIND -- it's purely so a real WebDAV client (iOS Files' "Connect to Server", Cyberduck, ...) can mount the same URL and browse the rom library (and current saves/states) as plain files.

Ports the retroarch-webdav-romm shim's romBrowser.ts + webdavXml.ts design:

  • roms/<platform_fs_slug>/<file> -- lists platforms with roms, then roms within a platform, using the same display-name logic as the shim (fs_name_no_ext.zip for multi-file roms, the real per-file name for single-file roms). Read-only: no PUT/DELETE for this tree.
  • saves/ / states/ -- browses the current cloud-sync manifest as a virtual directory tree. Unlike the shim, this only shows current entries, not full per-revision history (RomM's own web UI already covers save/state history; re-deriving that here felt out of scope for a browsing convenience).
  • LOCK/UNLOCK -- fake, always-succeeds. Some WebDAV clients (iOS Files by report) won't complete "Connect to Server" without a server that at least answers these, even read-only.

For actually downloading a rom file, GET/HEAD redirect (307) to RomM's existing /api/roms/{id}/content/{file_name} endpoint rather than reimplementing Range support, the multi-file zip cache, and (in production) nginx's X-Accel-Redirect -- that endpoint already accepts HTTP Basic Auth alongside OAuth, so the same credentials carry over on the redirect.

Found along the way

Rom.has_multiple_files/.files depend on a deferred column_property and a relationship that get_roms_scalar doesn't eager-load -- accessing them outside that query's own session raised DetachedInstanceError. Fixed by re-fetching the visibility-filtered rom ids via get_roms_by_ids (which does eager-load them via @with_details) instead of using the raw scalar results directly.

Test plan

  • 71/71 cloud-sync tests passing (11 new: PROPFIND for root/roms/platform/rom-file/saves-states, LOCK/UNLOCK, GET redirect, 404s, visibility)
  • ruff check clean

…brary

RetroArch's own Cloud Sync client never issues PROPFIND (verified against
its source, already noted in this router's docstring), so this isn't on
RetroArch's actual sync path -- it's for a real WebDAV client (iOS Files'
"Connect to Server", Cyberduck, ...) to mount the same /api/cloud-sync
URL and browse/download the library as plain files, same as the
retroarch-webdav-romm shim's romBrowser.ts + webdavXml.ts.

Adds PROPFIND for roms/<platform>/<file> (RomM's own library, read-only --
no PUT/DELETE) and saves/states/ (the current cloud-sync manifest; unlike
the shim, only current entries are browsable here, not full history --
RomM's own web UI covers that). LOCK/UNLOCK are a fake always-succeeds
handshake some WebDAV clients require before they'll mount a server at
all, matching the shim.

GET/HEAD for a rom file redirects (307) to RomM's existing
/api/roms/{id}/content/{file_name} endpoint rather than reimplementing
Range support, multi-file zip caching and (in production) nginx
X-Accel-Redirect -- Basic Auth carries over on the redirect since that
endpoint already accepts it alongside OAuth.

Found and fixed along the way: Rom.has_multiple_files / .files depend on
columns/relationships get_roms_scalar doesn't eager-load, so accessing
them outside the query's own session raised DetachedInstanceError --
re-fetch the visibility-filtered ids via get_roms_by_ids (which does
eager-load them) instead of using the raw scalar results directly.
@gantoine
gantoine self-requested a review July 24, 2026 15:40
@fmustafayaman

Copy link
Copy Markdown
Author

Closing in favor of merging this into #3933 instead, to keep everything on one branch.

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.

1 participant