-
-
Notifications
You must be signed in to change notification settings - Fork 5
Folder Structure
kei organizes downloaded photos into per-pass directory templates. v0.13 splits the single --folder-structure flag into three: one per pass (album / smart-folder / unfiled).
| Pass | Flag | Default | Token |
|---|---|---|---|
| Album | --folder-structure-albums |
{album} |
{album} |
| Smart folder | --folder-structure-smart-folders |
{smart-folder} |
{smart-folder} |
| Unfiled | --folder-structure |
%Y/%m/%d |
(none) |
All three templates also accept {library} as the first segment (see Multi-library). Standard strftime specifiers (%Y, %m, %d, %B, etc.) are supported in any template, plus the Python {:%Y/%m/%d} shape for backward compatibility.
With no folder-structure flags set, a kei sync run produces:
| Asset type | Path |
|---|---|
| Photo in user album "Vacation 2024" | /photos/Vacation 2024/IMG_1234.HEIC |
Photo in smart folder Favorites (only when --smart-folder Favorites was passed) |
/photos/Favorites/IMG_1234.HEIC |
| Photo in no user album (unfiled) | /photos/2024/03/15/IMG_1234.HEIC |
Smart-folder passes are off by default (--smart-folder defaults to none), so unless you opt in, you'll only see album folders and the date-hierarchy unfiled tree.
Which passes run depends on your selectors:
| Selector | Pass |
|---|---|
--album (default all, or any non-none value) |
Album pass per selected album |
--smart-folder (default none; non-none opts in) |
Smart-folder pass per selected smart folder |
--unfiled (default true) |
One library-wide unfiled pass |
Each pass uses its own template. A photo in user album "Vacation" is routed by the album pass and lands at the path produced by --folder-structure-albums. A photo in no album goes through the unfiled pass and uses --folder-structure.
A photo in N albums writes N on-disk copies (one per album folder) when --folder-structure-albums produces distinct paths per {album}. The unfiled pass carries an exclude_ids set built from every user album's membership, so albumed assets never also appear in the unfiled tree.
# default: per-album folders, unfiled at %Y/%m/%d, no smart-folder passes
kei sync -u me@example.com -d /photos
# add a date hierarchy under each album
kei sync --folder-structure-albums '{album}/%Y/%m/%d'
# disable unfiled, sync only specific albums
kei sync --album Vacation --album Family --unfiled false
# every Apple smart folder (except Hidden / Recently Deleted), date hierarchy
kei sync --smart-folder all --folder-structure-smart-folders '{smart-folder}/%Y/%m/%d'
# completely flat: all photos in the download dir, no subfolders
kei sync --album all --unfiled false --folder-structure-albums '{album}'When more than one library is in scope (see --library), prefix templates with {library} to keep paths separate:
kei sync --library all \
--folder-structure '{library}/%Y/%m/%d' \
--folder-structure-albums '{library}/{album}'The {library} token renders as PrimarySync for the personal library or a truncated raw zone name (SharedSync-A1B2C3D4) for shared libraries. The truncated form is what --library accepts back, so a path segment can be copied directly into the flag.
When no active template carries {library} and more than one library is in scope, kei warns at startup that paths share a namespace and the state DB will dedup cross-library matches by ID. The check is per-pass-active: only templates whose pass actually runs are required to carry the token.
Each token has placement constraints to keep paths stable when album / smart-folder / library names change:
-
{library}(when present) must be the first path segment, and may appear at most once per template. -
{album}(in--folder-structure-albums) must come before any strftime specifier and after{library}if present. At most once. -
{smart-folder}(in--folder-structure-smart-folders) follows the same rule.
Examples:
| Template | Valid? |
|---|---|
{album} |
yes |
{album}/%Y/%m/%d |
yes |
{library}/{album}/%Y |
yes |
Photos/{album}/%Y |
no (token not first segment) |
%Y/{album}/%m |
no (token after specifier) |
{album}/%Y/{album} |
no (duplicate token) |
Within each folder, filenames are preserved from iCloud. Characters invalid on the local filesystem (/\:*?"<>|) are stripped.
Names used as directory components are sanitized to prevent path traversal and invalid directories:
- Directory traversal sequences (
..) are replaced with_ - Leading/trailing dots and spaces are stripped
- Windows reserved names (
CON,NUL,PRN,COM1–COM9,LPT1–LPT9) are prefixed with_ - Invalid filesystem characters are removed
v0.12 had a single --folder-structure flag for everything; {album} in that template promoted the run to -a all and added an unfiled pass with the {album} token collapsed to empty.
v0.13:
- Splits the template into three (album / smart-folder / unfiled).
- Defaults
--albumtoall(explicit), and adds--unfiled trueas a separate toggle. - Auto-migrates
{album}in--folder-structureto--folder-structure-albumsat startup with a one-line deprecation warning. Removed in v0.20.
If your v0.12 template was %Y/%m/%d (no {album}), no migration is needed — that's now the unfiled-pass default. If your template was {album}/%Y/%m/%d, kei auto-promotes; to silence the warning, move the value to --folder-structure-albums.
Full migration guide: docs/v0.13-migration.md.