Files
mule-image/sidecar
dtoro e124809ad5 fix(move): move every originals file of a photo, not just the primary
Videos, Live Photos, and RAW+JPG pairs keep several files under Root "/". The
old movePhotoFiles moved only the primary (often the poster JPG), orphaning
the .mov: PhotoPrism then saw the photo as moved (dropped from the grid) while
the video stayed behind and broke. Move the whole originals group under one
shared stem (new uniqueStem helper) so siblings re-stack after reindex; fail
the photo and report it if any sibling can't move.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 00:06:18 +02:00
..

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.

Run

The sidecar is a sidecar service in the PhotoPrism compose stack. Bringing the whole stack up brings it up too:

podman-compose --env-file .env \
  -f docker-compose.yml \
  -f docker-compose.podman.yml \
  up -d

This builds Dockerfile (multi-stage golang:1.25-alpinegcr.io/distroless/static, ~12 MB final image), starts the container, and binds 127.0.0.1:8000 to the service. The SvelteKit dev server proxies /api/sidecar/* to that port transparently.

Dev-iteration loop (host build)

For tight iteration without rebuilding the image on every change you can run it as a host process — Go is already on the dev machine:

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 host build connects to mariadb via the loopback port the compose file publishes; stop pp-sidecar first so they don't fight for 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/
├── Dockerfile            multi-stage golang:1.25 → distroless/static
├── 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