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_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 — встроенный режим без внешней БД, только для одного экземпляра приложения на файл (без горизонтального масштабирования).

Сборка

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

Реконсиляция хранилища

Сборка мусора воркера управляется журналом objects и не сканирует хранилище, поэтому блобы без строки в журнале (после сброса БД в обход журнала и т.п.) накапливаются. Ручная сверка:

go run ./cmd/reconcile            # отчёт: сироты (есть в хранилище, нет в журнале) и пропавшие
go run ./cmd/reconcile --delete   # удалить сироты старше --min-age (по умолчанию 1h)
  • По умолчанию ничего не удаляется; удаление только по явному --delete.
  • Область — префикс objects/; блобы старого формата albums/ не затрагиваются.
  • Пропавшие ключи (в журнале есть, в хранилище нет) только сообщаются.

Деплой через systemd

  1. Соберите бинарник: make build.

  2. Создайте пользователя и каталог:

    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:

    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
S
Description
No description provided
Readme
2.1 MiB
0.2.0
Latest
2026-10-23 20:09:06 +00:00
Languages
Kotlin 44.2%
Go 32.7%
Svelte 12.1%
TypeScript 9%
Python 0.9%
Other 0.9%