Skip to content

Fix backup save lifecycle, snapshot consistency, and restore safety - #343

Merged
boubou19 merged 56 commits into
masterfrom
algent/backup-lifecycle-fixes
Sep 18, 2026
Merged

boubou19 merged 56 commits into
masterfrom
algent/backup-lifecycle-fixes

Conversation

@Algent

@Algent Algent commented Sep 11, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes several backup and save reliability issues:

  • World saving could still stay disabled in various failures scenarios
  • Snapshot world files in async mode to avoid desync between player data and chunks
  • Validates restore paths, checks file moves, and rolls back failed restores
  • Writes SU data atomically
  • Add a bunch of (AI generated) regression tests that are hopefully meaningful for what is imo a very sensitive surface

Checklist

  • I have tested this PR in DevEnv
  • I have tested this PR in Fullpack
  • This PR is in compliance with the GTNH AI Policy

@Algent
Algent requested a review from boubou19 September 11, 2026 15:39
@Algent Algent added the Bug Fix Fixes a bug. Please link it in the PR if an issue exists for it. label Sep 11, 2026
@Algent
Algent marked this pull request as draft September 11, 2026 16:44
Fail claim-only backups when a claimed dimension is unloaded, before filtering region files. This prevents a successful-looking archive from silently omitting claimed chunks.
Resolve dimension save folders before starting the backup worker, then
select region files from the captured SU claims. Unloaded dimensions
use their registered provider folder without loading the world.

Read claimed chunks with scoped region handles and fresh temporary
files. Cover queued claims, custom folders, negative coordinates,
whole-region and reconstructed backups, and both execution modes.
World-only restores skip unconfigured extras with a warning. Full
restores keep the allowlist to protect unrelated files in the instance.
Always retain the selected world even if a broad global pattern matches.
Fall back to a full-world backup when a claimed dimension is no longer
registered. Removed mods can leave files in custom save folders, so
neither skipping those regions nor aborting every backup is appropriate.
Copy undersized or misaligned regions unchanged instead of opening them
with RegionFile, whose read-write constructor can alter the live file.
Keep the raw bytes for recovery rather than silently dropping them.
Exclude backup storage, the selected archive, and recovery copies from
additional-file relocation. If the ZIP is inside the world being moved,
follow its new path in the preserved world before extracting it.

Exercise the actual GUI preparation and restore methods with real ZIPs.
Replace the full-world fallback with filtering by registered dimension
region folders. Preserve unknown regions unchanged with a warning, so
removed providers cannot disable claim filtering for the whole world.

Cover forced and configured filtering, synchronous and asynchronous
backups, both region modes, and empty claims.
listWorldFiles and addBaseFolderFiles canonicalized every file to test it
against the backup output and staging folders, three canonicalizations per
file. Modern JDKs no longer cache canonical paths, so on Windows that costs
about a millisecond each, on the server thread, while world saving is
suspended: measured 6.6s for a 6250 file world where the walk itself took
0.5s.

Prune at the directory instead. Only a directory can bring backup storage
into a walk, so files below a checked one need no test, which drops the
canonicalizations from three per file to one per directory.
A world with levelSaving set still logs its usual "Saving chunks for level"
line and then saves nothing, because saveAllChunks returns early on
canSave(). Rollback reports are therefore indistinguishable from healthy
sessions in a user's log.

Record the suspension, the resume, and any dimension loaded mid-backup. A
suspend with no matching resume is then readable straight from latest.log.
Applying the target's mode first can leave the temporary file without owner
write, so opening it fails for a target the server could previously write
through group access. Copy the mode once the bytes are on disk.
The GUI moves the current world aside before extracting, but extraction did
not know about that copy. An archive holding entries under a previous
<world>_old, allowed by a broad additional_backup_files pattern, wrote into
the only intact copy while the restore reported success.

Pass the preserved path down and reject any entry that resolves beneath it.
Rollback deleted installed files but left the directories made for them. A
displaced original that was a file could no longer be moved back, because a
directory now occupied its path, so recovery failed and left the user to
repair it by hand.

Track created directories and remove them deepest-first before restoring.
Resolving every registered dimension means one provider that cannot build a
save folder without a world aborts every claim-only backup, not just backups
for players holding claims there.

Leave the dimension unresolved instead. Its regions then fall into the
unknown-folder bucket and are preserved unchanged, as for removed mods.
The GUI made a timestamped directory for displaced extras while extraction
made its own restore-* directory for replaced globals, so a restore left two
places to look. Pass the GUI's directory down and drop it again when nothing
was displaced.
Additional paths may begin with files and collide with extractor-owned recovery data. Keep them under a dedicated child directory.
File symlinks can point into backup storage without appearing as directories. Canonicalize links so the optimized walk keeps the previous exclusion behavior.
Run live-file preparation only after every selected entry has passed CRC and size validation. Corrupt archives leave the current world untouched.
Create replacement staging with owner-only POSIX permissions, then
restore the target mode after writing. Preserve umask behavior for new
files and support replacement of read-only targets.
Skipped entries cannot overwrite the preserved world. Keep path safety
checks unconditional and still reject installed entries that target it.
The current world's external directory links move away with the old
world. Defer their destination checks until then, keeping archive path
validation and global containment checks before preparation. Recheck all
selected destinations before installing any file.
Canonical File paths do not resolve Windows junctions. Resolve existing ancestors and recheck the relocated world before installing files, so aliases cannot escape the restore root or overwrite the preserved world.
@boubou19
boubou19 merged commit d5c1c99 into master Sep 18, 2026
1 check passed
@boubou19
boubou19 deleted the algent/backup-lifecycle-fixes branch September 18, 2026 18:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Bug Fix Fixes a bug. Please link it in the PR if an issue exists for it.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants