package main import ( "bytes" "crypto/sha256" "encoding/hex" "encoding/json" "encoding/xml" "fmt" "io" "io/fs" "os" "path/filepath" "sort" "strconv" "strings" "sync" "time" "gitea.k3s.k0.nu/tools/photocli/internal/manifest" "gitea.k3s.k0.nu/tools/photocli/internal/photos" ) var ( progressPollInterval = 100 * time.Millisecond exportTimeout = 2 * time.Second configValues map[string]string configLoaded bool mkdirTempFunc = os.MkdirTemp createTempFunc = os.CreateTemp writeFileFunc = os.WriteFile readFileFunc = os.ReadFile statFunc = os.Stat renameFunc = os.Rename openFileFunc = os.OpenFile removeFunc = os.Remove ) type exportOptions struct { dryRun bool retry int onlyFavorites bool media string jsonOut bool verify bool format string sidecar string checksum string xmpPrivacy string xmpKeywords string xmpRating string metadataOnly bool reverseGeocode bool minSize int64 maxSize int64 dateTemplate string } type commandSummary struct { Exported int `json:"exported"` Failed int `json:"failed"` Total int `json:"total"` } const ( exitOK = 0 exitErr = 1 exitPartial = 2 exitAuth = 3 ) func run(args []string, stdout, stderr io.Writer, bridge photos.Bridge) int { if len(args) < 1 { usage(stderr) return exitErr } cmd := args[0] switch cmd { case "albums": return cmdAlbums(stdout, stderr, bridge) case "photos": return cmdPhotos(args[1:], stdout, stderr, bridge) case "tree": return cmdTree(stdout, stderr, bridge) case "backup-all": return cmdBackupAll(args[1:], stdout, stderr, bridge) case "export": return cmdExport(args[1:], stdout, stderr, bridge) case "report": return cmdReport(args[1:], stdout, stderr) case "diff": return cmdDiff(args[1:], stdout, stderr, bridge) case "verify": return cmdVerify(args[1:], stdout, stderr) case "manifest": return cmdManifest(args[1:], stdout, stderr) case "cleanup": return cmdCleanup(args[1:], stdout, stderr) case "retry-failed": return cmdRetryFailed(args[1:], stdout, stderr, bridge) case "failures": 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 case "help", "--help", "-h": usage(stderr) return exitOK default: fmt.Fprintf(stderr, "unknown command: %s\n", cmd) usage(stderr) return exitErr } } func usage(w io.Writer) { fmt.Fprintln(w, `photoscli - export and verify Apple Photos backups DESCRIPTION photoscli is a macOS-only Apple Photos exporter. It uses PhotoKit through a small Objective-C bridge to list albums, inspect assets, export previews or originals, keep resumable manifests, log structured export events, and verify backup integrity. Prebuilt releases target Apple Silicon Macs only (darwin/arm64: M1/M2/M3/M4 or newer). Intel Macs are not currently a supported release target. The tool is intended for repeatable backups. By default it records exported asset IDs in a manifest so later runs can skip work already completed. USAGE photoscli albums photoscli photos --album-id photoscli tree photoscli export --album-id --out [flags] photoscli backup-all --out [flags] photoscli report --out [--manifest jsonl|sqlite] photoscli diff --album-id --out [--manifest jsonl|sqlite] photoscli verify --out [--manifest jsonl|sqlite] photoscli manifest repair --out [--manifest jsonl|sqlite] [--checksum sha256] [--dry-run] photoscli cleanup --out [--manifest jsonl|sqlite] [--dry-run] photoscli retry-failed --out photoscli retry-failed --out --clear-on-success photoscli failures list --out photoscli failures clear --out photoscli sidecar inspect [--json] photoscli status --out [--json] photoscli version photoscli help COMMANDS albums Request Photos access and list user-created albums as: photos --album-id List assets in one album. The album can be a PhotoKit local identifier or an exact album title. Output includes asset ID, filename, cloud state, media type, dimensions, optional creation date, optional duration, and a trailing * for favorites. tree Print the Photos folder/album hierarchy as an indented tree. export --album-id --out [flags] Export assets from one album. Preview JPEGs are exported by default. Use --originals to export original files instead. backup-all --out [flags] Walk the Photos tree and export every album into a matching directory structure. Duplicate sibling album names are disambiguated with album IDs. report --out [--manifest jsonl|sqlite] Print manifest entry count and failure count. diff --album-id --out [--manifest jsonl|sqlite] Compare album assets against the manifest. Missing assets are printed as . Exits 2 when differences are found. 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 --strict with --sidecar to require photoscli schema/generator and exported filename metadata. Add --deep to verify manifest SHA-256 checksums when present. manifest repair --out [--manifest jsonl|sqlite] [--checksum sha256] [--dry-run] Fill missing manifest size/checksum metadata from files that exist on disk. cleanup --out [--manifest jsonl|sqlite] [--dry-run] Remove files not referenced by the manifest. Use --dry-run first. retry-failed --out Retry assets previously written to failures.jsonl. failures list|clear --out List or clear deduplicated failure records. sidecar inspect [--json] Read a generated XMP sidecar and print key photoscli metadata. status --out [--manifest jsonl|sqlite] [--json] Show manifest type, entry count, and failure count for a backup. COMMON EXPORT FLAGS --out Destination directory. Required for export, backup-all, report, diff, verify, and retry-failed. --album-id Album PhotoKit local identifier or exact album title. Required by export, photos, and diff. --size Target longest-side preview size in pixels. Default: 1024. Ignored when --originals is used. --quality <1-100> JPEG preview compression quality. Default: 85. Ignored when --originals is used. --originals Export original files instead of preview JPEGs. --concurrency Number of parallel export workers. Default: 3. Values above the bridge's progress slot count are capped automatically. --retry Retry failed exports N times with a small backoff. Default: 0. --dry-run Print assets that would be exported without writing files, manifests, or logs. Useful before large backup-all runs. --json Print a machine-readable summary to stdout: {"exported":N,"failed":N,"total":N} --verify Run manifest/file verification after export or backup-all. --checksum none|sha256 Store optional file checksum metadata in the manifest. Default: none. --sidecar none|xmp|json|xmp,json Write opt-in metadata sidecars next to each exported file. Default: none. If sidecar writing fails, the asset is counted as failed. --xmp-privacy keep|strip-location|strip-address Control location/address metadata in generated XMP sidecars. Default: keep. --xmp-keywords album-path|album|none Control dc:subject keywords in generated XMP sidecars. Default: album-path. --xmp-rating favorite|none Control favorite-to-rating mapping in generated XMP sidecars. Default: favorite. --metadata-only With --sidecar xmp, write or refresh XMP sidecars for files already in the manifest without exporting media files. Requires a manifest. --reverse-geocode With --sidecar xmp, use Apple MapKit on macOS 26+ to add address metadata for assets with GPS coordinates. Results are cached under .photoscli. FILTERING AND SELECTION --since Include only assets on or after a date. Accepts YYYY-MM-DD or RFC3339. --sort oldest|newest Sort assets by creation date. Default: oldest. --only-favorites Export only favorite assets. --media photos|videos|all Select media type. Default: photos. --include-videos Compatibility shortcut that includes videos/audio. --exclude-album backup-all only. Repeatable. Excludes album names by exact match or glob pattern, for example --exclude-album "Temp*". --min-size Include only assets with estimated pixel count >= n. --max-size Include only assets with estimated pixel count <= n. OUTPUT LAYOUT AND FORMAT --date-template