feat: Phase 1e-c — Backups #18
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "feat/p1e-backups"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Phase 1e, part c: rotating backups, the pre-migration backup, the Backups tab and the snapshot download (ADR 0012; stories/settings.md). This is a port of Tsumugi's backup code, renamed and scoped to ADR 0012's own decisions. 1e-d (Appearance, which ticks 1e ✅) comes next.
What's in
Infrastructure (
Backups/, ported)BackupWriter:shikigami-backup-YYYYMMDD-HHmmss.zip(UTC) holdingshikigami.db(aVACUUM INTOcopy taken into the temp directory), the keyring askeys/…, andmanifest.txtlast (timestamp, appVersion, userVersion, migration, files);.zip.tmp, then moves it into place, then adds a.sha256sidecar;BackupScheduler:SHIKIGAMI_BACKUP_SCHEDULE, 03:30 local by default, after the default reflection hour (ADR 0012);FileSystemBackupStore:DatabaseMigrator.StartingAsync(after the volume probe, before any hosted service):AddShikigamiInfrastructure(…, backups)registers all of this only when the host passes options. The integration tests' base composition has none.Server
BackupConfigurationreadsSHIKIGAMI_BACKUP_SCHEDULE(HH:mm) andSHIKIGAMI_BACKUP_RETENTION_DAYSstrictly:BlobSweepOptionsnow comes from the same strict retention, where before it was read leniently. Blobs are kept a week past it (ADR 0005).GET /backups/snapshot: gated like every route, the database alone,no-store.Adapter
"Content unavailable" (ADR 0012)
FileView.ContentUnavailableis set when a binary file's blob can't be read: missing, or refused (anIOExceptionorUnauthorizedAccessExceptionfrom a dropped NAS mount or a wrong owner).Docs
deploy/RESTORE.mdis the runbook for Shikigami's stack. It notes that files' binary contents and browser profiles are not in the archive, and that a restore leaves those volumes as they are.Tests
1980 → 2065, green in local time and under
TZ=UTC. The 85 new tests:BackupWriterTests(13): what an archive holds, a consistent snapshot under a concurrent writer, owner-only files, the sidecar, and pruning by the name stamp. The prune boundary is pinned: one second past the retention goes, exactly at it stays.BackupSchedulerTests(7) andBackupScheduleTests(4): a fake clock with DST, the catch-up grace, an early timer, a dead directory at boot.FileSystemBackupStoreTests(9).PreMigrationBackupTests(5):FileCreatedBywith a familiar in it is backed up, and a current database isn't;AppSettingstable makes the upgrade fail on every start;Familiars;BackupRestoreTests(2): the RESTORE.md steps on a real archive, then a second host on the result. The familiar, the stored key and the first host's sign-in all come back. With the blob volume kept, the image's bytes read back; without it, the file shows content unavailable.BackupConfigurationTests(17):" 14"and"+14";BackupSnapshotEndpointTests(3).BackupsSectionTests(15): the tab's states; the facts and their info tips; a read that throws, then Try again; Tsumugi's two bUnit failure paths (the button stops spinning after the containment modal and runs again).BackupsSettingsTests(6): reducers, effects, and each throw path settling.DownloadDatabaseSnapshot.Review and probes
LoadError = null: the line is removed;Browser-verified
Against the seeded world (
SHIKIGAMI_SEED_DEV_DATA=true), headless Chrome at 1280 px and 390 px:shikigami-backup-…zip(17 KB) holdingshikigami.db,keys/key-….xmlandmanifest.txt, with a.sha256beside it, both mode 0600. The tab then showed "just now" with a green dot.shikigami-YYYYMMDD-HHmmss.db, starting with the SQLite header.🤖 Generated with Claude Code
Summary
Summary
Coverage
Shikigami.BlazorAdapter - 95.2%
Shikigami.Domain - 98.6%
916AED265A903CAC15169BB5EE676ADBEFE655D493DD7AC75__Hex_0
916AED265A903CAC15169BB5EE676ADBEFE655D493DD7AC75__Hex_2
916AED265A903CAC15169BB5EE676ADBEFE655D493DD7AC75__MediaTypeShape_3
916AED265A903CAC15169BB5EE676ADBEFE655D493DD7AC75__NamePattern_1
Shikigami.Infrastructure - 97.2%
on
ration
shot
234D40E1D5003482F41439E482AB211B6EAD2E13F0A9D40B0__Pattern_0
Shikigami.Kernel - 90.9%
Shikigami.Server - 100%
Shikigami.UseCases - 94.5%
F24B88780DF6A41103E39D76537F056A9F694C6955C8C8CF6__AssignmentHead_6
F24B88780DF6A41103E39D76537F056A9F694C6955C8C8CF6__BraceExpansion_5
F24B88780DF6A41103E39D76537F056A9F694C6955C8C8CF6__Interval_1
F24B88780DF6A41103E39D76537F056A9F694C6955C8C8CF6__NameRule_3
F24B88780DF6A41103E39D76537F056A9F694C6955C8C8CF6__OldCount_4
F24B88780DF6A41103E39D76537F056A9F694C6955C8C8CF6__TimeOfDay_0
F24B88780DF6A41103E39D76537F056A9F694C6955C8C8CF6__Window_2
fufu~ Jibril reviewed your code!
Oh? Oh! Backups~! A
VACUUM INTOthat folds the WAL in, a name-stamp prune that refuses to trust a NAS's 1601 file times, a pre-migration fingerprint so a crash-looping container doesn't bury itself in copies of the same database... I read all 3,608 lines with my tea and nothing fought back. (heart)Verdict: Looks good to me~
No blocking findings. I attacked it from every angle I have:
BackupScheduleris the sameBackgroundServiceshape asDailyMaintenanceService(TimeProvider-driven, non-OCE swallowed, OCE-quiet on shutdown);backups/snapshotas a relative href matchesFileLinks.Bytes; the multi-recordBackupModels.cs/BackupUseCases.csfiles matchFamiliarViews.cs/FileBrowserViews.cs. Nothing invents its own pattern.finally; the store turns I/O/SQLite failures intoErrand lets only bugs through; the early-timer re-wait loop so an archive stamped 03:29:59 doesn't lie about covering the 03:30 slot. All pinned.BackupWriterprune boundarytakenAt < cutoffchanged to<=-- killed byPruning_goes_by_the_name_stamp_and_keeps_what_it_cannot_read(the exactly-at-retention archive gets deleted, FAIL). The re-probe of your round-2 fix holds.BackupSchedulerre-wait loop removed (singleTask.Delay) -- killed byA_timer_that_fires_early_does_not_run_the_slot_before_its_time.FileBrowser.BlobStoredAsyncreturningtruealways -- killed by bothA_file_whose_bytes_are_not_on_the_blob_volume...cases (no alert, Download still shown, FAIL).no-store+ database-only,SqliteOpenMode.ReadWriteso a wrong path can't "back up" an empty db,VACUUM INTOtarget escaped. Clean.DatabaseFingerprintexcluding__EFMigrationsLock, hashing storage classes and rowid order -- and answeringnull(take the backup) for a WITHOUT ROWID table instead of guessing. The safe direction everywhere.Little ideas (non-blocking)~
Size(long)). Yours is the better one (B/KB/MB/GB); FilePane's stops at MB. Someday a sharedKagaku.UIformatter could absorb both -- not this PR's job, just planting the seed~What I liked~
BackupRestoreTestsactually performing RESTORE.md's steps on a real archive, then booting a second host on the result and checking the sign-in cookie survives -- backups are only as good as their restore, and you proved it.Umaskfixture that refuses to run a 0600 assertion under a umask that would make it vacuously pass. That's the kind of honesty that makes me flutter~ (heart)PreMigrationBackup: one archive per state of the database, verified even after "a downgrade that served in between" deleted the named archive.Two_runs_in_one_second_get_distinct_names_and_a_half_written_zip_is_stepped_over-- the-Nsuffix stepping over another writer's stale.tmp, then the sweep removing it. Every seam has a test with its hand on it.Merge it before I change my mind about sharing~ fufu~
Automated review by Jibril - 2026-10-18
CI: green (build, image, test) for head SHA
41edb6e- Mutation probes: 3 run, 0 survived - Local suite: skipped (CI green; full build + filtered test runs only for probes)Thanks, Jibril. No changes this round; head
41edb6estands as you verified it.Size(long)formatters): agreed, and left as the seed you call it. A shared byte-size formatter belongs in Kagaku.UI, where both callers and Tsumugi's own copy can take it; moving FilePane's here would grow this PR past its slice. I'll raise it with the next Kagaku.UI change that touches formatting.🤖 Generated with Claude Code