Replace the legacy mule-image backend with PhotoPrism plus a thin SvelteKit client and a Node sidecar for endpoints PhotoPrism doesn't expose (file rename), and add a two-phase migrator (metadata via PUT, heaps → albums) for the existing Postgres library. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
4.0 KiB
migrate — legacy mule-image → PhotoPrism
Two-phase migration that lifts user metadata + heap memberships out of the legacy mule-image Postgres into PhotoPrism without touching originals.
┌────────────────────┐ legacy_export.mjs ┌─────────────────────┐
│ mule-image Postgres├───────────────────────►│ snapshot.json │
└────────────────────┘ └──────────┬──────────┘
│ run.mjs
▼
┌─────────────────────────────┐
│ PUT /api/v1/photos/:uid │ (Phase A)
│ POST /api/v1/albums + adds │ (Phase B)
└─────────────────────────────┘
Why two phases
- Phase A — metadata via PUT. For each legacy photo, the migrator
hashes the file on disk, resolves the PhotoPrism
UIDviaq=hash:<sha1>, and PUTs the merged metadata onto/api/v1/photos/:uid. We tried writing YAML sidecars directly first but PhotoPrism's reindex rewrites them from its DB state — PUT is the only authoritative path. - Phase B — heap memberships. Once every legacy photo has a stable
UID, the heap → Album conversion creates manual Albums and bulk-adds
the resolved UIDs via
/albums/:uid/photos.
Both phases are idempotent. Phase A's PUT is deep-merged (top-level
spread plus Details shallow-merge), so re-running pulls in any new
fields from the snapshot without clobbering server-side state. Phase B
looks up heaps by title and only creates if absent.
Files
-
legacy_export.mjs— reads from legacy mule-image Postgres (DATABASE_URLenv), emitssnapshot.json. Runs on the homecloud server where the legacy backend lives. -
run.mjs— reads a snapshot file and walks both phases against the PhotoPrism API atPHOTOPRISM_BASE_URL. Hashes files on disk underORIGINALS_ROOTto resolve UIDs. Runs wherever the new PhotoPrism is reachable and originals are mounted (homecloud first, dev workstation for the smoke test). -
example-snapshot.json— synthetic input that exercises Phase A + B against the local sample library. Smoke test:PHOTOPRISM_BASE_URL=http://localhost:2342 \ PHOTOPRISM_USER=admin \ PHOTOPRISM_PASSWORD=... \ ORIGINALS_ROOT=$(pwd)/photos-sample \ node migrate/run.mjs --snapshot migrate/example-snapshot.jsonAdd
--phase=sidecarsor--phase=heapsto run just one phase, or--dry-runto log without mutating.
Snapshot schema
{
"photos": [
{
"filepath": "Screenshot_20260510_110411.png",
"user_title": null,
"user_notes": "Trip to Paris",
"rating": 4,
"is_picked": true,
"is_discarded": false,
"is_hidden": false,
"taken_at": "2024-06-15T14:30:00Z",
"tags": ["paris", "trip"]
}
],
"heaps": [
{
"name": "Summer 2024",
"photo_filepaths": ["Screenshot_20260510_110411.png"]
}
]
}
filepathis relative toORIGINALS_ROOT.is_hiddenis dropped by the migrator per the merge plan.tagslands inDetails.Keywords(comma-separated,KeywordsSrc=manual).- Heaps reference photos by
filepath; the migrator resolves these to PhotoPrism UIDs after Phase A's reindex finishes.
What's NOT migrated
- mule-image's
is_hidden(per the planning round). - Heap+folder sharing state (planned for the Go sidecar service).
- Undo history.
- Nextcloud per-user roots.
- The 5-star user rating (PhotoPrism's
Ratingfield is server-managed in this build; we map mule-image'sis_pickedboolean →Favorite).