// OPENBOX DOCS
Save archives
ZIP structure, manifests, roots, retention, and restore validation.
Per-game save backups are ZIP archives stored in save-backups/ inside the data directory. This page documents the archive format, where backups live, retention, and the restore safety rules. The implementation is saves.py with contract coverage in test_saves.py and test_parity_features.py.
Location and naming
- Root:
<data_dir>/save-backups/. - Each game has a hash directory derived from its identity:
game_backup_dir= SHA-256 of"<name>:<path>", first 16 hex chars. - Archive names:
<timestamp>-<label>.zipwhere the label ismanual,on-close, orbefore-restore, and the timestamp isYYYYMMDD-HHMMSS-ffffff. Names sort newest-first, solist_backupsreturns the newest archive first. - Backups are written via a temp file and
os.replace, then the directory is fsynced.
Archive format
A save backup is a ZIP with DEFLATE compression:
manifest.jsonat the root:{ "game": "Game Name", "roots": [ {"path": "/home/you/.config/retroarch/saves", "file": false} ] }fileis true when the root is a single file rather than a directory.roots/<index>/...mirrors each configured save path: for a single-file root,roots/<index>/<filename>; for a directory root,roots/<index>/<relative path>.
The manifest is what restore uses to verify the archive matches the game's current save_paths.
What gets backed up
backup_saves uses the game's save_paths (configured paths, resolved with expanduser().resolve()). Prerequisites and errors:
- No configured path currently exists:
FileNotFoundError"No configured save paths currently exist." - Any backup source path is a symlink:
ValueError"Save backup paths may not be symlinks." - Any file inside a directory root is a symlink:
ValueError"Save backup source contains a symlink: <path>"
Saves are also backed up automatically when backup_on_close is enabled and the game has save_paths: each session close creates an on-close archive, then applies retention. Every restore first creates a before-restore archive of the current state.
Retention
After a backup, enforce_backup_limit trims the oldest archives beyond settings.save_backup_limit (default 10; validated 0..500). A limit of 0 disables trimming. Retention applies to all labels (manual, on-close, before-restore) since it counts every archive in the game's backup directory.
Restore validation
restore_saves refuses anything unsafe, in this order:
- The backup name resolves strictly inside the game's backup directory:
"Save backup not found."otherwise (no path traversal). - A
before-restorebackup of the current state is created first. manifest.jsonmust decode:"Save backup manifest is invalid."otherwise.- The manifest roots must match the game's current
save_pathsin count and order (compared by expanded absolute path):"Save backup roots do not match this game."This is why editing a game's save paths after a backup makes that backup un-restorable until the paths match again. - Every member is validated: no NUL bytes, no absolute paths, no
.., no duplicates, per-member cap 4 GiB (MAX_SAVE_ARCHIVE_MEMBER_BYTES), total cap 32 GiB (MAX_SAVE_ARCHIVE_TOTAL_BYTES). - The destination tree is checked for symlinks before and after each
mkdir, so a symlink planted at a missing intermediate directory cannot redirect the write. - Files are restored with mode
0o600viaatomic_copy_stream.
Restores write files, so they are destructive: the automatic before-restore archive is your safety copy. test_saves.py covers single-file roots, directory roots, relative save paths (resolved against the game, not the process cwd), and the automatic pre-restore snapshot.
Related
- API saves and operations for the routes (
/api/saves/backup,/api/saves/restore, retention) - Library backups for whole-library archives
- Data and recovery for the state store