# iPhoto (`IPhotoSync`) ## Purpose Sync the user's Photos library metadata to the server and upload binaries on demand. ## Direction Read-only Mac to server. There is no Sender; the web cannot push photos back. See the rationale in [known-limitations](../known-limitations.md#iphoto-has-no-sender). ## OS surface PhotoKit (`PhotosUI`/`Photos.framework`). Requires Photos library access prompted by `IPhotoSync.requestAuthorization()` (`@packages/iphoto/Sources/IPhotoSync/SyncManager.swift:118-120, 122-124`). ## Files - `Reader.swift` — `PhotosLibraryReader.shared`; enumerates assets and albums via `PHAsset` / `PHAssetCollection`. - `APIClient.swift` — metadata sync, binary upload, stats (`@packages/iphoto/Sources/IPhotoSync/APIClient.swift:129-240`): - `syncPhotos`, `syncAlbums` - `getPendingUploads` -> `uploadPhoto` (data-in-memory) or `uploadPhotoFromURL` (streamed) - `getStats` - `SyncManager.swift` — orchestrates metadata sync, then a bounded-concurrency upload pass: `metadataBatchSize = 100`, `maxConcurrentUploads = 4` (`@packages/iphoto/Sources/IPhotoSync/SyncManager.swift:90-92`). - No `Sender.swift`. ## Timing - Read interval: **300s** (`@packages/iphoto/Sources/IPhotoSync/SyncManager.swift:99`). - Metadata batch size: 100. - Max concurrent uploads: 4. ## Server surface - Entity tables: `icloud.albums`, `icloud.photos` (`src/server/src/app/server.ts:42-43`). - Client routes (`src/server/src/surfaces/client/iphoto.ts`): - `GET /client/iphoto/stats` (line 53) - `GET /client/iphoto/upload/pending` (line 58) - `POST /client/iphoto/sync` (line 63) - `POST /client/iphoto/albums` (line 69) - `POST /client/iphoto/upload/:localIdentifier` (line 75) - Web routes (`src/server/src/surfaces/my/photos.ts`): - `GET /my/photos/` (line 12) - `GET /my/photos/albums` (line 18) - `GET /my/photos/albums/:albumId` (line 22) - No admin send-queue surface. ## Server-side blob storage Photo originals + derivatives go through a provider-agnostic `ObjectStore` port (`src/server/src/features/iphoto/storage/`), never a vendor-specific path: - `index.ts` — the `ObjectStore` interface (`put`/`get`/`head`/`presignGet`), the config factory `createObjectStore()`, an explicit-creds `createS3Store()` (used by the imajin ETL source), and a memoized `defaultObjectStore()`. - `s3.ts` — `S3Adapter` over `Bun.S3Client`, **always SigV4-signed**; works against MinIO, DO Spaces, AWS, R2. Selected by `STORAGE_BACKEND=s3`. - `local.ts` — `LocalAdapter` (dev), blobs on disk under `STORAGE_LOCAL_PATH`. Consumers that fetch originals out-of-band (`face-worker`, the imajin ETL) receive **short-lived presigned GET URLs** (`presignGet`), never a bare object URL — matching a private, TLS-only bucket policy. Upload path: `service.ts` `storePhotoBlob` → `store.put`; thumbnail/classify workers read via `store.get`. See env in [dev-setup](../dev-setup.md#boot-the-server) and the design in `.project/storage-portability-plan.md`. ## Optional: originals offload mount (per-host, e.g. plum) An **opt-in** capability — not part of the default runtime, no code branches on it. When a host needs photo originals off local disk, the Photos.app `originals//.ext` subtree is served on demand by `rclone mount` over the object store (DO Spaces) with a bounded LRU cache; the SQLite core (`database/`) and derivatives (`resources/`) stay local on APFS. Both Photos.app and `IPhotoSync` read the same mounted files. - Mount runner: `deploy/photos-originals-mount.sh`. - Installer (phased, idempotent; gated symlink repoint): `deploy/install-photos-mount.sh`. - Seed (iCloud → Spaces, osxphotos + rclone, keyed by real bundle path): `deploy/seed-originals-to-spaces.sh`. Shared Spaces/rclone config: `deploy/lib/spaces-env.sh`. - The server's blob ingest and the default `deploy/install.sh` have **zero** dependency on this. Full rationale + gates (macFUSE approval, iCloud re-seed) in `.project/storage-portability-plan.md`. ## Web surface - Tab: `/photos` (`web/src/App.tsx:58`). - API helpers: `web/src/api/photos.ts`. ## Known limitations - No outbound path; web cannot add to the library. See [known-limitations](../known-limitations.md#iphoto-has-no-sender). - `uploadRate` / `bytesUploaded` statistics reset on every cold start; per [known-limitations](../known-limitations.md#iphotostatsuploadrate-is-per-session-only). - Server-side enrichment (face extraction via `face-worker`, classification via `classify-worker`, thumbnails via `thumbnail-worker`) runs off the uploaded blob; the Mac client itself only reads metadata + uploads binaries. ## Tests (`@packages/iphoto/Tests/IPhotoSyncTests/`) - `ReaderTests.swift` — exercises `PhotosLibraryReader` helpers that don't need a populated library. - `SyncStatsTests.swift` — `uploadRate`/`eta` formulas (`@packages/iphoto/Sources/IPhotoSync/SyncManager.swift:22-46`). Not covered: actual PhotoKit enumeration, binary upload (both need the OS).