From fbc37b8d8da4a3ad650f218df2533844d8dcfa40 Mon Sep 17 00:00:00 2001 From: Ein Anderssono Date: Mon, 15 Jun 2026 01:58:38 +0200 Subject: [PATCH] v0.8.4: add strict sidecar verification --- CHANGELOG.md | 7 +++++++ Makefile | 2 +- README.md | 6 ++++++ RELEASE_NOTES.md | 12 ++++++------ USERGUIDE.md | 8 ++++++++ cmd/photoscli/main.go | 23 ++++++++++++++++++++--- cmd/photoscli/main_test.go | 38 +++++++++++++++++++++++++++++++++++++- 7 files changed, 85 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f9604fa..a357f07 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ 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.8.4 + +Strict XMP sidecar verification release. + +- Add `verify --sidecar --strict` to require photoscli XMP schema metadata, sidecar generator metadata, and matching exported filename metadata. +- Keep existing `verify --sidecar` behavior unchanged for backup-wide existence, readability, and asset-ID checks. + ## v0.8.3 XMP sidecar inspection release. diff --git a/Makefile b/Makefile index 15ed6b5..46d9179 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ BINARY := ./bin/photoscli MODULE := gitea.k3s.k0.nu/tools/photocli -VERSION := 0.8.3 +VERSION := 0.8.4 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 0ad8e2a..37cb79e 100644 --- a/README.md +++ b/README.md @@ -363,6 +363,12 @@ Verify generated sidecars with: photoscli verify --out ./backup --sidecar ``` +For stricter checks against recent photoscli-generated XMP sidecars: + +```bash +photoscli verify --out ./backup --sidecar --strict +``` + Inspect one generated sidecar with: ```bash diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 3a4c5e6..bcf2551 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,17 +1,17 @@ -# v0.8.3 +# v0.8.4 -This release adds XMP sidecar inspection for quick validation and scripting. +This release adds strict XMP sidecar verification. ## Highlights -- Add `sidecar inspect ` to print photoscli metadata embedded in a generated sidecar. -- Add `sidecar inspect --json` for scriptable output. -- Keep `verify --sidecar` for backup-wide checks and use `sidecar inspect` for one-file investigation. +- Add `verify --sidecar --strict` to require photoscli schema metadata, sidecar generator metadata, and matching exported filename metadata. +- Keep existing `verify --sidecar` behavior unchanged for basic sidecar checks. +- Use strict mode when validating sidecars generated by recent photoscli versions. ## Assets - `photoscli`: Apple Silicon macOS binary (`darwin/arm64`). -- `photoscli-0.8.3-macos-arm64.zip`: Apple Silicon binary plus README, USERGUIDE, and CHANGELOG. +- `photoscli-0.8.4-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 080aa04..5386930 100644 --- a/USERGUIDE.md +++ b/USERGUIDE.md @@ -573,6 +573,14 @@ Verify sidecars after an export: This reports missing, zero-byte, unreadable, or asset-ID mismatched `.xmp` files. +Use strict verification for sidecars generated by recent photoscli versions: + +```bash +./bin/photoscli verify --out ./PhotosBackup --sidecar --strict +``` + +Strict mode also checks photoscli schema metadata, generator metadata, and the exported filename recorded inside the sidecar. + Inspect one generated sidecar when troubleshooting or scripting: ```bash diff --git a/cmd/photoscli/main.go b/cmd/photoscli/main.go index edffaf2..9be6dc8 100644 --- a/cmd/photoscli/main.go +++ b/cmd/photoscli/main.go @@ -172,7 +172,8 @@ COMMANDS verify --out [--manifest jsonl|sqlite] Verify that manifest entries point to files that exist on disk. Missing files are printed as . Exits 2 on missing files. - Add --sidecar to verify expected XMP sidecars too. + Add --sidecar to verify expected XMP sidecars too. Add --strict with + --sidecar to require photoscli schema/generator and exported filename metadata. retry-failed --out Retry assets previously written to failures.jsonl. @@ -2117,6 +2118,7 @@ func cmdDiff(args []string, stdout, stderr io.Writer, bridge photos.Bridge) int func cmdVerify(args []string, stdout, stderr io.Writer) int { outDir := flagVal(args, "--out") checkSidecar := hasFlag(args, "--sidecar") + strictSidecar := hasFlag(args, "--strict") if outDir == "" { fmt.Fprintln(stderr, "error: --out is required") return exitErr @@ -2156,7 +2158,7 @@ func cmdVerify(args []string, stdout, stderr io.Writer) int { fmt.Fprintf(stdout, "%s\t%s\tsize-mismatch\tmanifest=%d\tdisk=%d\n", id, checkPath, e.Size, info.Size()) } if checkSidecar { - bad += verifySidecar(stdout, outDir, id, checkPath) + bad += verifySidecar(stdout, outDir, id, checkPath, strictSidecar) } } if bad > 0 { @@ -2166,7 +2168,7 @@ func cmdVerify(args []string, stdout, stderr io.Writer) int { return exitOK } -func verifySidecar(stdout io.Writer, outDir, id, checkPath string) int { +func verifySidecar(stdout io.Writer, outDir, id, checkPath string, strict bool) int { xmpPath := sidecarPath(filepath.Join(outDir, checkPath)) rel, err := filepath.Rel(outDir, xmpPath) if err != nil || strings.HasPrefix(rel, "..") { @@ -2191,6 +2193,21 @@ func verifySidecar(stdout io.Writer, outDir, id, checkPath string) int { fmt.Fprintf(stdout, "%s\t%s\tsidecar-asset-mismatch\n", id, rel) return 1 } + if strict { + meta := inspectXMP(data) + if meta["xmpSchemaVersion"] != "2" { + fmt.Fprintf(stdout, "%s\t%s\tsidecar-schema-missing\n", id, rel) + return 1 + } + if meta["sidecarGenerator"] == "" { + fmt.Fprintf(stdout, "%s\t%s\tsidecar-generator-missing\n", id, rel) + return 1 + } + if meta["exportedFilename"] != filepath.Base(checkPath) { + fmt.Fprintf(stdout, "%s\t%s\tsidecar-filename-mismatch\n", id, rel) + return 1 + } + } return 0 } diff --git a/cmd/photoscli/main_test.go b/cmd/photoscli/main_test.go index 31f9649..3fbce66 100644 --- a/cmd/photoscli/main_test.go +++ b/cmd/photoscli/main_test.go @@ -4217,12 +4217,48 @@ func TestVerifySidecarBranches(t *testing.T) { oldRead := readFileFunc readFileFunc = func(string) ([]byte, error) { return nil, fmt.Errorf("read") } var out bytes.Buffer - if got := verifySidecar(&out, subdir, "x1", "../asset.jpg"); got != 1 || !strings.Contains(out.String(), "sidecar-unreadable") { + if got := verifySidecar(&out, subdir, "x1", "../asset.jpg", false); got != 1 || !strings.Contains(out.String(), "sidecar-unreadable") { t.Fatalf("expected unreadable with rel fallback, got=%d out=%q", got, out.String()) } readFileFunc = oldRead } +func TestVerifySidecarStrict(t *testing.T) { + dir := t.TempDir() + media := filepath.Join(dir, "photo.jpg") + if err := os.WriteFile(media, []byte("data"), 0644); err != nil { + t.Fatal(err) + } + if err := writeXMPSidecar(sidecarPath(media), xmpSidecarData{AssetID: "x1", ExportedFilename: "photo.jpg"}); err != nil { + t.Fatal(err) + } + var out bytes.Buffer + if got := verifySidecar(&out, dir, "x1", "photo.jpg", true); got != 0 || out.Len() != 0 { + t.Fatalf("strict valid got=%d out=%q", got, out.String()) + } + if err := os.WriteFile(sidecarPath(media), []byte(`photoscli:assetID="x1"`), 0644); err != nil { + t.Fatal(err) + } + out.Reset() + if got := verifySidecar(&out, dir, "x1", "photo.jpg", true); got != 1 || !strings.Contains(out.String(), "sidecar-schema-missing") { + t.Fatalf("strict schema got=%d out=%q", got, out.String()) + } + if err := os.WriteFile(sidecarPath(media), []byte(``), 0644); err != nil { + t.Fatal(err) + } + out.Reset() + if got := verifySidecar(&out, dir, "x1", "photo.jpg", true); got != 1 || !strings.Contains(out.String(), "sidecar-generator-missing") { + t.Fatalf("strict generator got=%d out=%q", got, out.String()) + } + if err := os.WriteFile(sidecarPath(media), []byte(``), 0644); err != nil { + t.Fatal(err) + } + out.Reset() + if got := verifySidecar(&out, dir, "x1", "photo.jpg", true); got != 1 || !strings.Contains(out.String(), "sidecar-filename-mismatch") { + t.Fatalf("strict filename got=%d out=%q", got, out.String()) + } +} + func TestSidecarInspect(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "photo.xmp")