diff --git a/.gitattributes b/.gitattributes index d7136e4..baf4a54 100644 --- a/.gitattributes +++ b/.gitattributes @@ -4,3 +4,4 @@ *.png binary gradlew text eol=lf *.sh text eol=lf +*.zip binary diff --git a/AGENTS.md b/AGENTS.md index 8eac602..7cc3b0c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -10,7 +10,7 @@ This is a backup tool. A bug that corrupts a backup or touches the original worl - Restores are copy-only. They create a new world and never write into the source world. - The mod never deletes a backup on its own. Every cleanup goes through a user-reviewed plan, and labeled backups are always kept. -- A destination must never read the world while it can still change. All capture work goes through `BackupCoordinator` and the save gate; do not add a path that copies world files outside that flow. +- A destination must never read the world while it can still change. All capture work goes through `SerializedBackupCoordinator` and the save gate; do not add a path that copies world files outside that flow. - An operation that fails must leave the previous state intact. Report failure honestly rather than report success with content half-removed (this applies to remote deletes especially: if the remote refuses to stop exposing a backup, the delete failed). ### 2. Clean-room provenance @@ -19,31 +19,39 @@ Another Git-based Minecraft backup mod exists ("Fast" + "Back", GPL-licensed). T ## Glossary -- **world** — one single-player world directory, identified by a `WorldId` / `WorldIdentity`. -- **capture** — the short phase that copies the world into an immutable source while saves are gated. -- **save gate** — the mechanism that pauses autosave and world writes for the duration of a capture. -- **destination** / **backend** — where a backup lands: a Git repository or a ZIP archive (`BackupBackend`). -- **snapshot** — one Git commit of a world. One repository per world; an optional remote per world. -- **archive** — one ZIP file with a SHA-256 checksum. -- **manifest** — the metadata recorded with each backup, including the Minecraft version it was made with. -- **catalog** — the persistent per-world index of backup records the UI reads. -- **trigger** — what started a backup: manual, world exit, or schedule. -- **label** — a user mark that protects a backup from cleanup. -- **cleanup** — a user-confirmed deletion plan with a preview. -- **import** / **recovery** — bringing old or external backups into the catalog, and restoring or deleting from it. +- **world**: one single-player world directory, identified by a `WorldId` / `WorldIdentity`. +- **capture**: the phase that copies the world into a private folder under `capture-temp`. It reads and hashes each file while it copies it; a capture of the open world reads every file a second time. +- **save gate**: for the open world, the save before a capture and the autosave pause during it; autosave then goes back to how the player set it. The world exit backup captures after the final save instead. +- **destination** / **backend**: where a backup lands: a Git repository or a ZIP archive (`BackupBackend`). +- **snapshot**: one Git commit of a world. One repository per world; an optional remote per world. +- **archive**: one ZIP file with a SHA-256 checksum. +- **manifest**: the metadata recorded with each backup, including the Minecraft version it was made with. +- **catalog**: the persistent index of backup records for all worlds (`catalog.json`) that the UI reads. +- **inventory**: the files and hashes of a world's last backup; the next backup counts its changed files against it. +- **trigger**: what started a backup: manual, world exit, or schedule. +- **label**: a user mark that protects a backup from cleanup. +- **cleanup**: a user-confirmed deletion plan with a preview. +- **deletion mark**: an entry in `deleted-backups.txt` that keeps a deleted backup out of a catalog rebuild while any file of it is left. +- **import** / **recovery**: bringing old or external backups into the catalog, and restoring or deleting from it. ## How a backup happens -A trigger in the client runtime asks `BackupCoordinator` for a backup. The coordinator serializes operations per world and coalesces compatible concurrent triggers. The capture phase copies the world under the save gate on a capture thread; only after the capture is sealed does destination work start asynchronously. Each enabled backend (Git, ZIP) writes its artifact and returns a `DestinationResult`. The result is recorded in the catalog, which the UI screens render. +A trigger in the runtime asks `SerializedBackupCoordinator` for a backup. For the open world, `LiveWorldBackups` has the server save, pauses autosave, and calls `prepareCapture`, which copies the world on a worker. When the copy is complete, autosave goes back to how the player set it, and `createPreparedBackup` queues the destination work. A world exit backup captures after the server's final save. A world that is not open goes through `createBackup`, which captures and then queues in the same way. + +The coordinator lets one capture copy a world at a time and writes the backups of one world in order, while different worlds run in parallel. It never merges triggers: each trigger makes its own backup. + +The capture (`FileSystemBackupCaptureFactory`) copies the world into `capture-temp` with up to four workers and hashes each file while it copies it. A capture of the open world reads and hashes every file a second time. A capture of a closed world reads a file a second time only when the file changed during the copy, or when its time is within 10 seconds of the newest file's or of the clock, or later. The caller says which kind it is (`CaptureKind`). When the world changed, it tries again and copies only the changed files. Destination work starts only after the capture is complete. Each enabled backend (`WorldGitSnapshotStore`, `ZipBackupBackend`) writes its copy from the capture and returns a `DestinationResult`. The coordinator records one `BackupRecord` in the catalog, updates the world's inventory, and deletes the capture. The UI screens render the catalog. ## Where code lives Two production source sets, split by Minecraft coupling: -- `src/main` — the engine. Pure Java, zero Minecraft imports. `core` (coordinator, capture, gates), `storage/git` and `storage/zip` (backends), `storage/management` (cleanup and retention), `catalog`, `model`, `config`, `importing`, `recovery`. -- `src/client` — the Fabric integration. `runtime` (lifecycle, save gates, scheduling), `ui` (screens), `settings`, `integration` (Mod Menu). New logic goes in `src/main` unless it needs a Minecraft class. -- `src/test` — plain JUnit tests against `src/main` and client logic. No Minecraft runtime in tests; keep it that way by keeping logic out of `src/client`. -- `src/main/resources/assets/worldarchive/lang` — every user-visible string. New UI text needs a lang entry, not a literal. +- `src/main`: the engine. Pure Java, zero Minecraft imports. `core` (coordinator, capture, gates), `storage/git` and `storage/zip` (backends), `storage/management` (cleanup and retention), `catalog`, `model`, `config`, `settings` (the settings service, drafts and validation), `importing`, `recovery`, `runtime` (the service graph built from the settings, the open world's backups behind the small `LiveServer` port, and world identity), `ui/model` (screen logic without Minecraft: rows, filters, selections, summaries), and `support` (shared file, JSON, path, and async helpers; it depends on no other WorldArchive package). +- `src/client`: the Fabric integration. `runtime` (the Fabric event adapter, toasts, navigation, and the screens' facade), `ui` (screens), `settings` (the settings screen and folder picker), `integration` (Mod Menu). New logic goes in `src/main` unless it needs a Minecraft class. +- `src/test`: plain JUnit tests against `src/main` and client logic. No Minecraft runtime in tests; keep it that way by keeping logic out of `src/client`. The `e2e` package runs the real engine through the production `ServiceGraph` with real Git and real ZIP files (`Engine`, `TestWorld`); prefer it for backup, restore, delete, and cleanup behavior. +- `src/main/resources/assets/worldarchive/lang`: every user-visible string. New UI text needs a lang entry, not a literal. + +Packages have no dependency cycles. The client `ui` package reaches the runtime and the settings only through `BackupClientFacade`. ## Build and verify @@ -54,7 +62,7 @@ The gates enforce hard ceilings you should design within, not bump into: - 1,000 lines per Java file, 100 lines per method, cyclomatic complexity 15. - Every text file (including `.md` and `.yml`): no tabs, no trailing whitespace, final newline. -Tests are focused. Test real behavior — capture ordering, retention math, catalog merges — not implementation detail or UI wiring. A change to backup, restore, or delete logic ships with a test for that behavior. +Tests are focused. Test real behavior, such as capture ordering, retention math, and catalog merges, not implementation detail or UI wiring. A change to backup, restore, or delete logic ships with a test for that behavior. ## Taste diff --git a/CHANGELOG.md b/CHANGELOG.md index 9034c38..8b05ec0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,186 @@ # Changelog -## 0.4.0 (2026-09-15) +## 0.4.0 (2026-09-22) + +### Changed + +- **Breaking.** WorldArchive no longer reads settings files from WorldArchive 0.1.0. The + settings screen says so and offers **Reset settings**, which keeps the old file. Settings + from 0.1.1 and newer load as before. +- **Breaking.** WorldArchive no longer reads the shared Git repository of WorldArchive 0.1.0. + Its backups stay out of the backup list until you import them one time. Open **World + Backups** and click **Import**. Paste the local path of the repository into **Repository + address**, then click **Find Backups from Repository**. The default path was + `.minecraft/worldarchive/worldarchive.git`. +- Each backup copies the world faster, up to four files at the same time, so autosave stops for + a shorter time. A backup of an open world checks every file twice. A backup of a closed world + reads each file once. +- A backup never makes a Git or ZIP folder that you chose when that folder is missing, for + example because its drive is not connected. The backup to that folder fails and says that the + folder cannot be reached. When you save the settings with a new folder, WorldArchive makes it. + The default folders in `worldarchive` are still made when a backup needs them. +- **Verify** tells you when the checksum file (`.sha256`) of a ZIP backup is missing. +- Git backups are much faster. A backup starts a few Git commands instead of one for each file, + and it copies only new Git LFS files. The full repository check with `git fsck` no longer + runs after each backup. **Verify** still reads every file of the backup. +- A ZIP backup writes its archive one time and reads it back one time to check it. ZIP backups + no longer use the temporary folder of your system. +- A delete of many backups and the backup scan at start are much faster. A Git delete also + frees its space on this computer at once. +- **World Backups** checks the storage of each world one time when you open it. Before, it + scanned every backup folder again each time you came back to it. +- The schedule skips a world that did not run since its last backup, for example while the + game is paused. A skip makes no save and shows no message. Before, the save before each + scheduled backup changed the world, so the schedule never skipped. +- **Sync**, **Verify**, and **Restore** now have a **Cancel** button. +- The delete prompt names each backup by date and label. It also says how many copies are on + the Git remote and how many backups have a label. The delete removes the remote copies too. +- A delete prompt no longer expires. WorldArchive deletes only the copies that the prompt + showed. If a backup changed after you confirmed, WorldArchive deletes nothing of it and + says why. +- Delete results name each backup by date and label. +- The cleanup confirmation shows the same date, label, and changes as the preview. The cleanup + result says why each kept backup stayed. It also warns you when Git could not free the space + yet. +- The backup filter matches labels, triggers as the list shows them, such as "world exit", + and the start of a backup ID. It no longer matches the world name, which every row has. +- **Review Cleanup** stays off until you save the storage limits. A storage limit accepts a + comma as the decimal mark, for example "1,5". +- The settings file no longer stores the default backup folders. A copied or moved game folder + uses its own backup folders. +- **Defaults** on the settings screen keeps your backup folders. +- Before WorldArchive upgrades an older settings file, it keeps a copy named + `worldarchive.json.schema.bak`. +- The screens say "remote" where they said "GitHub". The **Worlds** tab now says that + **Sync** tries a failed upload again. Before, it said that the next backup does. + +### Fixed + +- A save that replaces `level.dat` while a backup copies the world no longer fails the backup. + The backup copies the file again. +- WorldArchive can back up a world that contains a `.git` folder or file, for example a data + pack that is a Git clone. The backup leaves out every `.git` entry. +- A saves folder or a world folder that is a symbolic link or a Windows junction now works. + WorldArchive uses the real folder. A link inside a world still stops a backup. +- When a file name or a link stops a backup, the message names the file. +- Cancel during a Git upload keeps the copies that are already complete. +- When the backup list cannot be saved after a backup, the result says so. The next start lists + the backup again. +- A second game that uses the same WorldArchive folder no longer deletes the world copy of a + backup that the first game makes. +- A backup of the open world no longer turns autosave on again after `/save-off`. +- A schedule that you turned off no longer gives warnings. +- Menus no longer freeze while a backup runs. A change to a backup folder while a backup runs + gets a message that asks you to try again later. +- An error in WorldArchive while a world opens, saves, or closes no longer reaches the game, also + a Java error such as a missing class. A backup that the error stops fails with a notice. +- A backup that you start while the previous one gives autosave back no longer leaves autosave + off. WorldArchive asks you to try again in a moment. +- One file with a date in the future, as in some downloaded maps, no longer lets a backup of a + closed world miss a change that the game made during the copy. +- When you quit during a backup, the game waits at most about 35 seconds, world copy included. + A notice at the next start says what did not finish. +- A world whose name has no readable text gets automatic backups. WorldArchive uses the folder + name. +- A label keeps the exact text that you typed. Before, the credential filter could change it. +- One damaged object in a Git repository, for example after a power loss, no longer makes every + later Git backup of that world fail. +- A lock file that a stopped Git process left behind no longer blocks backups and deletes. +- Hooks in a world's Git repository never run. A `core.hooksPath` in your global Git settings no + longer breaks Git backups, and WorldArchive no longer writes Git LFS hooks into that folder. +- A remote that does not allow changes to `main` no longer makes uploads fail. +- When a remote refuses a delete, nothing changes on the remote. Before, `main` could move + although the delete failed. +- A delete of an imported Git backup also removes it from the world's remote. +- A deleted Git backup no longer comes back after a restart. +- A remote with more than 256 backups can be imported. When one backup of an import cannot be + downloaded, the other backups still import. +- When this computer's Git copy of a backup is damaged, a restore uses the remote copy and + repairs the local one. +- WorldArchive writes Git backups to disk before it reports them complete. It also writes the + Git LFS files that a restore or an import downloads to disk before it records them. +- When **Verify** or a restore finds a damaged Git LFS file, WorldArchive moves it out of the + way. The next backup writes the file again, and a restore gets it again from the remote. + Before, later backups used the damaged file and reported success. +- A file that holds the text of a Git LFS pointer, for example in a data pack cloned without + Git LFS, no longer makes every Git backup of the world fail. +- A cancel right after a Git backup was written no longer reports that backup as failed. It + waits for its upload as pending sync. +- **Open Folder** no longer makes an empty Git folder. The first Git backup also works when such + a folder is there. +- A ZIP backup that lost its `.sha256` file stays in the list, and it verifies and restores. +- The next backup removes the files that a crash left in a ZIP folder. It keeps the checksum + file (`.sha256`) of an archive that is missing for a moment, for example while a sync tool + delivers it. +- **Verify** and import refuse a ZIP archive with a folder entry that has the name of a file. No + restore can write such an archive. +- **Verify** and import refuse a ZIP backup whose checksum file is gone and whose end is cut + short, because other ZIP programs cannot open it. WorldArchive can still restore it. +- The ZIP folder of a world can be a link to another drive. Before, the list and delete did + not see the archives there. +- **Verify** says "unavailable" instead of "damaged" when it cannot read a ZIP archive, for + example on a drive that is not connected. +- Messages for a full drive, a missing ZIP folder, and a failed delete say what happened and + what to do. +- An import never writes into the folder that you choose. Identical copies of one archive + import one time. +- On Windows, WorldArchive tries again for a short time when another program, such as an + antivirus scanner, holds a file. +- A delete that leaves a copy now reports that it failed. Before, a backup whose remote refused + the delete counted as deleted. +- A delete fails when a backup folder that you chose cannot be reached, for example on a drive + that is not connected, and nothing is made in its place. Before, the delete said that the + backup was deleted, and the backup came back later. If you remove a world's folder by hand + from WorldArchive's default backup folders, you can still delete its backups. +- A deleted backup whose files are on a drive that was not connected at a start no longer comes + back when the drive is back. +- A delete that you confirmed before a sync finished no longer deletes the remote copy that the + sync made. WorldArchive deletes nothing and says that the backup changed. +- A delete of a ZIP backup whose archive has a new name now also deletes that archive when only + the checksum file kept the old name. +- A failure message with a line break no longer stops a delete. +- After you remove a world's remote from its settings, a delete removes the copies on this + computer and says that the copy on the old remote stays. A backup whose only copy is on that + remote stays in the list, and the result tells you to add the remote again. +- Cleanup decides from the files on disk. A backup whose last copy is gone leaves the backup + list. Before, cleanup could promise a copy that no longer existed. +- Cleanup never offers the last intact copy of a labeled backup. Before, it could offer the Git + copy of a labeled backup whose ZIP archive **Verify** had found damaged. +- WorldArchive moves a damaged backup list to `catalog.json.corrupt-