# v0.20 migration guide v0.20 is kei's breaking config cleanup release. Use the tagged Docker `:latest` image or pin `:0.20.0`. The rule is simple: - TOML is for settings you want to keep. - CLI flags are for one run or one action. - Env vars are for secrets, config path/bootstrap, process-manager glue, and tests. Use [`example.config.toml`](../example.config.toml) as the full config dictionary. Each option block lists the option name, default value, valid values, and description. The wiki [Configuration](https://github.com/rhoopr/kei/wiki/Configuration) page has the same searchable option index. ## Quick migration 1. Run `kei config show` on your current setup, if it still starts. 2. Copy the durable settings into `~/.config/kei/config.toml`. 3. Replace old env-as-config values in Docker or systemd with mounted TOML. 4. Keep only secrets and runtime glue in env. Minimal config: ```toml [auth] username = "you@example.com" [download] directory = "/photos" ``` Then run: ```sh kei sync --config ~/.config/kei/config.toml ``` For a guided file, run: ```sh kei config setup ``` The setup wizard defaults to all visible iCloud Photos libraries. When that would mix primary and shared-library unfiled files, it writes a `{library}` path segment for the unfiled template. ### Manual copy from old `icloudpd-rs` paths Current kei releases don't auto-copy the old `icloudpd-rs` config, cookies, or state DB at startup. If you still have files in those paths, copy only the ones you need before the first run: ```sh mkdir -p ~/.config/kei/cookies cp ~/.config/icloudpd-rs/config.toml ~/.config/kei/config.toml cp ~/.icloudpd-rs/* ~/.config/kei/cookies/ ``` Run only the `cp` commands whose source paths exist. Leave the old files in place until you've confirmed kei starts and can read the copied state. ## Upgrade failure hints v0.20 removed durable sync flags and env mirrors instead of accepting them with warnings. If an old command fails with `unexpected argument '--download-dir'` or another removed sync flag, move that setting into `config.toml`. If sync or import reports a missing `[download].directory` while old `KEI_*` sync env vars are still set, the error lists the ignored env names. Those env vars no longer configure sync. Keep env vars for secrets, config discovery, and runtime glue. Unknown TOML keys now include a v0.20 migration hint. This is usually an old pre-v0.20 key such as `[filters].album`, `[filters].library`, `[download].threads_num`, or `[metrics].port`. ## What changed ### Account and storage | Old | New | |---|---| | `--username`, `ICLOUD_USERNAME` as durable config | `[auth].username`; keep `ICLOUD_USERNAME` for automation | | `--domain` | `[auth].domain` | | `--data-dir`, `KEI_DATA_DIR` as durable config | top-level `data_dir`; Docker still uses `KEI_DATA_DIR=/config` as runtime glue | | `--download-dir`, `KEI_DOWNLOAD_DIR` | `[download].directory` | | `--folder-structure` | `[download].folder_structure` | | `--folder-structure-albums` | `[download].folder_structure_albums` | | `--folder-structure-smart-folders` | `[download].folder_structure_smart_folders` | ### Selection and filters | Old | New | |---|---| | `--album` | `[filters].albums = ["Name"]` | | `--smart-folder` | `[filters].smart_folders = ["Favorites"]` | | sync `--library` | `[filters].libraries = ["primary"]` | | `--unfiled false` | `[filters].unfiled = false` | | `--filename-exclude "*.AAE"` | `[filters].filename_exclude = ["*.AAE"]` | | `--skip-videos`, `--skip-photos` | `[filters].media` | | `--exclude-album Name`, `KEI_EXCLUDE_ALBUM` | `[filters].albums = ["!Name"]` | | `[filters].album` | `[filters].albums` | | `[filters].exclude_albums` | `[filters].albums = ["!Name"]` | | `[filters].library` | `[filters].libraries` | These stay as one-run overrides: - `--recent` - `--recent-scope` - `--skip-created-before` - `--skip-created-after` They also remain valid TOML keys for users who intentionally run a permanently scoped mirror: ```toml [filters] recent = 100 recent_scope = "per-filter" skip_created_before = "2024-01-01" skip_created_after = "30d" ``` `recent_scope` only applies to count-form `recent` values. The default is `global`, which takes the library-wide recent set first and then applies album or smart-folder filters. Use `per-filter` when you want each selected album, smart folder, or unfiled pass to get its own recent window. Date-only bounds compare each asset's capture-local calendar date using Apple's per-asset timezone offset when it is usable. Assets without a usable offset retain host-local date rendering. Datetime values and relative intervals are instant cutoffs. Existing files are not moved when capture-local path rendering places an asset in a different date folder; a later full sync downloads it into the capture-local folder and leaves the earlier copy in place. Use `kei sync --refresh-metadata` to apply the resolved timestamp and offset to sidecars and to embedded files that do not already hold a timestamp. To replace an older embedded timestamp, enable `metadata.set_exif_datetime` and run `kei sync --refresh-metadata --repair-capture-timestamps`. This can overwrite camera-supplied metadata. Selector values can be escaped with `=` when a real album, smart folder, or library name looks like a sentinel: ```toml [filters] albums = ["=all", "=none", "=!Drafts", "!Archive"] ``` Explicit album and smart-folder selectors are collection-scoped. When `[filters].albums` or `[filters].smart_folders` is set, kei looks for those passes across every visible library. `[filters].libraries` still scopes unfiled photos and default library selection. `smart_folders = ["all"]` still excludes Apple's sensitive folders. Use `all-with-sensitive`, or name `Hidden` / `Recently Deleted` directly, to include them. ### Photos, Live Photos, RAW, and filenames | Old | New | |---|---| | `--size original` | `[photos].resolution = "original"` | | `--live-photo-size original` | `[photos].live_resolution = "original"` | | `--live-photo-mode skip` | `[photos].live_photo_mode = "skip"` | | `--align-raw original` | `[photos].raw_policy = "prefer-raw"` | | `--align-raw alternative` | `[photos].raw_policy = "prefer-jpeg"` | | `--force-size` | `[photos].force_resolution = true` | | `--keep-unicode-in-filenames` | `[photos].keep_unicode_in_filenames = true` | | `--live-photo-mov-filename-policy original` | `[photos].live_photo_mov_filename_policy = "original"` | | `--file-match-policy name-id7` | `[photos].file_match_policy = "name-id7"` | The old multi-size workflow: ```sh kei sync --size adjusted --size original --size alternative ``` maps to: ```toml [photos] resolution = "original" edited = true alternative = true raw_policy = "as-is" ``` `edited = true` adds adjusted/edited extras. `alternative = true` adds the alternative or RAW sibling when Apple provides one. Missing extras are skipped. ### Retry, watch, service, reports, and server | Old | New | |---|---| | `--threads`, `KEI_THREADS` | `[download].threads` | | `--bandwidth-limit`, `KEI_BANDWIDTH_LIMIT` | `[download].bandwidth_limit` | | `--temp-suffix`, `KEI_TEMP_SUFFIX` | `[download].temp_suffix` | | `[download.retry].max_retries` | `[download.retry].per_transfer` | | `[download.retry].max_download_attempts` | `[download.retry].per_asset` | | `--watch-with-interval`, `KEI_WATCH_WITH_INTERVAL` | `[watch].interval` | | `--notify-systemd` | `[watch].notify_systemd` | | `--pid-file` | `[watch].pid_file` | | `--reconcile-every-n-cycles` | `[watch].reconcile_every_n_cycles` | | `--notification-script` | `[notifications].script` | | `--report-json` | `[report].json` | | `--http-bind` | `[server].bind` | | `--http-port` | `[server].port` | | `[metrics].port`, `KEI_METRICS_PORT` | `[server].port` | `kei service run` supplies a 24-hour watch fallback when `[watch].interval` is unset. Plain `kei sync` stays one-shot unless you set a watch interval. ### Metadata and UI | Old | New | |---|---| | `--set-exif-datetime` | `[metadata].set_exif_datetime = true` | | `--set-exif-rating` | `[metadata].set_exif_rating = true` | | `--set-exif-gps` | `[metadata].set_exif_gps = true` | | `--set-exif-description` | `[metadata].set_exif_description = true` | | `--embed-xmp` | `[metadata].embed_xmp = true` | | `--xmp-sidecar` | `[metadata].xmp_sidecar = true` | | `[download].no_progress_bar = true` | `[ui].progress_bar = false` | `--no-progress-bar` is still a one-run override. ## Docker migration Use `ghcr.io/rhoopr/kei:latest` or pin `ghcr.io/rhoopr/kei:0.20.0`. The Docker image reads `/config/config.toml` by default: ```yaml services: kei: image: ghcr.io/rhoopr/kei:0.20.0 container_name: kei restart: unless-stopped environment: - TZ=${TZ:-UTC} # The image sets this by default. It keeps long-running watch containers # from reserving large glibc malloc arenas on multi-threaded hosts. # - MALLOC_ARENA_MAX=2 # ICLOUD_USERNAME is still fine for automation, but [auth].username is preferred. # - ICLOUD_USERNAME=${ICLOUD_USERNAME} volumes: - ./config:/config - /path/to/photos:/photos ``` Put durable settings in `./config/config.toml`. For every option and default, see [`example.config.toml`](../example.config.toml) or the wiki [Configuration](https://github.com/rhoopr/kei/wiki/Configuration) page: ```toml [auth] username = "you@example.com" [download] directory = "/photos" [watch] interval = 86400 ``` Keep secrets out of Compose when you can. Good options: ```sh docker exec -it kei kei password set ``` or a Docker secret: ```yaml services: kei: secrets: - icloud_password command: - service - run - --config - /config/config.toml - --password-file - /run/secrets/icloud_password secrets: icloud_password: file: ./icloud_password.txt ``` The image still sets `KEI_DATA_DIR=/config`. That is container runtime glue so sessions, the state DB, and credentials stay on the mounted config volume. It also sets `MALLOC_ARENA_MAX=2` for glibc-based images. This lowers virtual memory reservations during long watch runs. RSS stays roughly the same; the knob mostly stops glibc from reserving a large arena for each worker thread. Linux `kei install` writes the same `MALLOC_ARENA_MAX=2` setting into the systemd unit. If you manage your own unit file, add: ```ini Environment=MALLOC_ARENA_MAX=2 ``` If a Docker container exits with `/config/config.toml is required for v0.20 Docker sync settings`, it found removed sync env config such as `KEI_DOWNLOAD_DIR` but no mounted config file. Create `/config/config.toml`, move durable settings there, and leave only secrets or runtime glue in env. For one-shot commands, override the command: ```sh docker run --rm \ -v "$PWD/config:/config" \ -v "/path/to/photos:/photos" \ ghcr.io/rhoopr/kei:0.20.0 \ sync --config /config/config.toml --dry-run ``` ## Kept runtime flags These remain public because they are action or one-run controls: - `--config` - `--log-level`, `--verbose` - `--friendly`, `--no-friendly` - `--password`, `--password-file`, `--password-command`, `--save-password` - `--dry-run` - `--only-print-filenames` - `--retry-failed` - `--no-progress-bar` - `--recent` - `--recent-scope` - `--skip-created-before` - `--skip-created-after` - subcommand action flags such as `reset --yes`, `uninstall --purge`, `verify --checksums`, `import-existing --library`, `import-existing --strict`, and service install flags