v0.8.4: add strict sidecar verification
pipeline / test (push) Has been cancelled
pipeline / build (push) Has been cancelled

This commit is contained in:
Ein Anderssono
2026-06-15 01:58:38 +02:00
parent 32a5819c86
commit fbc37b8d8d
7 changed files with 85 additions and 11 deletions
+7
View File
@@ -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. 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 ## v0.8.3
XMP sidecar inspection release. XMP sidecar inspection release.
+1 -1
View File
@@ -1,6 +1,6 @@
BINARY := ./bin/photoscli BINARY := ./bin/photoscli
MODULE := gitea.k3s.k0.nu/tools/photocli MODULE := gitea.k3s.k0.nu/tools/photocli
VERSION := 0.8.3 VERSION := 0.8.4
RELEASE_ZIP := ./bin/photoscli-$(VERSION)-macos-arm64.zip RELEASE_ZIP := ./bin/photoscli-$(VERSION)-macos-arm64.zip
RELEASE_NOTES := RELEASE_NOTES.md RELEASE_NOTES := RELEASE_NOTES.md
BRIDGE_DIR := bridge BRIDGE_DIR := bridge
+6
View File
@@ -363,6 +363,12 @@ Verify generated sidecars with:
photoscli verify --out ./backup --sidecar 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: Inspect one generated sidecar with:
```bash ```bash
+6 -6
View File
@@ -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 ## Highlights
- Add `sidecar inspect <file.xmp>` to print photoscli metadata embedded in a generated sidecar. - Add `verify --sidecar --strict` to require photoscli schema metadata, sidecar generator metadata, and matching exported filename metadata.
- Add `sidecar inspect <file.xmp> --json` for scriptable output. - Keep existing `verify --sidecar` behavior unchanged for basic sidecar checks.
- Keep `verify --sidecar` for backup-wide checks and use `sidecar inspect` for one-file investigation. - Use strict mode when validating sidecars generated by recent photoscli versions.
## Assets ## Assets
- `photoscli`: Apple Silicon macOS binary (`darwin/arm64`). - `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. - `USERGUIDE.md`: standalone user guide.
Intel Macs are not currently a supported release target. Intel Macs are not currently a supported release target.
+8
View File
@@ -573,6 +573,14 @@ Verify sidecars after an export:
This reports missing, zero-byte, unreadable, or asset-ID mismatched `.xmp` files. 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: Inspect one generated sidecar when troubleshooting or scripting:
```bash ```bash
+20 -3
View File
@@ -172,7 +172,8 @@ COMMANDS
verify --out <dir> [--manifest jsonl|sqlite] verify --out <dir> [--manifest jsonl|sqlite]
Verify that manifest entries point to files that exist on disk. Missing Verify that manifest entries point to files that exist on disk. Missing
files are printed as <asset-id><TAB><filename>. Exits 2 on missing files. files are printed as <asset-id><TAB><filename>. 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 <dir> retry-failed --out <dir>
Retry assets previously written to failures.jsonl. 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 { func cmdVerify(args []string, stdout, stderr io.Writer) int {
outDir := flagVal(args, "--out") outDir := flagVal(args, "--out")
checkSidecar := hasFlag(args, "--sidecar") checkSidecar := hasFlag(args, "--sidecar")
strictSidecar := hasFlag(args, "--strict")
if outDir == "" { if outDir == "" {
fmt.Fprintln(stderr, "error: --out is required") fmt.Fprintln(stderr, "error: --out is required")
return exitErr 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()) fmt.Fprintf(stdout, "%s\t%s\tsize-mismatch\tmanifest=%d\tdisk=%d\n", id, checkPath, e.Size, info.Size())
} }
if checkSidecar { if checkSidecar {
bad += verifySidecar(stdout, outDir, id, checkPath) bad += verifySidecar(stdout, outDir, id, checkPath, strictSidecar)
} }
} }
if bad > 0 { if bad > 0 {
@@ -2166,7 +2168,7 @@ func cmdVerify(args []string, stdout, stderr io.Writer) int {
return exitOK 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)) xmpPath := sidecarPath(filepath.Join(outDir, checkPath))
rel, err := filepath.Rel(outDir, xmpPath) rel, err := filepath.Rel(outDir, xmpPath)
if err != nil || strings.HasPrefix(rel, "..") { 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) fmt.Fprintf(stdout, "%s\t%s\tsidecar-asset-mismatch\n", id, rel)
return 1 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 return 0
} }
+37 -1
View File
@@ -4217,12 +4217,48 @@ func TestVerifySidecarBranches(t *testing.T) {
oldRead := readFileFunc oldRead := readFileFunc
readFileFunc = func(string) ([]byte, error) { return nil, fmt.Errorf("read") } readFileFunc = func(string) ([]byte, error) { return nil, fmt.Errorf("read") }
var out bytes.Buffer 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()) t.Fatalf("expected unreadable with rel fallback, got=%d out=%q", got, out.String())
} }
readFileFunc = oldRead 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(`<?xpacket begin=""?><x:xmpmeta xmlns:x="adobe:ns:meta/"><rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"><rdf:Description xmlns:photoscli="https://gitea.k3s.k0.nu/tools/photocli/ns/1.0/" photoscli:assetID="x1" photoscli:xmpSchemaVersion="2" photoscli:exportedFilename="photo.jpg" /></rdf:RDF></x:xmpmeta>`), 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(`<?xpacket begin=""?><x:xmpmeta xmlns:x="adobe:ns:meta/"><rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"><rdf:Description xmlns:photoscli="https://gitea.k3s.k0.nu/tools/photocli/ns/1.0/" photoscli:assetID="x1" photoscli:xmpSchemaVersion="2" photoscli:sidecarGenerator="photoscli" photoscli:exportedFilename="other.jpg" /></rdf:RDF></x:xmpmeta>`), 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) { func TestSidecarInspect(t *testing.T) {
dir := t.TempDir() dir := t.TempDir()
path := filepath.Join(dir, "photo.xmp") path := filepath.Join(dir, "photo.xmp")