feat: PhotoPrism M0 bring-up — compose stack, web client, sidecar, migrate
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>
This commit is contained in:
97
migrate/README.md
Normal file
97
migrate/README.md
Normal file
@@ -0,0 +1,97 @@
|
||||
# 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.
|
||||
|
||||
```text
|
||||
┌────────────────────┐ 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 `UID` via
|
||||
`q=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_URL` env), emits `snapshot.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 at `PHOTOPRISM_BASE_URL`. Hashes files on disk under
|
||||
`ORIGINALS_ROOT` to 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:
|
||||
|
||||
```sh
|
||||
PHOTOPRISM_BASE_URL=http://localhost:2342 \
|
||||
PHOTOPRISM_USER=admin \
|
||||
PHOTOPRISM_PASSWORD=... \
|
||||
ORIGINALS_ROOT=$(pwd)/photos-sample \
|
||||
node migrate/run.mjs --snapshot migrate/example-snapshot.json
|
||||
```
|
||||
|
||||
Add `--phase=sidecars` or `--phase=heaps` to run just one phase, or
|
||||
`--dry-run` to log without mutating.
|
||||
|
||||
## Snapshot schema
|
||||
|
||||
```json
|
||||
{
|
||||
"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"]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
- `filepath` is relative to `ORIGINALS_ROOT`.
|
||||
- `is_hidden` is **dropped** by the migrator per the merge plan.
|
||||
- `tags` lands in `Details.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 `Rating` field is server-managed in
|
||||
this build; we map mule-image's `is_picked` boolean → `Favorite`).
|
||||
Reference in New Issue
Block a user