Session-gated MinIO save sync with AppImage GUI, CLI edit/session flow, and Gitea release helper. Co-authored-by: Cursor <[email protected]>
3.7 KiB
3.7 KiB
SyncGames protocol (Python ↔ Android)
Schema version: 1
Both clients MUST implement these rules identically. Diverging behavior is a LOPE risk.
Object key layout
Bucket: configured (default syncgames).
| Key | Purpose |
|---|---|
games/<game-id>/meta.json |
Lease, checksums, retention |
games/<game-id>/live/<rel> |
Current SSOT save file(s) |
games/<game-id>/history/<device-id>/<iso-ts>/<rel> |
Immutable snapshot |
retired/<game-id>-<date>/… |
Soft-deleted games |
<game-id>: lowercase slug,[a-z0-9-]+<device-id>: stable per install (e.g.pc-desk,phone-pixel)<iso-ts>: UTCYYYYMMDDTHHMMSSZ<rel>: relative path using/, no..segments
meta.json
{
"schema": 1,
"game_id": "elden-ring-seamless",
"live_hash": "sha256:…",
"file_checksums": {
"ER0000.co2": "sha256:…"
},
"lease": {
"holder": "pc-desk",
"expires_at": "2026-07-14T03:00:00Z",
"session_id": "uuid"
},
"versions_to_keep": 5,
"updated_at": "2026-07-13T22:00:00Z",
"updated_by": "pc-desk"
}
live_hash: SHA-256 of the sorted concatenation ofpath\\0hexdigest\\nfor every live object (canonical tree hash).leasemay benullwhen idle.- Clients MUST treat unknown JSON fields as forward-compatible (ignore).
Lease rules
- Acquire before mutating native installs from SSOT (start session).
- Acquire succeeds if
leaseis null/expired ORholderequals this device. - Default TTL: 6 hours (refreshable by same holder via start again).
- Another device MUST refuse start while lease is valid for a different holder.
- Release on successful end session (set
leaseto null). Crash: wait for expiry or operator clears viadoctor --break-lease(destructive admin).
start_session
- Load
meta.json(if missing, treat as empty live + null lease). - Acquire lease (conditional overwrite: read-modify-write; if lost race, abort).
- Download all
games/<id>/live/*objects. - Install into configured native path(s) (create parents; overwrite existing).
- Persist local session state:
{ parent_live_hash, session_id, started_at }.
end_session
- Require local session state for game.
- Refuse if configured process/AppID still running (Linux); Android warns if user confirms force.
- Compute WIP tree hash from native files.
- Hash gate: if
parent_live_hash != meta.live_hashand notforce, abort (stale WIP). - Upload snapshot to
history/<device>/<ts>/…. - Upload/replace all
live/…objects to match WIP (delete remote live keys no longer present). - Update
meta.json: new hashes,lease=null,updated_by=device. - Prune: list
history/<device>/; keep newestversions_to_keep; delete older prefixes. - Clear local session state.
restore
- User selects
device/tshistory prefix explicitly. - Acquire lease (or require idle + force).
- Copy history objects →
live/. - Update
meta.jsonhashes; leave lease held by restoring device until they end or release. - Never auto-restore.
Checksum algorithm
- Per file: SHA-256 of raw bytes, encoded
sha256:<hex>. - Tree hash: sort relative paths lexicographically (UTF-8), for each path append
path + "\\0" + hex_digest + "\\n", then SHA-256 that byte string, encodedsha256:<hex>.
S3 addressing
- Path-style:
https://syncgames-s3.example.com/syncgames/games/... - Region can be
us-east-1dummy; path-style + custom endpoint required. - TLS required on WAN.
Error codes (CLI / UI mapping)
| Condition | Message key |
|---|---|
| Lease held by other | lease_held |
| Hash gate fail | stale_wip |
| Network / S3 | store_error |
| Game still running | game_running |
| Missing config | config_error |