Generalize the photos-originals rclone-mount pattern to a video-projects prefix so the video studio (and imajin ETL, per storage-portability-plan §2.3) can read/write multi-GB project sources/renders as local files while only hot data stays resident on plum (bounded VFS LRU cache). Lets a small-disk laptop work with large footage without filling APFS. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
4.9 KiB
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.
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 viaPHAsset/PHAssetCollection.APIClient.swift— metadata sync, binary upload, stats (@packages/iphoto/Sources/IPhotoSync/APIClient.swift:129-240):syncPhotos,syncAlbumsgetPendingUploads->uploadPhoto(data-in-memory) oruploadPhotoFromURL(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— theObjectStoreinterface (put/get/head/presignGet), the config factorycreateObjectStore(), an explicit-credscreateS3Store()(used by the imajin ETL source), and a memoizeddefaultObjectStore().s3.ts—S3AdapteroverBun.S3Client, always SigV4-signed; works against MinIO, DO Spaces, AWS, R2. Selected bySTORAGE_BACKEND=s3.local.ts—LocalAdapter(dev), blobs on disk underSTORAGE_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 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/<hex>/<UUID>.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.shhave 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.
uploadRate/bytesUploadedstatistics reset on every cold start; per known-limitations.- Server-side enrichment (face extraction via
face-worker, classification viaclassify-worker, thumbnails viathumbnail-worker) runs off the uploaded blob; the Mac client itself only reads metadata + uploads binaries.
Tests (@packages/iphoto/Tests/IPhotoSyncTests/)
ReaderTests.swift— exercisesPhotosLibraryReaderhelpers that don't need a populated library.SyncStatsTests.swift—uploadRate/etaformulas (@packages/iphoto/Sources/IPhotoSync/SyncManager.swift:22-46).
Not covered: actual PhotoKit enumeration, binary upload (both need the OS).