Backend (Go/Gin/pgx): env config, pgx pool with retries, Garage S3 client (path-style, region garage), embedded golang-migrate migrations, JWT auth with refresh rotation, albums CRUD, members/roles/invites with no-escalation matrix, photo upload + async imaging/goexif WebP variants, cursor pagination, presigned URLs, likes/comments, SSE hub (album + personal), notifications, share links. Frontend (SvelteKit 2 + Svelte 5 + Tailwind v4): API wrapper with 401 refresh, auth store, all screens, Uploader/PhotoGrid/Lightbox/Comments, SSE client with reconnect + REST catch-up, three themes + system via data-theme. Deploy without Docker: Makefile builds the frontend into backend/static and embeds it (go:embed) into a single statically linked binary; systemd unit and nginx SSE example included. Use gen2brain/webp (-tags nodynamic) instead of cgo kolesa-team/go-webp to keep CGO_ENABLED=0 static builds; design.md/proposal.md updated accordingly.
164 lines
5.9 KiB
Markdown
164 lines
5.9 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_SECRET`, `JWT_ACCESS_TTL`, `JWT_REFRESH_TTL`, `HTTP_ADDR`,
|
|
`SSE_HEARTBEAT_INTERVAL`, `SSE_CLIENT_BUFFER`.
|
|
|
|
## Сборка
|
|
|
|
```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
|
|
```
|
|
|
|
## Деплой через 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/auth # JWT, bcrypt, refresh
|
|
internal/albums # альбомы
|
|
internal/members # участники, роли, инвайты
|
|
internal/photos # загрузка, варианты, presigned URL
|
|
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`
|