feat(sidecar): port Node prototype to Go + Gin + GORM + MariaDB
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>
This commit is contained in:
@@ -1,43 +1,96 @@
|
||||
# mule-sidecar
|
||||
|
||||
Auxiliary service that handles operations PhotoPrism's REST API does not expose.
|
||||
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.
|
||||
|
||||
## Why this exists
|
||||
## What it owns
|
||||
|
||||
Per the merge plan at `/home/dtoro/.claude/plans/i-want-you-to-twinkly-galaxy.md`,
|
||||
a Go + Gin + GORM service (matching PhotoPrism's stack) will eventually own:
|
||||
| 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>/`. |
|
||||
|
||||
- Per-user heap sharing with pending invitations
|
||||
- Folder mutations under `originals/` (create / rename / delete / move)
|
||||
- **File rename** on disk (PhotoPrism's `OriginalName` is a display-only rename)
|
||||
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.
|
||||
|
||||
The plan picks Go for stack consistency and the option to upstream features.
|
||||
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.
|
||||
|
||||
## What ships today
|
||||
## Build & run
|
||||
|
||||
A **Node.js prototype** (`server.mjs`) covering only the **file rename** endpoint.
|
||||
```sh
|
||||
cd sidecar
|
||||
go build -o mule-sidecar .
|
||||
|
||||
The decision to ship Node first is pragmatic — Go isn't installed on this dev
|
||||
box and `sudo dnf install golang` needs a password. Node is already on PATH for
|
||||
the SvelteKit dev server, so a single-file Node service unblocks the feature
|
||||
without changing the host setup.
|
||||
|
||||
The endpoint contract is stable: when M4 lands the proper Go service, the
|
||||
SvelteKit client keeps calling the same paths.
|
||||
|
||||
## Endpoints
|
||||
|
||||
- `POST /api/sidecar/files/:photoUid/rename` `{ "newName": "newfile.png" }`
|
||||
Renames the primary file of the photo on disk under `${ORIGINALS_ROOT}`,
|
||||
then triggers a PhotoPrism reindex of the parent path.
|
||||
|
||||
## Run
|
||||
|
||||
```
|
||||
ORIGINALS_ROOT=/home/dtoro/projects/mule-image/photos-sample \
|
||||
ORIGINALS_ROOT=/path/to/photoprism/originals \
|
||||
PHOTOPRISM_BASE_URL=http://localhost:2342 \
|
||||
SIDECAR_PORT=8000 \
|
||||
node server.mjs
|
||||
./mule-sidecar
|
||||
```
|
||||
|
||||
The SvelteKit dev server proxies `/api/sidecar/*` to `http://localhost:8000`.
|
||||
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`](../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/
|
||||
├── 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.
|
||||
```
|
||||
|
||||
Reference in New Issue
Block a user