Replace the Node prototype (server.mjs) with the stack the merge plan calls for: Go 1.25, Gin for routing, GORM + MariaDB for persistence. Same wire contract on /api/sidecar/* so the SvelteKit client doesn't change. - Marks move from a JSON file on disk to mule_sidecar.marks (auto- migrated by GORM on first boot). The Node prototype's marks.json was dev-only; not migrated. - Folder/rename/heap-convert/duplicates handlers reproduce the prototype's behaviour, including the path-traversal defence (resolveUnderRoot + EvalSymlinks), the size-bucket prefilter for the duplicate hasher, and the background reindex fire-and-forget pattern. - Auth model unchanged: requireSession middleware proxies the caller's X-Auth-Token to PhotoPrism's /api/v1/photos?count=1 before any destructive op. - Expose pp-mariadb on 127.0.0.1:3306 in docker-compose so the host Go process can reach mule_sidecar.* without joining the container network. - Archive the Node prototype under sidecar/legacy/server.mjs for one cycle as reference. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5.2 KiB
mule-sidecar
Go + Gin + GORM service for the endpoints PhotoPrism's REST API does not
expose. Same wire contract as the M3 Node prototype it replaces; the
SvelteKit client at web/ talks to it transparently through
Vite's /api/sidecar/* proxy.
What it owns
| Method | Path | Purpose |
|---|---|---|
| GET | /api/sidecar/healthz |
Unauthenticated liveness probe. |
| GET | /api/sidecar/photos/marks |
Every per-photo {rating, color} mark. |
| GET | /api/sidecar/photos/:uid/marks |
One photo's mark (or {} if none). |
| PUT | /api/sidecar/photos/:uid/marks |
Patch one photo's mark. |
| POST | /api/sidecar/photos/marks/bulk |
Stamp the same mark onto many photos. |
| POST | /api/sidecar/files/:uid/rename |
Rename the primary file on disk + reindex. |
| POST | /api/sidecar/folders |
Create a folder under ${ORIGINALS_ROOT}. |
| POST | /api/sidecar/folders/:rel/rename |
Rename a folder (rel path URL-encoded). |
| DELETE | /api/sidecar/folders/:rel |
Delete an empty folder. |
| POST | /api/sidecar/albums/:uid/convert |
Move/copy every photo in a heap into folder X. |
| GET | /api/sidecar/duplicates/scan |
Walk originals, return same-hash groups. |
| POST | /api/sidecar/duplicates/archive |
Move duplicate paths into .duplicates/<ts>/. |
Auth: every endpoint except healthz requires the caller's
X-Auth-Token header. The sidecar holds no service credentials — it
proxies the token straight back to PhotoPrism's /api/v1/photos?count=1
to confirm the session is live before doing anything destructive.
Marks persist to MariaDB (mule_sidecar.marks); everything else
operates on the filesystem under ${ORIGINALS_ROOT} and triggers a
PhotoPrism reindex of the affected parent in the background.
Build & run
cd sidecar
go build -o mule-sidecar .
ORIGINALS_ROOT=/path/to/photoprism/originals \
PHOTOPRISM_BASE_URL=http://localhost:2342 \
SIDECAR_PORT=8000 \
./mule-sidecar
The SvelteKit dev server proxies /api/sidecar/* to
http://localhost:8000.
Env
| Var | Default | Notes |
|---|---|---|
ORIGINALS_ROOT |
/photoprism/originals |
Absolute path; must match PhotoPrism's mount. |
PHOTOPRISM_BASE_URL |
http://localhost:2342 |
Where to reach PhotoPrism for session validation + reindex calls. |
SIDECAR_PORT |
8000 |
Loopback-only; reverse-proxy fronts it in production. |
SIDECAR_DSN |
(built from the vars below) | Set this to override the assembled MySQL DSN entirely. |
SIDECAR_DB_HOST |
127.0.0.1 |
Host of the MariaDB the compose stack publishes on 127.0.0.1:3306. |
SIDECAR_DB_PORT |
3306 |
|
SIDECAR_DB_USER |
sidecar |
Provisioned by mariadb/init/01-sidecar.sql on first boot. |
SIDECAR_DB_PASSWORD |
replace-at-m4-bringup |
Literal placeholder — rotate before any non-local deployment. |
SIDECAR_DB_NAME |
mule_sidecar |
Schema
GORM AutoMigrate creates the only table the service owns:
CREATE TABLE marks (
photo_uid VARCHAR(64) PRIMARY KEY,
rating BIGINT NULL,
color VARCHAR(16) NULL,
updated_at DATETIME(3)
);
The M3 Node prototype kept the same data in sidecar/data/marks.json.
There is no migration path — the prototype's marks file was dev-only
state. Heap-sharing tables (M4) will land in subsequent migrations.
Layout
sidecar/
├── main.go entrypoint, route wiring, graceful shutdown
├── config.go env-driven Config
├── db.go GORM open + Mark model + AutoMigrate
├── auth.go requireSession middleware + ctxToken
├── fs.go path safety, walk, sha1
├── pp.go PhotoPrism HTTP client (validateSession, reindex)
├── handlers_rename.go
├── handlers_folders.go
├── handlers_marks.go
├── handlers_heap.go
├── handlers_dups.go
└── legacy/server.mjs Node prototype, retained for one cycle as a reference.