213 lines
8.9 KiB
Markdown
213 lines
8.9 KiB
Markdown
# Shared image repository
|
|
|
|
`icons/` is served as `/image/icons/`. General portraits follow these rules:
|
|
|
|
- `icons/장수/<장수명>.<확장자>` is the canonical shared path.
|
|
- Scenario-specific collections keep a separate directory such as
|
|
`icons/걸그룹/` or `icons/롤시나리오/`.
|
|
- Root-level numeric files such as `icons/1047.jpg` are compatibility aliases
|
|
for deployed legacy versions. Do not delete or replace them while those
|
|
versions are in service.
|
|
- A general name must map to one canonical file. If two records intentionally
|
|
need different portraits, disambiguate the scenario name or use an explicit
|
|
scenario-specific path.
|
|
|
|
The core2026 repository owns `resources/general-icons.json` and
|
|
`tools/manage-general-icons.mjs`. Use that tool with this repository as
|
|
`--image-root` to synchronize aliases, scenario paths, and verify that every
|
|
catalog source exists and has identical bytes.
|
|
|
|
## Public URLs
|
|
|
|
The same tracked files are exposed through both URL contracts:
|
|
|
|
- `https://sam.hided.net/image/game/...` and `/image/icons/...`
|
|
- `https://sam-image.hided.net/game/...` and `/icons/...`
|
|
- `https://sam-image.hided.net/image/...` is a compatibility alias.
|
|
|
|
Do not redirect the old `/image/*` URLs to the dedicated domain. Existing PHP
|
|
and core2026 clients use same-origin relative paths and can move independently.
|
|
The image server never exposes the repository root, `.git`, PHP sources, or
|
|
directory listings.
|
|
|
|
## Node webhook deployment service
|
|
|
|
`compose.yaml` runs a read-only Nginx static edge and an internal Node service.
|
|
Only the edge publishes port 8191. The Node service verifies the raw Gitea
|
|
`X-Gitea-Signature`, accepts push events for `devsam/image`, fetches the exact
|
|
remote branch tip, rejects dirty or non-fast-forward updates, and serializes all
|
|
Git changes. The initial and only allowed branch is `master` unless
|
|
`IMAGE_ALLOWED_BRANCHES` is explicitly expanded.
|
|
|
|
The service is reverse-proxy agnostic. Caddy, Nginx, HAProxy, or another proxy
|
|
can use the same port and path contract.
|
|
|
|
For a native reverse proxy running on the same host as Docker, publish port
|
|
8191 on loopback only and proxy to `127.0.0.1:8191`:
|
|
|
|
```dotenv
|
|
IMAGE_BIND_ADDRESS=127.0.0.1
|
|
IMAGE_PORT=8191
|
|
TRUSTED_PROXY_CIDRS=172.24.0.1/32
|
|
```
|
|
|
|
These two addresses intentionally describe different hops. The native proxy
|
|
connects to the host loopback address, while the Nginx process inside
|
|
`image-web` normally observes the connection as coming from that Compose
|
|
network's Docker bridge gateway (`172.24.0.1` in the example). Use the source
|
|
shown in the `image-web` access/error log, then recreate `image-web` whenever
|
|
`TRUSTED_PROXY_CIDRS` changes. Do not replace it with a Cloudflare CIDR: the
|
|
direct peer of `image-web` remains the host-native reverse proxy path.
|
|
|
|
If the reverse proxy is on another host, bind an explicit private interface
|
|
instead of loopback and apply the host `DOCKER-USER` (or equivalent) firewall
|
|
rule. Keep every source CIDR observed by `image-web` in the untracked
|
|
`TRUSTED_PROXY_CIDRS` value, separated by commas or spaces. Avoid `0.0.0.0`
|
|
unless the firewall has already been verified.
|
|
|
|
### Prepare
|
|
|
|
```sh
|
|
cp .env.example .env
|
|
./deploy/scripts/init-secrets.sh
|
|
docker compose config --quiet
|
|
docker compose build
|
|
```
|
|
|
|
Only for a proxy on another host, apply and inspect the dedicated Docker
|
|
ingress chain with root privileges:
|
|
|
|
```sh
|
|
sudo env TRUSTED_PROXY_CIDRS=192.0.2.10/32 IMAGE_PORT=8191 \
|
|
./deploy/scripts/firewall-8191.sh apply
|
|
sudo ./deploy/scripts/firewall-8191.sh check
|
|
```
|
|
|
|
The script only owns the `SAM_IMAGE_INGRESS` chain and its port-8191 jump from
|
|
`DOCKER-USER`; it does not flush shared firewall chains.
|
|
|
|
For the same-host native-proxy case, point the proxy upstream at loopback. A
|
|
minimal Caddy site is:
|
|
|
|
```caddyfile
|
|
sam-image.hided.net {
|
|
reverse_proxy 127.0.0.1:8191
|
|
}
|
|
```
|
|
|
|
The equivalent Nginx/HAProxy configuration should use the same upstream. The
|
|
application does not depend on Caddy-specific request behavior.
|
|
|
|
Secret values are generated under ignored `secrets/` files with mode 0600 and
|
|
are never printed. Put the contents of `secrets/gitea_webhook_secret` into the
|
|
Gitea webhook configuration. Configure a JSON push webhook targeting:
|
|
|
|
```text
|
|
https://sam-image.hided.net/v1/hooks/gitea
|
|
```
|
|
|
|
Use branch filter `master`. Disable the old PHP webhook before enabling the new
|
|
writer. The legacy PHP files remain in `hook/` for an explicit rollback, but
|
|
PHP and Node must never mutate the checkout concurrently.
|
|
|
|
### Fallback sync callers
|
|
|
|
If Gitea webhook delivery is missed, Core and Core2026 can request a restricted
|
|
reconciliation through `POST /v1/sync`. This endpoint cannot select or change a
|
|
branch: it only fetches the current active branch and applies the same clean
|
|
worktree and fast-forward checks as a webhook deployment.
|
|
|
|
Each caller has a separate secret:
|
|
|
|
- `secrets/image_sync_core_secret` for legacy Core
|
|
- `secrets/image_sync_core2026_secret` for Core2026
|
|
|
|
The caller sends its name, a timestamp, a unique request ID, and an HMAC-SHA256
|
|
signature over `timestamp.request-id.<exact JSON body>`. Requests expire after
|
|
five minutes and successful request IDs are persisted for replay protection.
|
|
The body is either `{}` or `{ "commit": "<full-image-commit-sha>" }`; all other
|
|
fields are rejected. The optional commit asserts the expected remote tip and
|
|
does not grant checkout selection.
|
|
|
|
Distribute only the matching caller secret through an ignored secret file.
|
|
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
|
|
|
|
Core and Core2026 can store validated account icons and editor attachments
|
|
through this service with
|
|
`PUT /v1/uploads/<user-icons|content>/<client>/<random-32-hex>.<extension>`. Each game
|
|
server validates the authenticated user and image first, then sends the raw
|
|
image body with `X-Image-Client`, `X-Image-Expires`, `X-Image-Request-Id`, and
|
|
`X-Image-Signature` headers.
|
|
|
|
The signature is HMAC-SHA256 over
|
|
`expires.requestId.pathname.contentType.sha256(body)`. Expiry may be at most
|
|
five minutes in the future, so a grant cannot be reused for another path,
|
|
content type, body, or later upload. The service also checks the image magic,
|
|
caller scope, and request replay before it writes one immutable file below the
|
|
host bind directory `runtime-data/uploads`. User uploads are deliberately not
|
|
added to Git; Nginx exposes that bind read-only at `/icons/users/` for account
|
|
icons and `/uploads/` for editor content. User icons retain the 50KB limit;
|
|
editor content retains the existing 1MB limit.
|
|
|
|
Create separate upload secrets with `deploy/scripts/init-secrets.sh`. Mount only
|
|
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.
|
|
|
|
Legacy HTTP mutation is disabled by default. An emergency PHP rollback must
|
|
first stop `image-hook`, then create the ignored `hook/legacy-enabled` sentinel
|
|
in the legacy checkout before restoring its Caddy/Gitea route. Remove the
|
|
sentinel before Node is started again. CLI execution of `hook/git_pull.php`
|
|
from the `hook/` directory remains available for local recovery without
|
|
exposing the HTTP endpoint.
|
|
|
|
### Start and verify
|
|
|
|
```sh
|
|
docker compose up -d --build
|
|
docker compose ps
|
|
curl -fsS https://sam-image.hided.net/healthz
|
|
curl -fsS https://sam-image.hided.net/v1/status
|
|
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.
|
|
|
|
### Explicit branch deployment
|
|
|
|
Automatic pushes only update the active branch. Add a branch to
|
|
`IMAGE_ALLOWED_BRANCHES`, recreate `image-hook`, and switch it with the internal
|
|
signed administration command:
|
|
|
|
```sh
|
|
./deploy/scripts/admin-deploy.sh <branch> [expected-commit]
|
|
```
|
|
|
|
The administration route is not exposed through the public reverse proxy.
|
|
|
|
### Tests and rollback
|
|
|
|
```sh
|
|
docker build -t sam-image-hook:test node-hook
|
|
docker run --rm sam-image-hook:test npm test
|
|
docker compose exec -T image-web nginx -t -c /tmp/nginx.conf
|
|
```
|
|
|
|
Stopping `image-hook` does not remove the last checked-out files from the
|
|
static service. For a full rollback, stop Node mutation first, restore the old
|
|
Caddy/PHP webhook route, and only then enable the legacy writer. Preserve both
|
|
checkouts until public file hashes and representative browser requests have
|
|
been verified.
|