Cloud Photo Downloader for macOS, Windows, and Linux.
Download your iCloud Photos to your own computer. PhotoHarbor copies photos and videos from iCloud Photos to a local folder, with support for albums, Apple smart folders, Live Photos, browsing downloaded files, and sync history. Based on the kei sync engine.
| Platform | Status |
|---|---|
| macOS 13+ | Primary target; native overlay title bar |
| Windows 10+ | Supported; native window decorations |
| Linux | Supported; native window decorations |
Tagged GitHub releases build a signed and notarized universal macOS app. The release workflow signs and notarizes the .app, creates a plain DMG with hdiutil, then signs, notarizes, and staples that DMG. The DMG intentionally does not use Tauri's fancy Finder layout script because that step is fragile in CI.
If you build locally without Apple signing credentials, Gatekeeper may still block the app on first launch. To open a local unsigned build anyway:
Option 1 — System Settings
- Try to open the app normally — it will be blocked
- Go to System Settings → Privacy & Security
- Scroll down and click Open Anyway next to the blocked app
Option 2 — Terminal (removes the quarantine flag permanently)
xattr -cr "/Applications/PhotoHarbor.app"- Rust + Cargo (via rustup)
- Node.js 18+
- On macOS: Xcode Command Line Tools (
xcode-select --install) - On Linux:
libwebkit2gtk,libgtk-3,libayatana-appindicator3(see Tauri Linux dependencies)
kei itself is downloaded automatically by npm run prepare-sidecar — no separate installation needed.
# Install JS dependencies
npm install
# Download the latest kei release from GitHub into src-tauri/binaries/
npm run prepare-sidecar
# Launch in development mode
npm run devThe first build takes a few minutes (Tauri compiles the WebView bindings). Subsequent runs are fast.
prepare-sidecar is a no-op if the binary is already present. The downloaded version is pinned in src-tauri/binaries/.kei-version so cross-platform builds always use the same release.
To update the bundled kei to the latest release:
node scripts/prepare-sidecar.js --force
git add src-tauri/binaries/.kei-version
git commit -m "update kei sidecar to vX.Y.Z"npm run prepare-sidecar # ensure sidecar is up to date
npm run buildOutput locations:
- macOS:
src-tauri/target/release/bundle/macos/PhotoHarbor.app - Windows:
src-tauri/target/release/bundle/msi/ornsis/ - Linux:
src-tauri/target/release/bundle/deb/orappimage/
.github/workflows/build.yml builds macOS, Windows, and Linux on v* tags and creates a draft GitHub Release.
macOS release signing expects these GitHub secrets:
| Secret | Meaning |
|---|---|
APPLE_CERTIFICATE |
Base64-encoded Developer ID Application .p12 certificate with private key |
APPLE_CERTIFICATE_PASSWORD |
Password for that .p12 file |
APPLE_ID |
Apple ID email used for notarization |
APPLE_PASSWORD |
Apple app-specific password, not the normal Apple ID password |
APPLE_TEAM_ID |
10-character Apple Developer Team ID |
If the .p12 was exported with legacy encryption and OpenSSL reports Algorithm (RC2-40-CBC) unsupported, convert it locally to a modern .p12 before updating APPLE_CERTIFICATE.
Tauri's Rust crate and npm packages must stay on the same major/minor version. For example, Rust tauri 2.11.x must be paired with @tauri-apps/api 2.11.x. The Tauri npm packages are pinned exactly in package.json to make Dependabot version drift obvious.
When release, signing, dependency, or workflow behavior changes, update both README.md and agents/AGENTS.md in the same change.
PhotoHarbor/
├── index.html # App shell — sidebar + 4 views + modals
├── scripts/
│ ├── prepare-sidecar.js # Downloads kei binary into src-tauri/binaries/
│ └── build-windows.sh # Cross-compiles Windows .exe on macOS (cargo-xwin)
├── src/
│ ├── app.js # Frontend: Tauri invoke/listen calls, view logic
│ ├── log-parsers.js # Extensible log parser registry
│ └── styles.css # Native-style theming with full dark-mode support
└── src-tauri/
├── binaries/
│ └── .kei-version # Pinned kei release tag (committed)
├── Cargo.toml
├── tauri.conf.json
├── capabilities/
│ └── default.json # Tauri v2 permission grants
├── icons/
│ └── icon.png # Replace with your own 512×512 RGBA PNG
└── src/
└── main.rs # All backend commands
| View | What it shows |
|---|---|
| Sync | Start/Stop button, kei friendly progress UI, expandable compact/full logs, recent thumbnail strip, and automatic password/2FA prompts |
| Browse | Folder browser rooted at the configured download directory, with cached thumbnails, video previews, Live Photo pairing, breadcrumbs, and Finder/File Explorer open actions |
| History | Table of the last 100 sync runs with duration and status, plus clear-history/statistics action |
| Settings | Form that reads and writes the kei config file, including selectable albums and Apple smart folders |
All backend logic lives in src-tauri/src/main.rs.
| Command | Description |
|---|---|
check_kei |
Returns the resolved kei binary path, or an error if not found |
get_config |
Reads kei's config file; returns defaults if absent |
save_config |
Serialises the config struct back to TOML |
get_app_settings |
Reads UI-only settings (folder structure, system kei toggle, etc.) |
save_app_settings |
Writes UI-only settings |
get_kei_versions |
Returns paths and version strings for bundled and system kei |
get_status |
Queries the kei SQLite DB for asset counts and last sync_run |
get_history |
Returns the last 100 rows from the sync_runs table |
start_sync |
Spawns kei sync, streams output as batched Tauri events |
stop_sync |
Sends SIGKILL to the running kei process |
submit_password |
Writes a password to kei's stdin |
request_2fa_code |
Runs kei login get-code to push a 2FA prompt to trusted devices |
submit_2fa |
Runs kei login submit-code <CODE> as a separate process |
clear_kei_session |
Deletes kei session/cookie files to force re-authentication |
list_kei_albums |
Runs kei list albums and returns the album names |
list_kei_smart_folders |
Runs kei and returns available Apple smart folders |
browse_photos |
Lists one folder level from the configured download root, returning child folders and direct media assets |
get_recent_downloads |
Returns up to 100 recently downloaded media assets with cached thumbnails and Live Photo pairing |
open_folder |
Opens a folder in the system file manager |
open_containing_folder |
Opens the containing folder for a media file |
| Platform | Config file | Database |
|---|---|---|
| macOS / Linux | ~/.config/kei/config.toml |
~/.config/kei/cookies/<user>.db |
| Windows | %USERPROFILE%\.config\kei\config.toml |
%USERPROFILE%\.config\kei\cookies\<user>.db |
[auth]
username = "you@icloud.com"
domain = "com" # or "cn" for China
[download]
directory = "~/Photos/iCloud"
threads_num = 10
folder_structure = "%Y/%m/%d" # unfiled photos
folder_structure_albums = "{album}/%Y/%m" # user albums
folder_structure_smart_folders = "{smart-folder}" # Apple smart folders
set_exif_datetime = false
[download.retry]
max_download_attempts = 10
[filters]
skip_videos = false
libraries = ["primary"]
albums = ["Vacation", "!Screenshots"]
smart_folders = ["Favorites"]
unfiled = true
recent = 0 # 0 = all
[watch]
interval = 3600 # seconds; omit to disable watch mode
log_level = "info"Passwords are never stored in the config file. kei stores credentials in the system keychain. Set up credentials once with kei config setup or kei password set.
# Generates all required sizes from a single source image
npx tauri icon path/to/your-icon.pngkei not found — Run npm run prepare-sidecar. If kei itself is missing, install it with cargo install kei first.
No data in Sync statistics — kei creates its SQLite database only after the first successful sync. Run kei sync once from the terminal to initialise it, or use the Sync view.
Advanced Data Protection (ADP) — kei cannot sync if ADP is enabled on your iCloud account. Disable it in System Settings → Apple ID → iCloud → Advanced Data Protection.
2FA loop — If the 2FA dialog keeps appearing, your kei session may have expired. Run kei verify in the terminal to re-authenticate.
-- Icon from IO Images: https://pixabay.com/de/vectors/wolke-cloud-herunterladen-speichern-2044822/