From bed3ea7bd0e249e97df3954c34139de792585584 Mon Sep 17 00:00:00 2001 From: Ein Anderssono Date: Mon, 15 Jun 2026 02:23:22 +0200 Subject: [PATCH] v0.9.1: add deep checksum verification --- CHANGELOG.md | 8 ++++++++ Makefile | 2 +- README.md | 1 + RELEASE_NOTES.md | 14 +++++++------- USERGUIDE.md | 8 ++++++++ cmd/photoscli/main.go | 12 ++++++++++++ cmd/photoscli/main_test.go | 33 +++++++++++++++++++++++++++++++++ 7 files changed, 70 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 38c09ca..9ea551a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,14 @@ This changelog is maintained from git history plus the published Gitea release series. Future releases should update this file and publish matching release notes on the release page. +## v0.9.1 + +Deep checksum verification release. + +- Add `verify --deep` to recompute SHA-256 checksums for manifest entries that have checksum metadata. +- Report `checksum-mismatch` when disk contents differ from the manifest checksum. +- Keep normal `verify` unchanged unless `--deep` is selected. + ## v0.9.0 Manifest checksum release. diff --git a/Makefile b/Makefile index ebe7d77..4ee5ce2 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ BINARY := ./bin/photoscli MODULE := gitea.k3s.k0.nu/tools/photocli -VERSION := 0.9.0 +VERSION := 0.9.1 RELEASE_ZIP := ./bin/photoscli-$(VERSION)-macos-arm64.zip RELEASE_NOTES := RELEASE_NOTES.md BRIDGE_DIR := bridge diff --git a/README.md b/README.md index 1cc4dbd..694fe03 100644 --- a/README.md +++ b/README.md @@ -117,6 +117,7 @@ Verify a backup later: ```bash ./bin/photoscli verify --out ./photos-backup --manifest sqlite +./bin/photoscli verify --out ./photos-backup --deep ``` ## Commands diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 7629c62..864e1b4 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,18 +1,18 @@ -# v0.9.0 +# v0.9.1 -This release starts the backup-integrity series with manifest checksums. +This release adds deep checksum verification for manifest-backed backups. ## Highlights -- Add opt-in `--checksum sha256` to store SHA-256 checksums in manifests. -- Support checksums in both JSONL and SQLite manifests. -- Add SQLite migration support for the new checksum column. -- Keep checksum collection disabled by default with `--checksum none`. +- Add `verify --deep` to recompute SHA-256 checksums when manifest entries include checksums. +- Report `checksum-mismatch` when file contents differ from the manifest checksum. +- Keep normal `verify` unchanged unless `--deep` is selected. +- `--deep` works with JSONL and SQLite manifests. ## Assets - `photoscli`: Apple Silicon macOS binary (`darwin/arm64`). -- `photoscli-0.9.0-macos-arm64.zip`: Apple Silicon binary plus README, USERGUIDE, and CHANGELOG. +- `photoscli-0.9.1-macos-arm64.zip`: Apple Silicon binary plus README, USERGUIDE, and CHANGELOG. - `USERGUIDE.md`: standalone user guide. Intel Macs are not currently a supported release target. diff --git a/USERGUIDE.md b/USERGUIDE.md index 88c360e..7db3819 100644 --- a/USERGUIDE.md +++ b/USERGUIDE.md @@ -830,6 +830,14 @@ Store SHA-256 checksums in the manifest when exporting: Checksums are opt-in and stored in JSONL or SQLite manifests. The default is `--checksum none`. +Deep-verify checksum-backed manifests: + +```bash +./bin/photoscli verify --out ./PhotosBackup --deep +``` + +`--deep` recomputes SHA-256 only for manifest entries that already have checksum metadata. + ## Safe Operating Practices - Run `--dry-run` before the first large backup. diff --git a/cmd/photoscli/main.go b/cmd/photoscli/main.go index ac749ff..b37695e 100644 --- a/cmd/photoscli/main.go +++ b/cmd/photoscli/main.go @@ -180,6 +180,7 @@ COMMANDS files are printed as . Exits 2 on missing files. Add --sidecar to verify expected XMP sidecars too. Add --strict with --sidecar to require photoscli schema/generator and exported filename metadata. + Add --deep to verify manifest SHA-256 checksums when present. retry-failed --out Retry assets previously written to failures.jsonl. @@ -2279,6 +2280,7 @@ func cmdVerify(args []string, stdout, stderr io.Writer) int { outDir := flagVal(args, "--out") checkSidecar := hasFlag(args, "--sidecar") strictSidecar := hasFlag(args, "--strict") + deep := hasFlag(args, "--deep") if outDir == "" { fmt.Fprintln(stderr, "error: --out is required") return exitErr @@ -2317,6 +2319,16 @@ func cmdVerify(args []string, stdout, stderr io.Writer) int { bad++ fmt.Fprintf(stdout, "%s\t%s\tsize-mismatch\tmanifest=%d\tdisk=%d\n", id, checkPath, e.Size, info.Size()) } + if deep && e.Checksum != "" { + checksum, err := fileSHA256(filepath.Join(outDir, checkPath)) + if err != nil { + bad++ + fmt.Fprintf(stdout, "%s\t%s\tchecksum-unreadable\n", id, checkPath) + } else if checksum != e.Checksum { + bad++ + fmt.Fprintf(stdout, "%s\t%s\tchecksum-mismatch\tmanifest=%s\tdisk=%s\n", id, checkPath, e.Checksum, checksum) + } + } if checkSidecar { bad += verifySidecar(stdout, outDir, id, checkPath, strictSidecar) } diff --git a/cmd/photoscli/main_test.go b/cmd/photoscli/main_test.go index e3a3826..59877c2 100644 --- a/cmd/photoscli/main_test.go +++ b/cmd/photoscli/main_test.go @@ -4273,6 +4273,39 @@ func TestMoreIntegrityBranches(t *testing.T) { } } +func TestVerifyDeepChecksums(t *testing.T) { + dir := t.TempDir() + if err := os.WriteFile(filepath.Join(dir, "good.jpg"), []byte("abc"), 0644); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(dir, "bad.jpg"), []byte("bad"), 0644); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(dir, "nocheck.jpg"), []byte("data"), 0644); err != nil { + t.Fatal(err) + } + if err := os.Mkdir(filepath.Join(dir, "adir"), 0755); err != nil { + t.Fatal(err) + } + m := manifest.LoadJSONL(dir) + if err := m.OpenAppend(); err != nil { + t.Fatal(err) + } + m.AddEntry(manifest.Entry{ID: "good", Filename: "good.jpg", Path: "good.jpg", Size: 3, Cloud: "local", Checksum: "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad", Exported: time.Now().Unix()}) + m.AddEntry(manifest.Entry{ID: "bad", Filename: "bad.jpg", Path: "bad.jpg", Size: 3, Cloud: "local", Checksum: "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad", Exported: time.Now().Unix()}) + m.AddEntry(manifest.Entry{ID: "nocheck", Filename: "nocheck.jpg", Path: "nocheck.jpg", Size: 4, Cloud: "local", Exported: time.Now().Unix()}) + m.AddEntry(manifest.Entry{ID: "unreadable", Filename: "adir", Path: "adir", Cloud: "local", Checksum: "abc", Exported: time.Now().Unix()}) + m.Close() + out, stderr, rc := runWith([]string{"verify", "--out", dir, "--deep"}, &mockBridge{}) + if rc != exitPartial || stderr != "" || !strings.Contains(out, "checksum-mismatch") || !strings.Contains(out, "checksum-unreadable") || strings.Contains(out, "good.jpg") || strings.Contains(out, "nocheck") { + t.Fatalf("deep verify rc=%d out=%q stderr=%q", rc, out, stderr) + } + out, stderr, rc = runWith([]string{"verify", "--out", dir}, &mockBridge{}) + if rc != exitOK || stderr != "" || strings.Contains(out, "checksum") { + t.Fatalf("plain verify rc=%d out=%q stderr=%q", rc, out, stderr) + } +} + func TestVerifySidecarBranches(t *testing.T) { dir := t.TempDir() subdir := filepath.Join(dir, "sub")