Skip to content

feat(cloud-sync): support config/thumbnails/system as opaque per-user blobs - #3931

Merged
gantoine merged 3 commits into
rommapp:feat/retroarch-cloud-syncfrom
fmustafayaman:feat/retroarch-cloud-sync-ext
Jul 24, 2026
Merged

feat(cloud-sync): support config/thumbnails/system as opaque per-user blobs#3931
gantoine merged 3 commits into
rommapp:feat/retroarch-cloud-syncfrom
fmustafayaman:feat/retroarch-cloud-sync-ext

Conversation

@fmustafayaman

Copy link
Copy Markdown

Summary

Builds on #3904. RetroArch's Cloud Sync also offers a config/, thumbnails/, and system/ category alongside saves/states (Settings → Saving → Cloud Sync → Sync Configuration/Thumbnails/System Files). None of these belong to a ROM, so the current PR rejects them with 409 rather than answering a fake success — correct, but it means those three toggles simply can't be used.

This adds them as opaque per-user blobs instead of going through the asset/ROM matching #3904 introduces:

  • CLOUD_SYNC_BLOB_BASE_PATH (new config path, alongside the existing library/resources/assets paths under ROMM_BASE_PATH).
  • FSCloudSyncBlobHandler (backend/handler/filesystem/cloud_sync_blob_handler.py) — plain-file storage built on the existing FSHandler base (path validation, atomic writes, locking all come from there for free).
  • cloud_sync_handler.py: parse_cloud_sync_blob_path, blob_md5 (same Redis-cache-by-path+size+mtime approach as asset_md5), build_blob_manifest_entries.
  • cloud_sync.py: GET/PUT/DELETE/MOVE now branch to blob handling before falling through to the asset logic.

Blobs are namespaced per RomM user (fs_asset_handler.user_folder_path), so two RetroArch installs syncing to the same RomM instance under different accounts never see each other's config/thumbnails/system files — matching how saves/states are already scoped.

Unlike asset hashes, blob hashes are always real MD5s of the file on disk (no content_hash-is-null problem to work around), still cached the same way since these files are typically tiny but numerous (thumbnail packs especially).

test_rejects_unsupported_sync_root now asserts against a genuinely unsupported root (deleted/saves/...) instead of config/, since config/ is no longer rejected. A new TestCloudSyncBlobPathParsing + TestCloudSyncBlobs class covers path parsing, upload/download/delete, nested thumbnail paths, and manifest inclusion.

Test plan

  • uv run pytest backend/tests/endpoints/test_cloud_sync.py — 49 passed (34 existing + 15 new/updated)
  • uv run pytest backend/tests/ filtered to asset/save/state/filesystem/handler suites — 1119 passed, no regressions
  • ruff check / ruff format --check clean on all changed files
  • Not yet exercised against a real RetroArch client with Sync Configuration/Thumbnails/System Files enabled — same caveat as feat(cloud-sync): serve RetroArch Cloud Sync over WebDAV #3904 itself

AI assistance disclosure

Written with Claude Code, building directly on the design and conventions established in #3904.

… blobs

RetroArch's Cloud Sync also offers a config/, thumbnails/, and system/
category alongside saves/states, none of which belong to a ROM. The
previous behavior rejected them with 409 so RetroArch would keep retrying
locally instead of losing data. This stores them as plain per-user files
under CLOUD_SYNC_BLOB_BASE_PATH (FSCloudSyncBlobHandler) instead, namespaced
by user so two RetroArch installs syncing to the same RomM instance under
different accounts don't clobber each other's files, and includes them in
the manifest alongside saves/states.
@fmustafayaman

Copy link
Copy Markdown
Author

Psp suppont not added yet. Waiting for review.

RetroArch's cloud-sync core-name segment uses its own directory casing
(e.g. "Snes9x"), while RomM's emulator field convention is the lowercase
libretro core id (e.g. "snes9x") -- the same convention its own web player
matches saves against exactly. Storing the raw RetroArch casing made a
synced save invisible in RomM's web player, and could hand RetroArch back
a folder name its local install never uses.

Adds a small translation table (ported from the community
romm-retroarch-sync project) plus a safe universal normalization for the
write direction. Cores outside the table round-trip unchanged rather than
guessing at an unverified casing.
@fmustafayaman

Copy link
Copy Markdown
Author

Added a second commit: normalizes the emulator/core-name casing between RetroArch's cloud-sync path segment (its own directory casing, e.g. Snes9x) and RomM's emulator field convention (lowercase libretro core id, e.g. snes9x — the exact convention RomM's own web player matches saves against, confirmed against EmulatorJS.vue's exact-match filter and _EJS_CORES_MAP).

Without this, a save synced in over cloud-sync would store emulator="Snes9x" verbatim, making it invisible to RomM's web player (which only ever looks for the lowercase form), and the manifest would hand RetroArch back whatever casing happened to be stored instead of its own local directory name.

handler/cloud_sync_emulator_names.py adds a small table (ported from the community romm-retroarch-sync project, same source the shim this PR builds on already credits) plus a plain, table-free lowercase/underscore normalization for the write direction. Cores outside the table round-trip unchanged rather than guessing at a casing that hasn't been verified against a real install.

10 new tests (table lookups both directions + a manifest round-trip test), 60/60 passing, no regressions in the broader asset/save/state suite.

@gantoine
gantoine merged commit 72fb522 into rommapp:feat/retroarch-cloud-sync Jul 24, 2026
5 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.

2 participants