записано решение уйти с PocketBase на SQLite со своим каталогом файлов
- разведка storage-without-pocketbase: шесть ролей библиотеки в этом коде, отпавший довод перевода, шесть модулей достижимы только через неё - ADR-2026-08-22-storage-without-pocketbase заменяет три записи: перевод в PocketBase, очередь коллекцией, файл за защищённым полем - задача storage-without-pocketbase встала первой строкой плана стройки
This commit is contained in:
@@ -22,6 +22,7 @@ SpeechKit, Yandex Object Storage и `ffmpeg`. Мерить нужно то, чт
|
||||
|
||||
| Дата | Запись | О чём |
|
||||
| --- | --- | --- |
|
||||
| 2026-08-22 | [Хранилище: PocketBase против голого SQLite с каталогом файлов](storage-without-pocketbase.md) | Шесть ролей библиотеки в этом коде, отпавший довод перевода, объём кода на её типах, шесть модулей только через неё |
|
||||
| 2026-08-15 | [Раздача приложения: что делают за нас библиотека и сборщик](webapp-serving.md) | Раскодированный путь у маршрутизатора, второй журнал у PocketBase, нулевое время у вшитого файла, зависание установщика без сети |
|
||||
| 2026-08-13 | [Разбор TOML: какое семейство отказов несёт значения из файла](toml-decode-errors.md) | Значения только в `ParseError.Message`, врущее поле `Line`, отказ значением в BurntSushi/toml v1.5.0 |
|
||||
| 2026-08-12 | [PocketBase: умолчания, которые ломают штатный сценарий](pocketbase-defaults.md) | Потолок файла 5 МиБ, тело 32 МиБ, таймаут чтения, суффикс имени, хук правки |
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
# Хранилище: 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` прирастает кодом на чужих типах с каждой задачей.
|
||||
Reference in New Issue
Block a user