Skip to content

Folder Structure

rhoopr edited this page Apr 22, 2026 · 12 revisions

Folder Structure

kei organizes downloaded photos into date-based subdirectories within your download directory.

Default Layout

With the default --folder-structure "%Y/%m/%d", a photo taken on March 15, 2024 is saved to:

/photos/2024/03/15/IMG_1234.HEIC

Custom Templates

The folder structure is a strftime format string applied to each asset's creation date. All standard strftime specifiers are supported.

Template Example path
%Y/%m/%d 2024/03/15/IMG_1234.HEIC
%Y/%m 2024/03/IMG_1234.HEIC
%Y/%B 2024/March/IMG_1234.HEIC
%Y 2024/IMG_1234.HEIC
{album}/%Y/%m Favorites/2024/03/IMG_1234.HEIC
none IMG_1234.HEIC

The Python {:%Y/%m/%d} syntax is also accepted for compatibility with existing configurations.

Album Token

The {album} token expands to the album name for each asset. This is useful when syncing specific albums into separate directories:

kei sync --album "Favorites" --album "Travel" --folder-structure "{album}/%Y/%m"

Produces:

/photos/Favorites/2024/03/IMG_1234.HEIC
/photos/Travel/2024/01/IMG_5678.HEIC

Album names are sanitized for filesystem safety (see below).

Album and Library Pass Behavior

Which passes kei runs depends on the combination of -a and whether {album} is in the template. The table below uses %Y/%m/%d as a stand-in date format; any strftime template works the same way.

Command Passes run Where files land
kei sync --folder-structure "%Y/%m/%d" One library pass. 2025/06/15/<filename> for every asset.
kei sync --folder-structure "{album}/%Y/%m/%d" Auto-upgraded to -a all. Per-album passes plus a library-wide unfiled pass. Albumed assets at <AlbumName>/2025/06/15/<filename>. Unfiled assets at /2025/06/15/<filename> (the {album} segment collapses to empty).
kei sync -a all --folder-structure "%Y/%m/%d" Per-album passes merged into one stream. No unfiled pass. All albumed assets at 2025/06/15/<filename>. Unfiled assets are not synced. Assets in multiple albums dedup to one on-disk copy.
kei sync -a all --folder-structure "{album}/%Y/%m/%d" Per-album passes plus an unfiled pass. Albumed assets at <AlbumName>/2025/06/15/<filename>. Unfiled assets at /2025/06/15/<filename>. Assets in multiple albums get one copy per album folder.
kei sync -a Vacation --folder-structure "%Y/%m/%d" One pass for Vacation. 2025/06/15/<filename> for each Vacation asset. No unfiled pass.
kei sync -a Vacation --folder-structure "{album}/%Y/%m/%d" One pass for Vacation. Vacation/2025/06/15/<filename>. No unfiled pass.
kei sync -a Vacation -a Family --folder-structure "{album}/%Y/%m/%d" Two named passes. Vacation/... and Family/.... Shared assets duplicated per folder. No unfiled pass.

Key rules behind the table:

  • The unfiled pass runs only when {album} is in the template and the selection is -a all (explicit or auto-upgraded).
  • An "unfiled" asset is one not in any user-created album. Apple's smart folders (Favorites, Screenshots, etc.) don't count.
  • With {album} in the template, an asset in N albums writes N on-disk copies, one per album folder.
  • Without {album} in the template, an asset in N albums writes one copy. The state DB's (asset_id, version_size) key catches the duplicate before the second write.
  • The unfiled pass carries an exclude_ids set built from every user album's membership (including excluded albums), so albumed assets never also appear at the collapsed root path.

Filename Handling

Within each folder, filenames are preserved from iCloud. Characters that are invalid on the local filesystem (/\:*?"<>|) are stripped.

Album Name Sanitization

When downloading from specific albums, album names are used as directory components. These are sanitized to prevent path traversal attacks or invalid directories:

  • Directory traversal sequences (..) are replaced with _
  • Leading/trailing dots and spaces are stripped
  • Windows reserved names (CON, NUL, PRN, COM1COM9, LPT1LPT9) are prefixed with _
  • Invalid filesystem characters are removed

Compatibility with Python Version

The Rust version produces identical folder paths when using the same template string. If you're migrating from the Python version with the default folder structure, your existing directory layout will be preserved.

Related Flags

Clone this wiki locally