diff --git a/README.md b/README.md new file mode 100644 index 0000000..0e15cbc --- /dev/null +++ b/README.md @@ -0,0 +1,98 @@ +# Tanabata File Manager + +A multi-user, tag-based web file manager for images and video. Go + Gin backend +(Clean Architecture, pgx, goose migrations), SvelteKit SPA frontend, PostgreSQL, +JWT auth — shipped as a single Docker image that serves both the API and the +built SPA on one port. + +## Documentation + +- [`openapi.yaml`](openapi.yaml) — full REST API specification +- [`docs/DEPLOY.md`](docs/DEPLOY.md) — production deploy (Gitea Actions → host) +- [`docs/GO_PROJECT_STRUCTURE.md`](docs/GO_PROJECT_STRUCTURE.md) — backend architecture +- [`docs/FRONTEND_STRUCTURE.md`](docs/FRONTEND_STRUCTURE.md) — frontend architecture +- [`.env.example`](.env.example) — every configuration variable, documented + +## Quick start + +```bash +cp .env.example .env # then edit the secrets (JWT_SECRET, ADMIN_PASSWORD, …) +docker compose up -d --build +``` + +By default this runs the app plus a bundled PostgreSQL container +(`COMPOSE_PROFILES=with-db`). To point at a Postgres already on the host, set +`COMPOSE_PROFILES=` empty and aim `DATABASE_URL` at `host.docker.internal`. See +[`.env.example`](.env.example) for the full matrix. + +The app is published on **127.0.0.1** only and expects a reverse proxy in front +(see below). The default port is **42776** — the sum of the Unicode code points +of 七夕. + +## Reverse proxy (nginx) + +The container publishes its port on loopback (`127.0.0.1:${APP_PORT}:42776` in +[`docker-compose.yml`](docker-compose.yml)), so a reverse proxy on the host +terminates TLS and forwards to it. Three settings matter for this app: + +1. **`client_max_body_size`** — uploads go up to `MAX_UPLOAD_BYTES` (500 MiB by + default). nginx caps request bodies at **1 MiB** out of the box, so without + this every large upload fails with `413`. +2. **Forwarded headers** — the app trusts `X-Forwarded-For` only from the hops in + `TRUSTED_PROXIES` (default: loopback + Docker bridge ranges) and keys its + login/refresh rate limiter on the resulting client IP. If the proxy doesn't + send the header, every request looks like it comes from the proxy and shares + one rate-limit bucket. +3. **Streaming for big media** — turning request/response buffering off lets + large uploads stream straight to the app and lets video range-seeks work + without nginx spooling whole files to disk first. + +```nginx +server { + listen 443 ssl; + server_name tanabata.example.com; + + # ssl_certificate / ssl_certificate_key ... (e.g. from certbot) + + # Match MAX_UPLOAD_BYTES (500 MiB default); nginx defaults to 1m → 413. + client_max_body_size 512m; + + location / { + proxy_pass http://127.0.0.1:42776; # APP_PORT + proxy_http_version 1.1; + + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + # Stream large uploads/downloads instead of buffering to disk; keeps + # video range-seek responsive. Scope these to file/preview locations + # instead if you'd rather keep buffering for small JSON responses. + proxy_request_buffering off; + proxy_buffering off; + proxy_read_timeout 300s; + proxy_send_timeout 300s; + } +} +``` + +If you run the app **without** a proxy and want it reachable on the LAN, drop the +`127.0.0.1:` prefix from the `ports` line in +[`docker-compose.yml`](docker-compose.yml) and adjust `TRUSTED_PROXIES` +accordingly. + +## Development + +```bash +# Backend +cd backend +go run ./cmd/server # dev server +go test ./... # all tests + +# Frontend +cd frontend +npm run dev # Vite dev server +npm run build # production build +npm run generate:types # regenerate API types from openapi.yaml +```