Фотогалереи для фотографов и их клиентов: просмотр, лайки и скачивание оригиналов по одной ссылке. Независимое приложение с подключаемыми хранилищами.
  • TypeScript 64.4%
  • Vue 14.2%
  • HTML 9.6%
  • JavaScript 8.2%
  • CSS 2.9%
  • Other 0.7%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
2026-11-02 02:29:19 +07:00
apps исправил лимиты на загрузку галереи 2026-11-02 02:29:19 +07:00
docs исправил лимиты на загрузку галереи 2026-11-02 02:29:19 +07:00
example-design редизайн 2026-11-01 19:10:39 +07:00
infra добавил возможность через .env указать network докера 2026-11-02 01:42:13 +07:00
packages рефактор 2026-11-01 12:44:59 +07:00
scripts добавил возможность через .env указать network докера 2026-11-02 01:42:13 +07:00
tests редизайн, правки, добавил допуск для выделения 2026-11-02 01:03:22 +07:00
.dockerignore редизайн 2026-10-16 22:01:49 +07:00
.env.example добавил возможность через .env указать network докера 2026-11-02 01:42:13 +07:00
.gitignore .gitignore update 2026-10-31 01:17:58 +07:00
.prettierignore редизайн, правки, багованный вариант, выделение сломано 2026-11-01 21:15:17 +07:00
.prettierrc.json первый коммит 2026-10-15 22:10:49 +07:00
AGENTS.md редизайн, правки, исправлен внешний вид состояний иконок на превью фото 2026-11-02 00:54:34 +07:00
eslint.config.js масштабный рефактор 2026-11-01 17:52:51 +07:00
gallery редизайн 2026-10-16 22:01:49 +07:00
package.json рефактор кода, выделил базовые компоненты 2026-10-31 13:12:05 +07:00
playwright.config.ts редизайн, правки, исправлен баг ошибки в консоли из-за выделения 2026-11-01 23:37:24 +07:00
pnpm-lock.yaml рефактор кода, выделил базовые компоненты 2026-10-31 13:12:05 +07:00
pnpm-workspace.yaml обновил зависимости 2026-10-31 02:03:03 +07:00
README.md исправил лимиты на загрузку галереи 2026-11-02 02:29:19 +07:00
tsconfig.json второй этап реализован 2026-10-16 00:20:32 +07:00
tsup.config.ts промежуточный 2026-10-16 01:44:23 +07:00
vitest.config.ts масштабный рефактор 2026-11-01 17:52:51 +07:00

Gallery

Самостоятельная фотогалерея со своим Vue-интерфейсом, Fastify API и SQLite. Фотограф импортирует публичную папку Nextcloud, клиент открывает /g/:slug, выбирает фотографии и скачивает оригинал, выбранные кадры или всю галерею ZIP без регистрации. Оригиналы остаются в Nextcloud. Демогалерея на MockProvider сохранена.

Документация для самостоятельной работы

Начните с оглавления документации. Основные руководства:

  • Карта проекта — где лежат файлы, что делают функции и как проходят данные.
  • Разработка — запуск dev, изменение интерфейса/API/БД, провайдеры и отдельные тесты.
  • Эксплуатация — настройки, хранение данных, backup, диагностика и доступ с телефона.

Собственный шрифт иконок: классы и добавление новых иконок. Каталог после запуска — /icons.html; пересборка — ./gallery icons:build.

Запуск с реальными фотографиями

На компьютере нужны Docker Engine и Compose >=2.20.0; системные Node.js/npm/pnpm не используются. Gallery имеет отдельный Compose и не изменяет Nextcloud или другие контейнеры.

./gallery admin:setup
./gallery up

Первая команда дважды запросит пароль (не отображается, минимум 12 символов) и запишет в .env Argon2id hash, а не пароль, а также случайный ключ шифрования источников. Файл получает права 0600. Повторный запуск сохраняет существующий ключ источников и меняет пароль; старые сессии после перезапуска станут недействительными.

Все команды ./gallery явно передают Compose корневой .env. После настройки администратора или изменения .env выполните ./gallery up: уже работающий контейнер получает новые переменные только при пересоздании. Если ранее панель сообщала «Административный доступ не настроен», хотя настройка была выполнена, достаточно ./gallery up с исправленным скриптом; повторно менять пароль не нужно. Обычный docker compose restart не обновляет окружение контейнера.

Откройте http://localhost:8080/admin:

  1. Войдите с выбранным паролем.
  2. Откройте «Галереи → Создать галерею» и вставьте уже существующую публичную ссылку на папку: https://nextcloud.example.com/s/TOKEN.
  3. Укажите название и slug, например our-journey. Описание необязательно.
  4. Нажмите «Создать и опубликовать». Gallery проверит источник, обойдёт вложенные папки и построит пробное превью до сохранения.
  5. Откройте ссылку /g/our-journey в списке галерей. Клиентам вход не нужен. «Скачать оригинал» находится в полноэкранном просмотре и запускает обычный GET со скачиванием файла.

Первое создание большой галереи может занимать несколько минут: Gallery заранее определяет размеры фотографий. Для nginx установите proxy_read_timeout 1200s;, чтобы дождаться ответа. Бюджет анализа и диагностика.

Ссылка должна разрешать чтение и скачивание, не иметь пароля и работать через публичный WebDAV Nextcloud 29+. Поддерживаются JPEG, PNG, WebP и AVIF; RAW, HEIC, SVG, GIF и видео пока исключены. Если папка не содержит поддерживаемых кадров или пробное превью не удалось, запись галереи не создаётся. Существующие Nextcloud shares Gallery не создаёт и не изменяет.

Администратор может отключить галерею и опубликовать её снова. Отключение блокирует API списка, превью и оригиналов, включая старые URL. Slug — адрес, а не пароль: любой, у кого есть ссылка на опубликованную галерею, может её открыть.

./gallery logs
./gallery down

up применяет миграции, собирает приложение, выполняет API/Nextcloud-контрактные тесты на фикстурах и ждёт успешного healthcheck до 90 секунд. При ошибке выводятся последние логи. down сохраняет SQLite; down -v удаляет данные Gallery, но никогда не оригиналы Nextcloud.

Настройки и HTTPS

Значения в корневом .env:

PUBLIC_ORIGIN=http://localhost:8080
NEXTCLOUD_BASE_URL=https://nextcloud.example.com
GALLERY_PORT=8080
SEED_DEMO=true

ADMIN_PASSWORD_HASH и SOURCE_ENCRYPTION_KEY создаёт ./gallery admin:setup. Не заменяйте ключ при обычном обновлении: без него сохранённые источники не расшифровать. Сохраняйте резервную копию .env отдельно от БД. PHC-хэш в .env заключён в одинарные кавычки, чтобы Compose не интерполировал символы $.

Для внешнего доступа используйте свой HTTPS reverse proxy на 127.0.0.1:8080 и задайте точный PUBLIC_ORIGIN=https://photo.example.com без завершающего /. Запросы изменения состояния требуют совпадающего Origin. Secure cookie обязательна при HTTPS; HTTP разрешён только для loopback-разработки. При использовании другого локального порта измените и GALLERY_PORT, и PUBLIC_ORIGIN, затем повторите ./gallery up.

Для nginx в Docker можно включить штатный режим внешней сети в .env:

GALLERY_NETWORK_MODE=proxy
GALLERY_DOCKER_NETWORK=proxy-network
PUBLIC_ORIGIN=https://example.org

После ./gallery up Gallery подключается к уже существующей сети, не публикуя порт на хосте. Nginx в той же сети использует proxy_pass http://gallery-public:3000;. Команды ./gallery deploy, backup, logs и down учитывают тот же режим автоматически; дополнительные файлы на сервере и ручной docker network connect не нужны. По умолчанию GALLERY_NETWORK_MODE=host сохраняет локальный порт 8080. Этот режим не означает Docker network_mode: host. Подробности и проверка nginx после обновления — эксплуатация.

Разрешённый сервер задаёт только NEXTCLOUD_BASE_URL. Поле формы не может изменить origin или выбрать произвольный HTTP endpoint. Редиректы источника не выполняются. Сохранённая ссылка привязана к настроенному серверу: смена NEXTCLOUD_BASE_URL не перенаправит старые галереи на другой сервер.

Если настройки администратора отсутствуют, MockProvider и публичное чтение продолжают работать, а административный API отвечает AUTH_DISABLED. Если задан только hash или только ключ, запуск останавливается с ошибкой конфигурации. Mock: http://localhost:8080/g/demo, если включён SEED_DEMO. Повторное заполнение демо не меняет ID и публикацию.

SQLite хранится в production-томе gallery-data. Для резервной копии остановите приложение и скопируйте том целиком (включая WAL/SHM при наличии). Оригиналов в этой БД нет. Никогда не монтируйте приватный Nextcloud data или его БД в Gallery.

Что реализовано

  • Независимый домен и порт MediaSourceProvider; MockProvider и отдельный NextcloudProvider.
  • Собственные UUID галерей/кадров и cursor-пагинация; клиент не передаёт файловые пути.
  • Публичный DAV PROPFIND с X-Requested-With, рекурсивный обход через Depth: 1, обработка успешных DAV-свойств по namespace и HTTP status.
  • Из источника берутся имя, MIME, размер, ETag и размеры при наличии DAV-метаданных. Неизвестные ширина/высота записываются как 0; сетка использует реальные пропорции загруженного превью.
  • Sharp-превью с авторотацией, ограниченным кэшем только производных JPEG; оригинал потоково читается из источника и не записывается на диск.
  • Перед метаданными/списком проверяется источник; перед превью/оригиналом — источник и конкретный файл. Кэш превью проверяется после проверки прав; даже для кэшированного кадра выполняется GET-проверка с отменой передачи после заголовков, чтобы один только DAV не обходил новый запрет скачивания. Учитываются oc:permissions, hide-download, запрет download в share attributes и отказ самого GET. При недостоверном праве чтения доступ закрывается.
  • Админ-вход с Argon2id, HttpOnly/SameSite=Strict cookie, серверными сессиями SQLite, ограничением попыток входа и Origin-check. В БД хранится SHA-256 digest сессии. Пароль не сохраняется.
  • Источники зашифрованы AES-256-GCM; токены, DAV href и credentials не возвращаются публичным или административным DTO и не логируются.
  • Раздельные серверные лайки сердечком и локальное выделение для скачивания, фильтр, счётчик, «Выбрать все»/«Снять выделение». LocalStorage привязан к UUID галереи и assetId; это локальный выбор для скачивания, не отправка фотографу.
  • Потоковые ZIP64 выбранных и всей галереи через общий сервис над MediaSourceProvider; краткоживущие download tickets в SQLite. MockProvider также отдаёт реальные JPEG-фикстуры как оригиналы.
  • Русский mobile-first интерфейс: обложка, lazy-сетка, fullscreen dialog, клавиатура, свайпы, ошибки и повтор загрузки, отдельный оригинал по attachment-ссылке. Админка с отдельными страницами, карточками, настройками, папками и статистикой.
  • noindex, no-store, CSP, отсутствие трекеров, типизированные ошибки, OpenAPI /api/v1/openapi.json.

Ограничения: до 10 000 фотографий, 500 папок, глубина вложенности 10; XML-ответ до 2 MiB и ограниченное время обхода. Оригинал — до 200 MiB и 120 секунд передачи; превью — вход до 40 MiB, до 80 MP, две операции одновременно, до 64 ожидающих. Декодирование ограничено по времени; перед Sharp проверяется растровая сигнатура. Выход превью ограничен 8 MiB; кэш — 16 MiB/128 записей, не более 4 MiB на запись. Для отсутствующего ETag кэш не используется. Большие исходники требуют памяти на декодирование, но не сохраняются на диск.

Ссылка с паролем (или ответ 401) даёт PASSWORD_PROTECTED_SOURCE_UNSUPPORTED, отозванная/удалённая — 404, запрещённая — 403, временно недоступная — 503, upstream 429 сохраняется как 429. hide download не обходится альтернативным preview endpoint или сервисной учётной записью: собственному preview pipeline необходим разрешённый GET оригинала.

API и архитектура

Метод Путь Назначение
POST / GET / DELETE /api/v1/admin/session Вход / проверка сессии / выход
GET / POST /api/v1/admin/galleries Список / создание из source URL
PATCH /api/v1/admin/galleries/:id Публикация или отключение
GET /api/v1/galleries/:slug Метаданные и возможности
GET /api/v1/galleries/:slug/assets?limit=24&cursor=UUID Кадры, максимум 100 за страницу
GET /api/v1/galleries/:slug/assets/:assetId/preview?width=640 Превью 320 / 640 / 1280 / 1920
GET /api/v1/galleries/:slug/assets/:assetId/file Поток оригинала, attachment
POST /api/v1/galleries/:slug/download-tickets Ticket для выбранных assetId
GET /api/v1/downloads/:ticket Потоковый ZIP выбранных кадров
GET /api/v1/galleries/:slug/archive Потоковый ZIP всей галереи
GET /api/v1/openapi.json Контракт API
GET /health/live, /health/ready Процесс и БД

Ошибка: {"error":{"code":"NOT_FOUND","message":"…"}}. Ни оригинал, ни превью не перенаправляют браузер к Nextcloud. Range сейчас явно отклоняется с 416; поддержка докачки ещё не реализована. Скачивание использует обычный GET, без браузерного Blob.

apps/api/                    Fastify, auth, композиция, SQLite/Drizzle
apps/web/                    Vue, публичная галерея и панель фотографа
packages/domain/             переносимые типы, порты, политика публикации
packages/api-contract/       Zod DTO/валидация, публичный клиент, OpenAPI-схемы
packages/providers/mock/     независимые локальные JPEG-фикстуры
packages/providers/nextcloud/ WebDAV, политика источника, ограниченный preview pipeline
infra/                       отдельные runtime/tooling Compose и Dockerfile

Домен не импортирует Fastify, ORM или конкретные провайдеры. Вызовы Nextcloud и парсинг ссылок/XML находятся только в адаптере; административная логика пользуется портом. Первичная индексация транзакционна. Новый адаптер реализует тот же контракт, не добавляя публичных endpoints. Решения: ADR 0001, ADR 0002, ADR 0003, ADR 0004. Аудит текущего этапа: AUDIT.md.

Разработка, обновление и тесты

./gallery dev             # localhost:5173, Vite HMR + API watch
./gallery check           # typecheck, ESLint/Prettier, Vitest, build
./gallery test:e2e        # Playwright desktop/mobile viewport
./gallery update          # явное обновление пакетов, infra-пинов (Node/pnpm) и проверки
./gallery format

Dev автоматически использует Origin http://localhost:5173 (или GALLERY_DEV_PORT). Для своего dev origin задайте GALLERY_DEV_ORIGIN. Dev SQLite — data/dev.sqlite, отдельно от production. Нативные зависимости и pnpm store находятся в named volumes; исходники монтируются с хоста. Node.js и pnpm изолированы в образе (точные версии — в infra/docker/Dockerfile и корневом package.json), системный runtime не меняется. Точные manifests и lockfile сохранены; обычные установки — --frozen-lockfile. Для намеренного изменения зависимостей доступен ./gallery install (обновляет lockfile); update обновляет весь набор stable — пакеты, патч-версию образа Node в текущем мажоре и pnpm — с исключениями совместимости TypeScript 6.0.x и типов Node 24.x.

Обычный tooling не использует apt, берёт готовые нативные пакеты. Если для архитектуры нужен компилятор: NODE_TOOLING_IMAGE=node:24-bookworm ./gallery up. Только Playwright target устанавливает системные библиотеки браузера. Журнал сборки — plain; запросы npm/pnpm ограничены таймаутом и одним повтором.

Проверка реального источника: добавьте в .env NEXTCLOUD_TEST_SHARE_URL с публичной ссылкой на отдельную тестовую папку и выполните:

./gallery smoke:nextcloud

При отказе smoke-проверка выводит этап (validate-source, list-assets, preview, original) и последний ответ upstream: метод, HTTP-код, категорию содержимого или безопасный код сетевой ошибки. Например, ENOTFOUND указывает на DNS, TLS-коды — на сертификат, 3xx — на отклонённое перенаправление. При 207/XML ошибка может находиться в DAV-ответе; при 200/image на этапе preview — в обработке изображения. URL, токены, XML, имена файлов и исходные сообщения исключений не выводятся. Для диагностики присылайте эту итоговую JSON-строку, а не содержимое .env.

DAV может вернуть HTTP 207 с несколькими блоками propstat: поддерживаемые свойства имеют статус 200, неподдерживаемые — 404 и пустые значения (например, размер папки). Адаптер проверяет значения только успешного блока, остальные не применяет.

Проверка выполняет только чтение: импорт/PROPFIND, построение одного превью и чтение начала потока оригинала с отменой дальнейшей передачи. Выводит только статус, количество кадров и размеры, без URL/токенов/имён. Это не тест полного скачивания большого файла. После этого создайте галерею через /admin, откройте её в отдельном браузере, проверьте оригинал на смартфоне. Отзовите тестовую ссылку в Nextcloud и повторите запросы старых превью/оригинала: они должны перестать выдаваться. Gallery сама ничего в Nextcloud не отзывает и не изменяет.

Автотесты также раскрывают ZIP независимым yauzl-ридером из зафиксированной Playwright-зависимости и побайтово сравнивают вложенные оригиналы, проверяют ZIP64, вложенность, коллизии имён, истечение tickets, удаление и отзыв доступа. Автотесты не обращаются к реальному Nextcloud. Они покрывают namespace-aware XML, вложенные папки, SSRF/redirect/path traversal, password/revocation/HTTP errors, broken streams, сигнатуры, отзыв перед кэшем, auth/CSRF/brute force, шифрование, транзакцию импорта, чужие assetId, оригиналы и выключенную галерею. Playwright дополнен входом и обработкой ошибки импорта.

Исторический статус первых итераций: Docker daemon и сетевой npm были недоступны агенту. Актуальные результаты последующих контейнерных и браузерных проверок см. в последних разделах аудита. Для доступных проверок восстановлена изолированная копия из локального кэша: Node 24.16.0, Fastify 5.12.7, SQLite driver 13.0.3, Zod 4.6.7; некоторые инструменты отличаются от lockfile (Vitest 3.2.7, TypeScript 5.9.3, Vite 7.3.7, Sharp 0.34.5, Drizzle 0.44.7, плагины Fastify 8.3.0/10.3.0). Результаты и ограничения записаны в AUDIT.md. Они не заменяют ./gallery check на точном lockfile и живой тест папки. Успешный прогон на личной папке Nextcloud в этой среде не подтверждён.

Выбор и скачивание

В /g/demo или существующей галерее отметьте фотографии квадратными кнопками выбора. Сердечко сохраняет отдельный лайк, который видит фотограф. Они доступны и в сетке, и в просмотре; клавиша F меняет отметку текущего кадра. «Только выбранные» включает фильтр, «Выбрать все» получает ID текущего поддерева (в корне — всей галереи), «Снять выделение» очищает его. Сохранение работает только в этом браузере. Если localStorage заблокирован, интерфейс сообщит об этом. Серверная проверка выбора исключает удалённые assetId; при ошибке превью кадр также исключается. Сбой загрузки очередной страницы не стирает выбор ещё не загруженных кадров.

«Скачать оригинал» вызывает собственный file endpoint. «Скачать выбранные» отправляет только assetId, получает ticket и открывает его обычным GET в iframe того же origin. «Скачать всё» сразу открывает archive endpoint. Оригиналы и ZIP не загружаются через fetch/Blob; страница галереи остаётся открытой. Сообщение о подготовке не означает завершение скачивания: прогресс и результат смотрите в загрузках браузера. JSON-ошибки GET показываются на странице, если браузер позволяет прочитать документ iframe; при сетевом обрыве после начала передачи результат показывает менеджер загрузок.

Новые маршруты:

  • POST /api/v1/galleries/:slug/download-tickets — { "assetIds": ["UUID", "UUID"] }, ответ 201 { "downloadUrl": "/api/v1/downloads/…", "expiresAt": "…" }.
  • GET /api/v1/downloads/:ticket — ZIP выбранных кадров.
  • GET /api/v1/galleries/:slug/archive — ZIP всех текущих проиндексированных кадров.

Ticket — случайные 256 бит, в SQLite хранится SHA-256 digest, galleryId и выбранные assetId. Действует 5 минут и допускает повторный GET до истечения для повторной попытки загрузки. Ticket не обходит отключение галереи или отзыв источника: права и наличие файлов проверяются заново. Просроченные записи очищаются при создании новых tickets; одновременно сохраняется не более 1000 записей.

Архив формирует zip-stream с ZIP64 и режимом STORE для уже сжатых изображений. Оригиналы открываются строго последовательно, с backpressure; диск и полная буферизация не используются. Переносимый ProviderAsset.relativePath задаёт папки, при его отсутствии используется filename. Текущие Nextcloud и Mock адаптеры его предоставляют; ключи источника не интерпретируются сервисом ZIP. Опасные пути отклоняются. Несовместимые с Windows символы заменяются, совпадающие имена получают суффиксы (2), (3) перед расширением; совпадения учитываются без регистра и с Unicode NFC.

Ограничения: до 500 выбранных файлов за один ticket; вся галерея — до 10 000 кадров и 100 GiB суммарных исходных данных; операция — до 30 минут, одновременно до двух подготовок/ZIP на процесс. Для маршрутов архивов/tickets — 10 запросов в минуту с IP. Действуют также ограничения адаптера (у Nextcloud оригинал до 200 MiB). Новые файлы появляются после ручной или периодической синхронизации. Удалённые до подготовки файлы пропускаются в «Скачать всё»; старый выбранный asset приводит к отказу, а не молчаливой потере кадра. Если файл исчез или доступ отозван уже после начала передачи, поток обрывается без успешного завершения ZIP. Range/докачка с позиции пока не поддерживаются. При внешнем reverse proxy учитывайте время подготовки/передачи и отключите буферизацию для ZIP; API устанавливает X-Accel-Buffering: no.

Проверка на телефоне: отметьте несколько кадров, закройте просмотр и обновите страницу; проверьте выбор и фильтр. Скачайте JPEG, выбранные ZIP и всё ZIP, откройте архивы в приложении «Файлы» и проверьте имена, папки и снимки. Отдельно нужны реальные Safari/iPhone и Chrome/Android: эмуляция мобильного размера не подтверждает поведение их менеджеров загрузок.

Управление и статистика

Добавлены /admin (обзор), /admin/galleries, /admin/galleries/new, /admin/galleries/:id с вкладками обзор/фотографии/статистика/настройки. Есть поиск, серверная пагинация, сортировка, редактирование и удаление с подтверждением. Удаление касается только записей Gallery.

Над публичной сеткой расположены фильтры папок: «Все фото» выбран по умолчанию и показывает снимки из корня и всех вложенных папок. Фильтр папки включает её поддерево; вложенные названия показаны как «Банкет / Гости». API: /api/v1/galleries/:slug/content?scope=all или ?scope=folder&folderId=UUID, с серверной пагинацией. Выбор для скачивания сохраняется между фильтрами, «Только выбранные» сочетается с выбранной папкой. ZIP папки сохранён; админ-навигация по папкам не менялась. В админке видны серверные лайки, осмысленные просмотры, завершённые передачи файлов/архивов, график по дням и рейтинги с периодом.

Ручной POST /api/v1/admin/galleries/:id/sync возвращает 202; периодическая синхронизация включена по умолчанию с интервалом 300 секунд, настраивается отдельно. Nextcloud fileid позволяет сохранить assetId и статистику при перемещении. Удаления подтверждаются полным успешным обходом и сохраняются как soft-delete. Ошибка не затирает каталог.

Миграция 0003_catalog_analytics.sql дополняет существующую БД и добавляет папки, лайки, просмотры, скачивания и историю sync. Сохраняйте резервную копию; сбрасывать том не нужно. ANALYTICS_RETENTION_DAYS=90 управляет сроком хранения просмотров/скачиваний; в статистике есть ручная очистка. Серверное завершение передачи не доказывает сохранение файла на устройстве.

Все новые маршруты, файлы, определения метрик, ограничения и подробный сценарий проверки: Управление и синхронизация. Архитектура — ADR 0005.

Что остаётся

Range/докачка, защищённые паролем источники, события/дельты синхронизации и проверка на физических Android/iPhone. Полный браузерный и контейнерный прогон в этой среде ограничен; см. аудит.

Обновление кода, библиотек и Node.js/pnpm, команды update:check, backup, deploy и откат: Инструкция обновления.