Delete the old Python+React mule-image stack (backend/, frontend/,
docker-compose.yml, mulita.yml, .env*) plus the one-shot migration and
sample dirs (migrate/, photos-sample/, photovault-app-prompt.md). Only
the PhotoPrism + Go sidecar + SvelteKit web stack remains, so drop the
".photoprism." qualifier from the compose+env filenames.
Bind PhotoPrism's port to 127.0.0.1 so the user-facing surface is just
the SvelteKit web/ app; admin reaches PP's UI via SSH tunnel. Flatten
PHOTOPRISM_INDEX_WORKERS' nested default (podman-compose's interpolator
doesn't expand ${A:-${B:-…}}). Rewrite README for the current stack.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
117 lines
5.9 KiB
Markdown
117 lines
5.9 KiB
Markdown
# 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/<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:
|
|
|
|
```sh
|
|
podman-compose --env-file .env \
|
|
-f docker-compose.yml \
|
|
-f docker-compose.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
|
|
```
|