Added
- v0.20 migration guide and example config. Added
docs/v0.20-migration.mdandexample.config.tomlto show every supported TOML key, default, and allowed value. README, Docker Compose, and migration docs now describe the v0.20 source model: TOML for persistent settings, CLI flags for one-run actions, and env vars for secrets or runtime glue. (#411) - Keyring-first setup wizard.
kei config setupnow stores accepted passwords through the same credential path askei password set, keeps.envas an explicit fallback, and shows which credential backend was used. Generated TOML stays password-free. (#410) - Strict import adoption.
import-existing --strictand[import].strict = trueverify a small iCloud prefix before adopting same-name, same-size local files. Strict refusals are counted in the import summary. (#408) - Upgrade hints for v0.20 config failures. Removed sync flags now print a direct v0.20 migration note, unknown TOML fields point at the migration guide, and stale removed env vars are named when
[download].directoryis missing. The Docker entrypoint also fails early when a sync-like command still relies on removed durable env config and/config/config.tomlis absent. (#496)
Changed
- TOML is now the durable config surface. Persistent sync, path, photo, media, retry, metadata, UI, and import settings resolve from
config.toml. CLI flags are for one-run behavior, and env vars remain for secrets and runtime/container glue. Docker now startskei service run --config /config/config.toml, keeps state under/config, and takes the download location from[download].directory. (#403, #405, #406, #408, #409, #411) - Config names were tightened for v0.20. Durable media filtering moved to
[filters].media; selector literals can escape sentinels with=; photo resolution settings split intoresolution,live_resolution,edited,alternative,raw_policy, andforce_resolution; retry settings moved to[download.retry].per_transferand[download.retry].per_asset; progress moved to[ui].progress_bar; metadata settings moved to[metadata]. (#405, #406, #409) - JPEG/TIFF EXIF writes no longer require the
xmpfeature. Native EXIF updates now work in default builds. XMP sidecars and HEIC metadata writes still require thexmpfeature. (#409) - Watch and full-sync runs make fewer Apple and SQLite calls. Idle watch cycles can skip album refresh when
/changes/databasereports no selected-library changes. Multi-pass full syncs preload download state once, batch same-library album counts, bound known-count streams, and run independent album streams concurrently. (#416, #464, #465, #466, #467, #468) - Download scheduling now keeps iCloud URLs fresher. Full download enumeration stays close to the worker pool so signed URLs spend less time waiting before transfer. Cleanup retries target the exact failed asset/version/path entries instead of retrying the whole library. (#473)
- Explicit album and smart-folder selection is collection-scoped. When users ask for albums or smart folders by config, sync and
import-existingresolve those passes across every visible library. Unfiled photos still follow[filters].libraries, so a primary-only unfiled pass does not unexpectedly widen to shared libraries. Named sensitive smart folders such asHiddenandRecently Deletedfollow the same collection scope. (#492, #493) - Setup and listing guide multi-library users more clearly.
kei config setupstill defaults to all visible libraries, but now writes library-scoped unfiled templates by default when that choice would otherwise mix primary and shared files.kei list librariesnow labels primary/shared zones and prints the selector values users can paste into[filters].libraries.--recenthelp now says the cap is per selected library, album, or smart-folder pass. (#495) - Service behavior is safer to preview and easier to operate.
kei install --dry-runon Linux and macOS prints the service artifact without writing unit/plist files or log directories. Linux system install previews work without root.kei statusgives clearer background-sync guidance, Docker and generated Linux systemd units setMALLOC_ARENA_MAX=2, and Linux uninstall restores the pre-install linger state when kei recorded it. (#413, #417, #451, #471, #472) - Sync internals have narrower ownership boundaries. Selection config, post-cycle reporting, cycle execution, download planning/finalization, and state access were split into smaller owner modules and role traits. These changes keep the v0.20 behavior easier to test without changing the user command shape. (#418, #449, #450, #452, #453, #455, #456)
- Release validation now carries more of the release-candidate checks. The repo-owned full-test path covers release archive smoke tests, Docker image smoke tests, live import rehearsal, service lifecycle checks, and stricter sync safety assertions. (#457, #459, #460, #471, #488, #489, #490)
Removed
- Old v0.13-v0.19 compatibility names were removed. v0.20 rejects the deprecated durable flags, env mirrors, TOML keys, and hidden command aliases from the warning window. Removed names include
--directory/KEI_DIRECTORY,--threads-num,--cookie-directory,[auth].cookie_directory,[download].threads_num,[metrics].port,--exclude-album/KEI_EXCLUDE_ALBUM,{album}in the base folder template, implicit--album allfrom that token,[filters].album,[filters].exclude_albums,[filters].library, and the legacykei setup/kei retry-failed/kei reset-statealiases. Use the v0.20 TOML keys and current subcommands instead. (#402, #403, #405, #406, #409, #485) - Import-only durable flags and env mirrors were removed.
import-existingnow reads durable matching settings from TOML, like sync.--library,--recent,--dry-run,--force-empty,--no-progress-bar, and--strictremain one-off import controls. (#408) - Stale local docs and old logo experiments were removed. The deleted v0.13 migration note and synctoken diagnostic scratch guide had drifted behind the v0.20 config model. Current migration guidance now lives in
docs/v0.20-migration.mdand the CloudKit synctoken reference remains indocs/synctoken-reference.md. (#494)
Fixed
- Existing media files are no longer replaced by duplicate
.partpromotion. If another writer already created the final path, kei removes only its redundant.partfile and leaves the existing file untouched. (#407) - Interrupted downloads remain resumable. Shutdown during a response body now leaves the
.partfile in place, skips downloaded/failed state updates for interrupted in-flight assets, and resumes with byte-range validation on the next run. (#421) - Expired iCloud CDN URLs no longer poison sync tokens. HTTP 410 is treated as an expired signed URL. The current URL batch aborts with an interrupted partial result so tokens don't advance past work that must be re-enumerated. (#473)
- Cross-zone album members are downloaded from their source library. Album relations that point outside the selected owner zone now fetch the referenced member from its source CloudKit zone. Stale or orphaned relation records warn instead of failing the album. (#474)
- Sync-token and state-write failures are safer. Config-hash purge failures force a full enumeration without persisting the unsafe hash;
complete_sync_runfailures count as state-write failures even on zero-download runs; full-enumeration fetchers and pass streams must agree on the token before it advances; repeated deferred state-write failures stop more downloads from streaming. Incomplete CloudKit pagination, per-record errors, non-finished indexing, failed zone discovery, and malformed advertised resources now block token advancement. (#444, #445, #446, #447, #454, #480) - Recent-limited syncs no longer look like incomplete enumeration. Sparse
--recentwindows don't trip the pagination-undercount failure path, recent-limited full syncs don't advance zone sync tokens, and smaller signed-URL download pages no longer narrow the requested recent window. (#483, #484) - Media validation rejects known extension/content mismatches. Downloads now check common magic-byte mismatches before promotion so HTML or otherwise wrong content can't land as a selected media file. (#480)
- Suspicious successful syncs are louder. Deferred state-write retries log at info level with retry metadata, and a completed full sync that enumerates zero assets warns without changing the exit code.
sync --retry-failedno-op runs stay quiet. (#419) - Terminal Apple auth failures get their own exit path. Apple
/signin/completeresponses withserviceErrors[].code = "-20209"or"-20101"now surface as terminal auth errors with account recovery or stored-password update steps, and kei exits 4 instead of the generic auth exit. (#420) - Credential deletion reports partial failures. Clearing saved credentials now reports when one backend fails instead of presenting the cleanup as wholly successful. (#480)
- Session files survive permission-denied reads. kei no longer deletes session files when reading them fails with
PermissionDenied, and the log includes aPUID/PGIDhint for Docker users. (#400) import-existinghandles Ctrl+C cleanly. SIGINT now cancels the import scan through kei's shutdown path so the next run resumes from the last committed database state. (#487)- Unicode-stripped filenames no longer collapse to extension-only paths. When stripping Unicode leaves an empty stem, sync and import fall back to fingerprint filenames for primary files, extras, size variants, and live-photo MOV paths. (#462)
- Malformed HEIF
ilocdata no longer panics in the pinned parser.mp4-atomis pinned to the crates.io0.11.0release with the HEIF fixes kei depends on. (#412, #461, #470) - Narrow CloudKit and Windows service panic paths now return errors. CloudKit enumeration/cache failures and Windows SCM mutex failures now surface as errors instead of panics. (#448)
Security
- GitHub Actions workflows were hardened. Workflow
uses:references are pinned to full commit SHAs, CI rejects mutable action refs and risky Docker publishing settings, Docker publishing is split from arbitrary-ref manual builds, coverage-comment artifacts are size-limited and sanitized before privileged posting, and release workflows default to read-only permissions. (#399)
Full changelog: CHANGELOG.md