feat: implement shared-album multi-user photo album
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.
This commit is contained in:
@@ -0,0 +1,163 @@
|
||||
# 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`
|
||||
Reference in New Issue
Block a user