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

128 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Хранилище: PocketBase против голого SQLite с каталогом файлов
Отвечает на вопрос владельца от 2026-08-22: не окажется ли голый SQLite с
каталогом аудиозаписей гибче встроенной PocketBase. Записи в каталоге задач у
вопроса нет — он поднят по ходу работы, и разведка идёт от него, а не от
постановки.
Мерилось **дерево репозитория**, а не внешний сервис: вопрос о том, что
библиотека уже держит в этом коде и чем за это плачено. Замеров на живом потоке
нет.
## Как снималось
Дата — 2026-08-22, коммит `a8fb479`, дерево чисто. Всё считано в самом
репозитории, боевые данные не участвовали.
- **Пакеты в сборке:** `go list -deps ./... | wc -l`**584**; из них с
`pocketbase` в пути — `go list -deps ./... | grep -c pocketbase`**40**.
- **Кто тянет тяжёлый модуль:** `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](../adr/ADR-2026-08-11-pocketbase-storage-with-admin-panel.md)
покупал библиотеку за три вещи разом, и отвергал половинчатые пути тем, что
порознь ни одна перевода не оправдывает.
1. **Правка записей в панели** — работает. Единственная роль, которой сегодня
нет замены.
2. **Файлы видны в панели, и копия накрывает их вместе с базой** — работает; тот
же ADR оговаривает, что копии сервер делает своими средствами.
3. **Вход через её провайдер OIDC****отпал**. Цитата, которой ADR отверг
вариант «взять библиотеку только хранилищем»:
> Панель показывает свою коллекцию пользователей и ничего больше. Отсюда
> следствие для целевого входа: **пользователи Authelia в панели не появятся,
> если вход делает само приложение**.
Вход делает само приложение с 2026-08-22
([ADR-2026-08-22-login-by-trusted-header](../adr/ADR-2026-08-22-login-by-trusted-header.md)):
пришедшего называет заголовок прокси, а учётную запись заводит наш
`EnsureUser`. Пользователи в коллекции есть **потому, что их пишет наш код**,
а не провайдер библиотеки. Довод, которым отвергнут отвергнутый вариант,
перестал быть верным — и вместе с ним отпало основание держать вход в
библиотеке.
## Что оплачено и не работает
- **Захват записи идёт сырым запросом мимо записей коллекции**
(`record_repo.go`, `FindAndAcquire`): `app.DB().NewQuery` с `UPDATE … RETURNING`.
На самом горячем месте абстракция не помогает, а её ограничения действуют —
хуки коллекции не срабатывают, время изменения проставляет наш запрос, времена
сравниваются строками побайтово.
- **Налог соседства двух периметров в одном процессе и на одном порту.**
Появился секрет, которого не было, — пароль суперпользователя. Панель
опубликована в интернет, и её барьер обходится подменой знака: `/%5f/`
попадает в ту же группу, что `/_/`, а правило прокси написано на литерал
([../security.md](../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](../adr/ADR-2026-08-22-storage-without-pocketbase.md).
Обстоятельство, которое решило дело: на сервере данных нет и сервис остановлен,
поэтому смена стоит только кода. Дешевле она не станет никогда — каталог
`internal/controller/http` прирастает кодом на чужих типах с каждой задачей.