feat: manage uploaded image retention

This commit is contained in:
2026-08-07 07:14:14 +00:00
parent dc0e1a6da9
commit a935b23571
13 changed files with 966 additions and 23 deletions
+64 -9
View File
@@ -134,7 +134,7 @@ Never give either caller `image_admin_secret`, which also authorizes explicit
branch changes. This fallback handles webhook delivery outages; if the image
service itself is stopped, restore it and run the caller command again.
### Short-lived user-icon uploads
### User uploads
Core and Core2026 can store validated account icons and editor attachments
through this service with
@@ -158,10 +158,59 @@ the matching `image_upload_core_secret` or `image_upload_core2026_secret` on the
game server. The shared secrets stay server-side in Docker secrets; they are
not returned to browsers or forwarded to Cloudflare.
Run `deploy/scripts/init-secrets.sh` before the first Compose start so
`runtime-data/uploads` exists with permissions that allow the hook container to
write and the Nginx container to read. Back up this directory independently of
the Git repository when moving servers.
Run `deploy/scripts/init-secrets.sh` before the first Compose start so the upload
directory exists with permissions that allow the hook container to write and
the Nginx container to read. `IMAGE_UPLOAD_PATH` can point only the uploaded
images at a separate ZFS dataset. Keep `IMAGE_RUNTIME_PATH` on SSD storage for
the small SQLite database and service state. Back up both paths independently
of the Git repository when moving servers.
### Retention and image administration
Account icons below `/icons/users/` are permanent. They are inventoried in the
metadata database but never become deletion candidates. Tiptap images below
`/uploads/` become candidates after 730 days without a request reaching the
origin Nginx. Nginx mirrors a small internal notification to Node for those
requests; Node deduplicates paths in memory and writes each path to SQLite at
most once per flush interval (60 seconds by default).
Cloudflare and browser cache hits do not necessarily reach the origin. The
two-year interval is therefore an operational expiry policy, not proof that an
old HTML document no longer contains the URL. Confirm that the Cloudflare edge
TTL does not exceed one year before enabling this policy.
Candidates remain publicly available until an administrator quarantines them.
Quarantine moves the file into a hidden directory on the same upload bind and
is reversible. Permanent deletion is disabled until the 30-day quarantine
period has elapsed. A request received while an image is only a candidate
returns it to active state; requests do not restore quarantined files.
Open the minimal administration panel at:
```text
https://sam-image.hided.net/admin/
```
Log in with the value in the ignored `secrets/image_admin_panel_password` file.
The panel can filter and preview assets, show storage and retention timestamps,
quarantine candidates, restore quarantined files, and permanently delete files
whose grace period has elapsed. It uses a separate short-lived, HttpOnly,
SameSite session; the stronger `image_admin_secret` used for branch deployment
is never sent to the browser. Rotate the panel password by replacing its secret
file and recreating `image-hook`.
Retention settings are available in `.env`:
```dotenv
IMAGE_CONTENT_RETENTION_DAYS=730
IMAGE_CONTENT_QUARANTINE_DAYS=30
IMAGE_ASSET_TOUCH_FLUSH_SECONDS=60
IMAGE_ASSET_MAINTENANCE_SECONDS=21600
```
The SQLite database uses WAL and synchronous writes. Back it up with the service
stopped or with a SQLite-aware backup; do not copy only the main `.sqlite3` file
while the service is running because committed data can still be in `-wal`.
Legacy HTTP mutation is disabled by default. An emergency PHP rollback must
first stop `image-hook`, then create the ignored `hook/legacy-enabled` sentinel
@@ -181,9 +230,13 @@ curl -fsS https://sam-image.hided.net/game/back.jpg -o /dev/null
curl -fsS https://sam-image.hided.net/image/icons/default.jpg -o /dev/null
```
Nginx serves only `game/`, `icons/`, `hook/list.json`, and
`hook/inventory.v2.json`. The API inventory additionally reports the deployed
branch, full commit, generation time, asset list, and both public base URLs.
Then log in to `/admin/` and verify that an existing account icon is shown as
permanent (`user-icons`, `active`) and a Tiptap upload is shown as `content`.
Nginx serves the static `game/`, `icons/`, and `uploads/` paths, the two public
inventory files, and the authenticated `/admin/` panel. The API inventory
additionally reports the deployed branch, full commit, generation time, asset
list, and both public base URLs.
### Explicit branch deployment
@@ -195,7 +248,9 @@ signed administration command:
./deploy/scripts/admin-deploy.sh <branch> [expected-commit]
```
The administration route is not exposed through the public reverse proxy.
The signed branch-deployment route under `/v1/admin/` is not exposed through
the public reverse proxy. It is separate from the password-authenticated image
management panel at `/admin/`.
### Tests and rollback