v0.8.4: add strict sidecar verification
This commit is contained in:
@@ -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,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
|
||||||
|
|||||||
@@ -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
@@ -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.
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
Reference in New Issue
Block a user