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 и заполните значения:
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.
Сборка
make build # frontend -> backend/static -> go:embed -> bin/shared-album
Отдельные шаги:
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, благодаря чему бинарник остаётся полностью статическим.
Проверка:
file bin/shared-album # => statically linked
Миграции
Миграции встроены в бинарник и применяются автоматически при старте сервера. Отдельно:
make migrate # go run ./cmd/migrate — применяет up-миграции к pg.localnet
Локальный запуск
make run # соберёт backend и запустит ./bin/shared-album
# или напрямую:
./bin/shared-album
Сервер слушает HTTP_ADDR (по умолчанию :8080). Health-check:
curl -s localhost:8080/api/health # {"status":"ok","db":"ok","s3":"ok"}
Разработка фронтенда с HMR:
cd frontend && npm run dev # Vite проксирует /api на :8080
Тесты
make test
# интеграционные тесты репозиториев требуют тестовую БД:
TEST_DATABASE_URL='postgres://user:pass@pg.localnet:5432/galo?sslmode=disable' \
cd backend && go test ./internal/repo/ -v
Деплой через systemd
-
Соберите бинарник:
make build. -
Создайте пользователя и каталог:
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 -
Установите unit:
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/meGET|POST /api/albums,GET|PATCH|DELETE /api/albums/:idGET /api/albums/:id/members,POST /api/albums/:id/invites,POST /api/invites/:token/accept,PATCH|DELETE /api/albums/:id/members/:user_idGET|POST /api/albums/:id/photos,GET|DELETE /api/photos/:id,GET /api/photos/:id/downloadPOST|DELETE /api/photos/:id/likes,GET|POST /api/photos/:id/comments,DELETE /api/comments/:idGET /api/sse/albums/:id,GET /api/sse/mePOST|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