# 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/](../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//`. | 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: ```sh podman-compose --env-file .env.photoprism \ -f docker-compose.photoprism.yml \ -f docker-compose.photoprism.podman.yml \ up -d ``` This builds [Dockerfile](Dockerfile) (multi-stage `golang:1.25-alpine` → `gcr.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: ```sh 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`](../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: ```sql 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 ```text 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 ```