iu 6528abad82 fix(ui): correct horizontal drag direction in 360 panorama viewer
The panorama viewer inverted horizontal drag relative to vertical: the
yaw sign was opposite, so dragging right turned the view right instead
of moving the scene right. Flip yaw to match the pitch convention
(drag the world) and lock the behavior with a ui-workspace scenario.
2026-10-11 21:02:01 +03:00
2026-10-11 20:41:20 +03:00

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)
  • ffmpeg и ffprobe на хосте — только для видео; без них приём видео отключается, а изображения продолжают работать (см. «Видео»)
  • make

Конфигурация

Скопируйте .env.example в .env и заполните значения:

cp .env.example .env

Секрет подписи токенов (JWT_SECRET) в окружении не задаётся: он генерируется при первом старте и хранится в слое метаданных (app_settings).

Все параметры окружения (регистр имён учитывается):

Переменная Значение по умолчанию Описание
HTTP_ADDR :8080 Адрес прослушивания HTTP-сервера
APP_BASE_URL http://localhost:8080 Внешний базовый URL приложения (для ссылок и SSE)
База данных (метаданные)
DB_BACKEND sqlite Слой метаданных: sqlite или postgres
SQLITE_PATH ./galo.db Путь к файлу SQLite (при DB_BACKEND=sqlite)
PG_HOST pg.localnet Хост PostgreSQL
PG_PORT 5432 Порт PostgreSQL
PG_USER album Пользователь PostgreSQL
PG_PASSWORD (пусто) Пароль PostgreSQL (обязателен при DB_BACKEND=postgres)
PG_DATABASE galo Имя БД PostgreSQL
PG_SSLMODE disable SSL-режим PostgreSQL
Блочное хранилище
STORAGE_BACKEND fs Бэкенд блобов: fs или s3
FS_ROOT ./data Корень ФС для блобов (при STORAGE_BACKEND=fs)
S3_ENDPOINT http://s3.localnet:3900 Endpoint S3/Garage
S3_REGION garage Регион S3
S3_ACCESS_KEY (пусто) Access key S3
S3_SECRET_KEY (пусто) Secret key S3
S3_BUCKET albums Имя бакета S3
S3_USE_PATH_STYLE true Использовать path-style запросы (обязательно для Garage)
OBJECT_URL_TTL 1h Время жизни подписанных URL объектов
URL_SECRET (пусто) Секрет подписи URL объектов; при пустом — секрет из app_settings
Auth / JWT
JWT_ACCESS_TTL 15m Время жизни access-токена
JWT_REFRESH_TTL 720h Время жизни refresh-токена
SSE
SSE_HEARTBEAT_INTERVAL 25s Период heartbeat-сообщений каналов SSE
SSE_CLIENT_BUFFER 64 Размер буфера клиента SSE (количество сообщений)
Фоновый воркер (генерация вариантов + GC объектов)
WORKER_INTERVAL 5s Период проверки воркером очереди задач
WORKER_BATCH 10 Размер батча обрабатываемых задач
WORKER_PENDING_TTL 10m TTL задачи в статусе pending до повторной попытки
Загрузка и видео
IMAGE_MAX_UPLOAD_SIZE 100MB Максимальный размер изображения (байты или суффикс KB/MB/GB)
VIDEO_MAX_UPLOAD_SIZE 2GB Максимальный размер видео
FFMPEG_PATH ffmpeg Путь к ffmpeg (нужен для приёма видео)
FFPROBE_PATH ffprobe Путь к ffprobe
VIDEO_TRANSCODE_CONCURRENCY 1 Параллельных транскодов (отдельно от воркера изображений)
VIDEO_TEMP_DIR (системный tmp) Каталог для временных файлов загрузки/транскода
Логирование
LOG_LEVEL info Уровень логов: debug, info, warn, error
LOG_FORMAT json Формат логов: json или text

На уровне debug сервер пишет по строке на каждый HTTP-запрос (метод, маршрут, статус, длительность, пользователь, request_id) и логирует ожидаемые отказы 4xx. Внутренние ошибки (неожиданные и 5xx) логируются на уровне error с исходной причиной и request_id; сам request_id возвращается клиенту в заголовке ответа X-Request-ID. Маршрут /api/health в access-логи не попадает, а /api/objects/* и /api/sse/* — только на уровне debug. Query-строка, заголовок Authorization и тела запросов в логи не пишутся.

Слои данных выбираются параметрами (по умолчанию — локальный режим):

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

Видео

Видео загружается в те же альбомы, что и фото (kind=video), и готовится для воспроизведения в браузере фоновым воркером:

  • Постер: из видео извлекается кадр, из которого генерируются обычные варианты thumb/medium/preview (WebP) — сетка и обложка показывают постер.
  • Воспроизведение: web-совместимый источник (h264/yuv420p, профиль Baseline/Main/High, звук aac или без звука, контейнер mp4/mov/mkv, разрешение ≤ 1080p) ремуксится без перекодирования; иначе выполняется транскод в h264/yuv420p/aac с +faststart. Готовый вариант хранится как объект роли playback и отдаётся с поддержкой Range (перемотка).
  • Оригинал сохраняется отдельно и используется для скачивания.
  • Транскод идёт в отдельном малом пуле (VIDEO_TRANSCODE_CONCURRENCY) с длинным лизингом и не вытесняет генерацию вариантов изображений. Роль playback появляется по готовности и транслируется подписчикам альбома (photo.updated).

Без ffmpeg/ffprobe приём видео отклоняется, а /api/health сообщает "video": "unavailable"; изображения работают как прежде.

Сборка

make build      # frontend -> backend/static -> go:embed -> bin/galo

Отдельные шаги:

make frontend   # npm ci && npm run build, копирует статику в backend/static
make backend    # CGO_ENABLED=0 go build -tags nodynamic -o bin/galo

-tags nodynamic обязателен: он заставляет gen2brain/webp использовать транспилированный в Go libwebp вместо динамической загрузки через purego, благодаря чему бинарник остаётся полностью статическим.

Проверка:

file bin/galo     # => statically linked

Миграции

Миграции встроены в бинарник и применяются автоматически при старте сервера. Отдельно:

make migrate    # go run ./cmd/migrate — применяет up-миграции к pg.localnet

Локальный запуск

make run        # соберёт backend и запустит ./bin/galo
# или напрямую:
./bin/galo

Сервер слушает 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

Наполнение тестовыми данными

Скрипт scripts/seed.py наполняет запущенный инстанс тестовыми данными через публичный API: создаёт альбомы, генерирует фотографии и, по флагам, добавляет теги, комментарии, лайки, share-ссылки и инвайты. Работает только на добавление — существующие данные не удаляются и не изменяются.

GALO_EMAIL=owner@example.com GALO_PASSWORD=secret \
  python3 scripts/seed.py --albums 2 --photos-per-album 10 --tags --likes

Ту же операцию можно выполнить через Make-цель (параметры — переменные сборки):

GALO_EMAIL=owner@example.com GALO_PASSWORD=secret \
  make testdata ALBUMS=3 PHOTOS=5 TAGS=1 LIKES=1 SEED=42

Значения по умолчанию: ALBUMS=2, PHOTOS=10, SEED=0, TIMEOUT=30. Булевы параметры включаются значением 1: TAGS, COMMENTS, LIKES, SHARE, INVITES, WAIT, DRY, VIDEO. Дополнительные флаги передаются через ARGS.

Переменные окружения:

Переменная По умолчанию Описание
GALO_BASE_URL http://localhost:8080 Базовый URL инстанса
GALO_EMAIL — Email владельца (обязательно)
GALO_PASSWORD — Пароль владельца (обязательно)

Параметры:

Флаг По умолчанию Описание
--albums 2 Число альбомов
--photos-per-album 10 Фотографий в каждом альбоме
--seed 0 Seed генератора (воспроизводимость)
--tags выкл. Добавлять теги
--comments выкл. Добавлять комментарии
--likes выкл. Добавлять лайки
--share-links выкл. Создавать share-ссылки на альбомы
--invites выкл. Создавать инвайты (роль member)
--wait-variants выкл. Дожидаться генерации превью каждого фото
--video выкл. Загружать видео-фикстуры (требует ffmpeg; иначе пропускаются)
--timeout 30 Таймаут ожидания и сетевых запросов, сек
--dry-run выкл. Показать план и не отправлять изменяющих запросов

Изображения генерируются на лету: при установленной Pillow — JPEG с EXIF (дата съёмки и GPS), иначе — PNG без EXIF на стандартной библиотеке. Ядро скрипта не требует сторонних пакетов; Pillow — необязательная зависимость.

Тесты

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/galo album
    sudo mkdir -p /opt/galo
    sudo cp bin/galo /opt/galo/
    sudo cp .env /opt/galo/.env
    sudo chown -R album:album /opt/galo
    
  3. Установите unit:

    sudo cp deploy/galo.service /etc/systemd/system/
    sudo systemctl daemon-reload
    sudo systemctl enable --now galo
    sudo journalctl -u galo -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
4.5 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%