v0.8.3: add sidecar inspection
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
@@ -91,6 +92,8 @@ func run(args []string, stdout, stderr io.Writer, bridge photos.Bridge) int {
|
||||
return cmdFailures(args[1:], stdout, stderr)
|
||||
case "status":
|
||||
return cmdStatus(args[1:], stdout, stderr)
|
||||
case "sidecar":
|
||||
return cmdSidecar(args[1:], stdout, stderr)
|
||||
case "version", "--version", "-v":
|
||||
fmt.Fprintln(stdout, version)
|
||||
return exitOK
|
||||
@@ -132,6 +135,7 @@ USAGE
|
||||
photoscli retry-failed --out <dir> --clear-on-success
|
||||
photoscli failures list --out <dir>
|
||||
photoscli failures clear --out <dir>
|
||||
photoscli sidecar inspect <file.xmp> [--json]
|
||||
photoscli status --out <dir> [--json]
|
||||
photoscli version
|
||||
photoscli help
|
||||
@@ -176,6 +180,9 @@ COMMANDS
|
||||
failures list|clear --out <dir>
|
||||
List or clear deduplicated failure records.
|
||||
|
||||
sidecar inspect <file.xmp> [--json]
|
||||
Read a generated XMP sidecar and print key photoscli metadata.
|
||||
|
||||
status --out <dir> [--manifest jsonl|sqlite] [--json]
|
||||
Show manifest type, entry count, and failure count for a backup.
|
||||
|
||||
@@ -2187,6 +2194,68 @@ func verifySidecar(stdout io.Writer, outDir, id, checkPath string) int {
|
||||
return 0
|
||||
}
|
||||
|
||||
func cmdSidecar(args []string, stdout, stderr io.Writer) int {
|
||||
if len(args) < 1 || args[0] != "inspect" {
|
||||
fmt.Fprintln(stderr, "error: expected sidecar inspect <file.xmp>")
|
||||
return exitErr
|
||||
}
|
||||
if len(args) < 2 {
|
||||
fmt.Fprintln(stderr, "error: sidecar inspect requires <file.xmp>")
|
||||
return exitErr
|
||||
}
|
||||
path := args[1]
|
||||
data, err := readFileFunc(path)
|
||||
if err != nil {
|
||||
fmt.Fprintf(stderr, "error: %v\n", err)
|
||||
return exitErr
|
||||
}
|
||||
meta := inspectXMP(data)
|
||||
if len(meta) == 0 {
|
||||
fmt.Fprintln(stderr, "error: no photoscli metadata found")
|
||||
return exitErr
|
||||
}
|
||||
if hasFlag(args[2:], "--json") {
|
||||
if err := json.NewEncoder(stdout).Encode(meta); err != nil {
|
||||
fmt.Fprintf(stderr, "error: %v\n", err)
|
||||
return exitErr
|
||||
}
|
||||
return exitOK
|
||||
}
|
||||
keys := make([]string, 0, len(meta))
|
||||
for k := range meta {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
for _, k := range keys {
|
||||
fmt.Fprintf(stdout, "%s\t%s\n", k, meta[k])
|
||||
}
|
||||
return exitOK
|
||||
}
|
||||
|
||||
func inspectXMP(data []byte) map[string]string {
|
||||
attrs := map[string]string{}
|
||||
dec := xml.NewDecoder(bytes.NewReader(data))
|
||||
for {
|
||||
tok, err := dec.Token()
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return attrs
|
||||
}
|
||||
start, ok := tok.(xml.StartElement)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
for _, a := range start.Attr {
|
||||
if a.Name.Space == "photoscli" || a.Name.Space == "https://gitea.k3s.k0.nu/tools/photocli/ns/1.0/" {
|
||||
attrs[a.Name.Local] = a.Value
|
||||
}
|
||||
}
|
||||
}
|
||||
return attrs
|
||||
}
|
||||
|
||||
func cmdRetryFailed(args []string, stdout, stderr io.Writer, bridge photos.Bridge) int {
|
||||
outDir := flagVal(args, "--out")
|
||||
clearOnSuccess := hasFlag(args, "--clear-on-success")
|
||||
|
||||
@@ -35,6 +35,10 @@ type mockBridge struct {
|
||||
cancelled atomic.Bool
|
||||
}
|
||||
|
||||
type errWriter struct{}
|
||||
|
||||
func (errWriter) Write([]byte) (int, error) { return 0, fmt.Errorf("write") }
|
||||
|
||||
type noEntryManifest struct{}
|
||||
|
||||
func (noEntryManifest) Has(string) bool { return false }
|
||||
@@ -4219,6 +4223,50 @@ func TestVerifySidecarBranches(t *testing.T) {
|
||||
readFileFunc = oldRead
|
||||
}
|
||||
|
||||
func TestSidecarInspect(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
path := filepath.Join(dir, "photo.xmp")
|
||||
if err := writeXMPSidecar(path, xmpSidecarData{AssetID: "x1", ExportedFilename: "photo.jpg", Album: "Trips"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
out, stderr, rc := runWith([]string{"sidecar", "inspect", path}, &mockBridge{})
|
||||
if rc != exitOK || stderr != "" || !strings.Contains(out, "assetID\tx1") || !strings.Contains(out, "album\tTrips") {
|
||||
t.Fatalf("inspect rc=%d out=%q stderr=%q", rc, out, stderr)
|
||||
}
|
||||
out, stderr, rc = runWith([]string{"sidecar", "inspect", path, "--json"}, &mockBridge{})
|
||||
if rc != exitOK || stderr != "" || !strings.Contains(out, `"assetID":"x1"`) || !strings.Contains(out, `"exportedFilename":"photo.jpg"`) {
|
||||
t.Fatalf("inspect json rc=%d out=%q stderr=%q", rc, out, stderr)
|
||||
}
|
||||
_, stderr, rc = runWith([]string{"sidecar"}, &mockBridge{})
|
||||
if rc != exitErr || !strings.Contains(stderr, "expected sidecar inspect") {
|
||||
t.Fatalf("inspect missing subcommand rc=%d stderr=%q", rc, stderr)
|
||||
}
|
||||
_, stderr, rc = runWith([]string{"sidecar", "inspect"}, &mockBridge{})
|
||||
if rc != exitErr || !strings.Contains(stderr, "requires <file.xmp>") {
|
||||
t.Fatalf("inspect missing path rc=%d stderr=%q", rc, stderr)
|
||||
}
|
||||
_, stderr, rc = runWith([]string{"sidecar", "inspect", filepath.Join(dir, "missing.xmp")}, &mockBridge{})
|
||||
if rc != exitErr || !strings.Contains(stderr, "error:") {
|
||||
t.Fatalf("inspect missing file rc=%d stderr=%q", rc, stderr)
|
||||
}
|
||||
plain := filepath.Join(dir, "plain.xmp")
|
||||
if err := os.WriteFile(plain, []byte(`<x:xmpmeta xmlns:x="adobe:ns:meta/"></x:xmpmeta>`), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, stderr, rc = runWith([]string{"sidecar", "inspect", plain}, &mockBridge{})
|
||||
if rc != exitErr || !strings.Contains(stderr, "no photoscli metadata") {
|
||||
t.Fatalf("inspect no metadata rc=%d stderr=%q", rc, stderr)
|
||||
}
|
||||
bad := inspectXMP([]byte(`<x:xmpmeta><rdf:RDF>`))
|
||||
if len(bad) != 0 {
|
||||
t.Fatalf("expected empty metadata on malformed XML, got %#v", bad)
|
||||
}
|
||||
stderrBuf := &bytes.Buffer{}
|
||||
if rc := cmdSidecar([]string{"inspect", path, "--json"}, errWriter{}, stderrBuf); rc != exitErr || !strings.Contains(stderrBuf.String(), "error:") {
|
||||
t.Fatalf("expected json encoder error rc=%d stderr=%q", rc, stderrBuf.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestXMPSidecarHelpers(t *testing.T) {
|
||||
if got := sidecarPath("/tmp/IMG_0001.HEIC"); got != "/tmp/IMG_0001.xmp" {
|
||||
t.Fatalf("sidecar path = %q", got)
|
||||
|
||||
Reference in New Issue
Block a user