Fix backup save lifecycle, snapshot consistency, and restore safety - #343
Merged
Merged
Conversation
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.
# Conflicts: # dependencies.gradle
boubou19
approved these changes
Sep 18, 2026
2 tasks done
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fixes several backup and save reliability issues:
Checklist