Files
transcriber/docs/research/storage-without-pocketbase.md
T
av ad5b5e377f записано решение уйти с PocketBase на SQLite со своим каталогом файлов
- разведка storage-without-pocketbase: шесть ролей библиотеки в этом коде, отпавший довод перевода, шесть модулей достижимы только через неё
- ADR-2026-08-22-storage-without-pocketbase заменяет три записи: перевод в PocketBase, очередь коллекцией, файл за защищённым полем
- задача storage-without-pocketbase встала первой строкой плана стройки
2026-08-22 21:00:14 +03:00

11 KiB
Raw Blame History

Хранилище: PocketBase против голого SQLite с каталогом файлов

Отвечает на вопрос владельца от 2026-08-22: не окажется ли голый SQLite с каталогом аудиозаписей гибче встроенной PocketBase. Записи в каталоге задач у вопроса нет — он поднят по ходу работы, и разведка идёт от него, а не от постановки.

Мерилось дерево репозитория, а не внешний сервис: вопрос о том, что библиотека уже держит в этом коде и чем за это плачено. Замеров на живом потоке нет.

Как снималось

Дата — 2026-08-22, коммит a8fb479, дерево чисто. Всё считано в самом репозитории, боевые данные не участвовали.

  • Пакеты в сборке: go list -deps ./... | wc -l584; из них с pocketbase в пути — go list -deps ./... | grep -c pocketbase40.
  • Кто тянет тяжёлый модуль: go mod why -m <модуль> по семи модулям.
  • Объём кода: find <каталог> -name '*.go' [! -name '*_test.go'] | xargs wc -l.
  • Протечка библиотеки за адаптер: grep -l "pocketbase/" internal/controller/http/*.go.
  • Узлы для варианта с монтированием — чтением кэша модулей: router.Router.BuildMux() отдаёт http.Handler (tools/router/router.go:61), а apis.Serve присваивает e.Server.Handler (apis/serve.go:223). Прототипа на этих двух узлах не собирал — проверено только их существование.

PocketBase здесь — фреймворк приложения, а не хранилище

Разрез «хранилище против хранилища» вопроса не покрывает: библиотека держит шесть ролей сразу, и только две из них про хранение.

Роль Где Чем заменяется
SQLite без CGO, пул записи одним соединением pbrepo.New modernc.org/sqlite напрямую, свои WAL, busy_timeout и единственный писатель
Схема и шаги миграций internal/adapter/repo/pocketbase/migrations, 1039 строк свой раннер либо goose, который тут уже был
Файлы записей на диске и отдача /api/files/… по токену file_repo.go, FileTokenPath каталог и свой обработчик отдачи
Маршрутизатор, цепочка слоёв с приоритетами, ограничитель частоты internal/controller/http целиком net/http и счётчик по ключу
Учётная запись значением e.Auth identity.go строка своей таблицы
Панель /_/ покупалась ради неё ничем

Библиотека вышла за адаптер. Из пяти не-тестовых файлов internal/controller/http её импортируют все пять, из шести тестовых — все шесть: 1497 строк кода контроллера и 3075 строк его проверок написаны на *core.RequestEvent. Адаптер хранилища — ещё 1787 строк без шагов схемы.

Три довода прежнего решения: один умер 2026-08-22

ADR-2026-08-11-pocketbase-storage-with-admin-panel покупал библиотеку за три вещи разом, и отвергал половинчатые пути тем, что порознь ни одна перевода не оправдывает.

  1. Правка записей в панели — работает. Единственная роль, которой сегодня нет замены.

  2. Файлы видны в панели, и копия накрывает их вместе с базой — работает; тот же ADR оговаривает, что копии сервер делает своими средствами.

  3. Вход через её провайдер OIDCотпал. Цитата, которой ADR отверг вариант «взять библиотеку только хранилищем»:

    Панель показывает свою коллекцию пользователей и ничего больше. Отсюда следствие для целевого входа: пользователи Authelia в панели не появятся, если вход делает само приложение.

    Вход делает само приложение с 2026-08-22 (ADR-2026-08-22-login-by-trusted-header): пришедшего называет заголовок прокси, а учётную запись заводит наш EnsureUser. Пользователи в коллекции есть потому, что их пишет наш код, а не провайдер библиотеки. Довод, которым отвергнут отвергнутый вариант, перестал быть верным — и вместе с ним отпало основание держать вход в библиотеке.

Что оплачено и не работает

  • Захват записи идёт сырым запросом мимо записей коллекции (record_repo.go, FindAndAcquire): app.DB().NewQuery с UPDATE … RETURNING. На самом горячем месте абстракция не помогает, а её ограничения действуют — хуки коллекции не срабатывают, время изменения проставляет наш запрос, времена сравниваются строками побайтово.
  • Налог соседства двух периметров в одном процессе и на одном порту. Появился секрет, которого не было, — пароль суперпользователя. Панель опубликована в интернет, и её барьер обходится подменой знака: /%5f/ попадает в ту же группу, что /_/, а правило прокси написано на литерал (../security.md); дефект не закрыт. Узнавание нельзя повесить на всю поверхность хранилища, иначе узнанный перепишет себе provider_login и заберёт чужой архив — область слоя сужена, и причина стоит абзацем в identity.go.
  • Пространство /api/ нельзя закрыть на прокси, потому что за файлами ходит туда браузер пользователя. Своя отдача файла снимает это ограничение: закрыть можно всё пространство хранилища разом.

Чем платит уход

  • Панель теряется целиком. Правка записи, возврат остановленной в работу, просмотр очереди фильтром, прослушивание файла — всё это сегодня живёт только там.
  • Пишем сами шаги схемы, отдачу файла, ограничитель частоты и настройку базы. Последняя — не формальность: у modernc.org/sqlite запись идёт единственным соединением, и библиотека держит это за нас двумя пулами.
  • Раскладка каталога данных названа необратимой в ../../CLAUDE.md. На стройке цена нулевая: на сервере пусто, переносить нечего.

Что уходит из сборки, а что остаётся

go mod why -m показал, что шесть модулей достижимы только через PocketBase: disintegration/imaging (через tools/filesystem), domodwyer/mailyak/v3 и golang-jwt/jwt/v5 (через core), ganigeorgiev/fexpr (через apis), spf13/cobra (набор команд), go-sql-driver/mysql (через pocketbase/dbx). modernc.org/sqlite остаётся: он нужен и без библиотеки, и требование обходиться без CGO с ним сохраняется.

Варианты и что выбрано

Вариант Что делает Чем платит
Оставить как есть ноль работы долг растёт с каждой задачей на RequestEvent
Уйти целиком SQLite напрямую, каталог файлов, свои шаги схемы и маршруты панель теряется сразу, работа одной порцией
Уйти в два шага сначала снять с периметра HTTP, панель оставить; потом с хранилища панель жива в промежутке, работа та же, но двумя порциями
Локализовать протечку библиотека не выходит за internal/adapter/repo решение откладывается, панель остаётся

Решением владельца от 2026-08-22 взят уход целиком, и панель не заменяется ничем: пока стройка не кончилась, остановленную запись возвращают в работу запросом к базе. Решение записано в ADR-2026-08-22-storage-without-pocketbase.

Обстоятельство, которое решило дело: на сервере данных нет и сервис остановлен, поэтому смена стоит только кода. Дешевле она не станет никогда — каталог internal/controller/http прирастает кодом на чужих типах с каждой задачей.