Files
ds-galo/README.md
iu cb272398ce feat(auth): store the token signing secret in the metadata layer
- app_settings table (PostgreSQL migration 000010, SQLite 000002)
- internal/settings repository (Get/Set/Ensure) wired into store.Repos
- secret generated with crypto/rand on first start and persisted; reused on restart
- drop required JWT_SECRET from env/config; app.New takes a TokenManager and
  storage.Open takes the resolved secret (URL_SECRET still overrides)
- docs updated
2026-10-11 10:18:31 +03:00

191 lines
8.0 KiB
Markdown

# Galo — многопользовательский фотоальбом
Self-hosted веб-приложение: общие фотоальбомы с ролями, загрузкой фото в S3,
live-обновлениями через SSE и доступом по ссылке без аккаунта.
Без Docker: один статичный Go-бинарник (`CGO_ENABLED=0`) со вшитой статикой
SvelteKit через `go:embed`, запуск через systemd.
## Стек
- **Backend:** Go, Gin, pgx (PostgreSQL на `pg.localnet`), AWS SDK v2 (Garage на
`s3.localnet`, path-style), нативный SSE, golang-migrate, slog.
- **Frontend:** SvelteKit 2 + Svelte 5, adapter-static, TypeScript, TailwindCSS.
- **Обработка изображений:** `disintegration/imaging` + `gen2brain/webp` +
`rwcarlsen/goexif` (всё чистый Go).
## Требования
- Go 1.22+ (сборка проверена на 1.26)
- Node.js 20+ (только для сборки фронтенда)
- Доступ к `pg.localnet:5432` (PostgreSQL) и `s3.localnet:3900` (Garage)
- `make`
## Конфигурация
Скопируйте `.env.example` в `.env` и заполните значения:
```bash
cp .env.example .env
```
Ключевые переменные: `PG_*`, `S3_*` (обязательны `UsePathStyle=true` и регион
`garage`), `JWT_ACCESS_TTL`, `JWT_REFRESH_TTL`, `HTTP_ADDR`,
`SSE_HEARTBEAT_INTERVAL`, `SSE_CLIENT_BUFFER`. Секрет подписи токенов не задаётся
в окружении: он генерируется при первом старте и хранится в слое метаданных
(`app_settings`).
Слои данных выбираются параметрами (по умолчанию — локальный режим):
- `STORAGE_BACKEND=fs|s3` — где лежат блобы: `fs` (по умолчанию, `FS_ROOT=./data`)
или `s3` (S3/Garage); наружу всегда отдаётся бэкенд (`/api/objects/<key>`).
- `DB_BACKEND=sqlite|postgres` — где лежат метаданные: `sqlite` (по умолчанию,
`SQLITE_PATH=./galo.db`) или `postgres`. SQLite — встроенный режим без внешней БД,
**только для одного экземпляра приложения на файл** (без горизонтального
масштабирования).
## Сборка
```bash
make build # frontend -> backend/static -> go:embed -> bin/shared-album
```
Отдельные шаги:
```bash
make frontend # npm ci && npm run build, копирует статику в backend/static
make backend # CGO_ENABLED=0 go build -tags nodynamic -o bin/shared-album
```
> `-tags nodynamic` обязателен: он заставляет `gen2brain/webp` использовать
> транспилированный в Go libwebp вместо динамической загрузки через purego,
> благодаря чему бинарник остаётся полностью статическим.
Проверка:
```bash
file bin/shared-album # => statically linked
```
## Миграции
Миграции встроены в бинарник и применяются автоматически при старте сервера.
Отдельно:
```bash
make migrate # go run ./cmd/migrate — применяет up-миграции к pg.localnet
```
## Локальный запуск
```bash
make run # соберёт backend и запустит ./bin/shared-album
# или напрямую:
./bin/shared-album
```
Сервер слушает `HTTP_ADDR` (по умолчанию `:8080`). Health-check:
```bash
curl -s localhost:8080/api/health # {"status":"ok","db":"ok","s3":"ok"}
```
Разработка фронтенда с HMR:
```bash
cd frontend && npm run dev # Vite проксирует /api на :8080
```
## Тесты
```bash
make test
# интеграционные тесты репозиториев требуют тестовую БД:
TEST_DATABASE_URL='postgres://user:pass@pg.localnet:5432/galo?sslmode=disable' \
cd backend && go test ./internal/repo/ -v
```
## Реконсиляция хранилища
Сборка мусора воркера управляется журналом `objects` и не сканирует хранилище, поэтому
блобы без строки в журнале (после сброса БД в обход журнала и т.п.) накапливаются.
Ручная сверка:
```bash
go run ./cmd/reconcile # отчёт: сироты (есть в хранилище, нет в журнале) и пропавшие
go run ./cmd/reconcile --delete # удалить сироты старше --min-age (по умолчанию 1h)
```
- По умолчанию ничего не удаляется; удаление только по явному `--delete`.
- Область — префикс `objects/`; блобы старого формата `albums/` не затрагиваются.
- Пропавшие ключи (в журнале есть, в хранилище нет) только сообщаются.
## Деплой через systemd
1. Соберите бинарник: `make build`.
2. Создайте пользователя и каталог:
```bash
sudo useradd --system --home /opt/shared-album album
sudo mkdir -p /opt/shared-album
sudo cp bin/shared-album /opt/shared-album/
sudo cp .env /opt/shared-album/.env
sudo chown -R album:album /opt/shared-album
```
3. Установите unit:
```bash
sudo cp deploy/shared-album.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now shared-album
sudo journalctl -u shared-album -f
```
## nginx (опционально)
Пример в `deploy/nginx-sse.conf`. Для SSE критично `proxy_buffering off` и
`X-Accel-Buffering: no` (последнее сервер выставляет сам).
## Структура
```
backend/
cmd/server/ # точка входа
cmd/migrate/ # применение миграций
internal/config # чтение env
internal/db # pgx + golang-migrate
internal/storage # S3 (Garage) / локальная ФС, отдача через бэкенд
internal/store # фабрика репозиториев (postgres / sqlite)
internal/auth # JWT, bcrypt, refresh
internal/albums # альбомы
internal/members # участники, роли, инвайты
internal/photos # загрузка, варианты, отдача через бэкенд
internal/social # лайки, комментарии
internal/notifications # персональные уведомления
internal/sharelinks # ссылки для просмотра
internal/sse # SSE-хаб
internal/app # маршруты и статика
migrations/ # SQL-миграции
static/ # сюда копируется build SvelteKit
frontend/ # SvelteKit 2 + Svelte 5
deploy/ # systemd unit, пример nginx
```
## API
Полный список эндпоинтов: `project.md`, раздел 11. Кратко:
- `POST /api/auth/register|login|refresh|logout`, `GET /api/auth/me`
- `GET|POST /api/albums`, `GET|PATCH|DELETE /api/albums/:id`
- `GET /api/albums/:id/members`, `POST /api/albums/:id/invites`,
`POST /api/invites/:token/accept`, `PATCH|DELETE /api/albums/:id/members/:user_id`
- `GET|POST /api/albums/:id/photos`, `GET|DELETE /api/photos/:id`,
`GET /api/photos/:id/download`
- `POST|DELETE /api/photos/:id/likes`, `GET|POST /api/photos/:id/comments`,
`DELETE /api/comments/:id`
- `GET /api/sse/albums/:id`, `GET /api/sse/me`
- `POST|GET /api/albums/:id/share`, `DELETE /api/albums/:id/share/:link_id`,
`GET /api/share/:token[/photos[/:photo_id[/download]]]`
- `GET /api/notifications`, `POST /api/notifications/:id/read`