Skip to content

Folder Structure

rhoopr edited this page May 2, 2026 · 12 revisions

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).

Three templates, one per pass

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.

Defaults explained

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.

Pass behavior

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.

Examples

# 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}'

Multi-library

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.

Token placement rules

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)

Filename handling

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

Album / smart-folder name sanitization

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, COM1COM9, LPT1LPT9) are prefixed with _
  • Invalid filesystem characters are removed

Migrating from v0.12

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 --album to all (explicit), and adds --unfiled true as a separate toggle.
  • Auto-migrates {album} in --folder-structure to --folder-structure-albums at 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.

Related Flags

Clone this wiki locally