Files
transcriber/docs/research/pocketbase-defaults.md
T
av 8f7c3a057a удалён вход Telegram, владелец записи стал обязателен в схеме
- убраны клиент бота, транспорт обновлений, отправитель сообщений, сборка
  входа при старте, секция настроек и зависимость go-telegram-bot-api; из
  конвейера ушла доставка ответа отправителю — исход виден опросом готовности.
  Колонки адресата и значение источника остались в схеме: применённые шаги не
  переписываются
- шаг 202608140003 запрещает пустого владельца у аудиозаписи и у файла;
  существующие строки он не проверяет, и это принято сознательно — искать их
  надо запросом до выкладки
- ревью нашло два пред-существующих дефекта, оба закрыты: пустой второй ответ
  распознавателя стирал сохранённую расшифровку, а пустая расшифровка перестала
  быть заметной вместе с убранной доставкой. Попутно поднят golang.org/x/image
  до v0.45.0 — красный шаг vulns, воспроизводился и на чистом master
2026-08-15 07:24:35 +03:00

132 lines
9.0 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: умолчания, которые ломают штатный сценарий
Наблюдения, снятые по ходу задачи `pocketbase-storage` уже на своём коде. От
[записки разведки](pocketbase.md) отличаются предметом: та мерила, **что даёт
панель**, эта — **что библиотека делает молча**, если её не переубедить.
Наблюдения нашлись ревью, а не чтением документации, и все об одном роде промаха:
объявление библиотеки выглядит как «ограничения нет» либо «ограничение есть», а
значит обратное.
## Как снималось
Версия **0.39.10**, та же, что у первой записки. Прогоны — на пустом каталоге
данных во временном каталоге и на поднятом сервере `127.0.0.1:18099`; боевые
данные и ключи не участвовали. Числа сняты 2026-08-11 и 2026-08-12, последнее
наблюдение — 2026-08-15.
## Нулевой потолок у поля файла значит 5 МиБ, а не «без предела»
`&core.FileField{MaxSize: 0}` читается библиотекой как её собственное умолчание:
```
core/field_file.go:28 const DefaultFileFieldMaxSize int64 = 5 << 20
core/field_file.go:310 if f.MaxSize <= 0 { return DefaultFileFieldMaxSize }
```
Проверено укладкой: файл в 6 МиБ отвергается на сохранении записи —
`the maximum allowed file size is 5242880 bytes`. Прогон через боевой роутер дал
границу дословно:
| тело запроса | ответ |
| --- | --- |
| 4 194 304 байта | `201` |
| 5 238 784 байта | `201` |
| 5 246 976 байт | `500` |
| 34 603 008 байт | `413` |
**Цена для сервиса:** 5 МиБ — это примерно 5,5 минут mp3 при 128 кбит/с. Отвергалась
бы не только длинная запись на приёме: результат конвертации в ogg переваливает
тот же порог примерно на пятой минуте, и **уже принятая** задача исчерпывала бы
попытки на шаге конвертации.
## Тело запроса режется на 32 МиБ раньше обработчика
`apis/base.go:36` вешает `BodyLimit(DefaultMaxBodySize)` на **корневой** роутер,
то есть и на чужие маршруты; `apis/middlewares_body_limit.go:14`
`const DefaultMaxBodySize int64 = 32 << 20`. Ответ `413` уходит мимо обработчика,
без строки в журнале приёма (последняя строка таблицы выше).
Снимается на маршруте: `.Bind(apis.BodyLimit(<своё число>))`.
## Таймаут чтения запроса — пять минут
`apis/serve.go:151` ставит `ReadTimeout: 5 * time.Minute`. Заливка шестичасовой
записи по медленному каналу его переживает: соединение рвётся на середине.
Снимается в хуке `OnServe``se.Server.ReadTimeout = 0`.
## Суффикс к имени файла дописывает конструктор, а не укладка
Первая записка наблюдала `sample.ogg → sample_uztrv6wvz3.ogg` и читала это как
свойство хранилища. Наблюдение верно **только когда имя строит сама библиотека**:
десять случайных знаков добавляет `normalizeName`, вызываемый из
`filesystem.NewFileFrom*`. Имя, положенное в поле `File.Name` после
конструктора, ложится на диск дословно:
```
задано 11111111-2222-3333-4444-555555555555.mp3
на диске 11111111-2222-3333-4444-555555555555.mp3
```
**Цена:** тот, кто задаёт имя сам, не получает от суффикса никакой
неугадываемости — и защищать ссылку на файл ему приходится другим.
## Хук правки записи не различает, кто пишет
`app.OnRecordUpdate(<коллекция>)` — событие **модели**: оно срабатывает на каждом
`app.Save`, включая сохранение из собственного кода. Хук, написанный «для
панели», правил записи конвейера: проверено прогоном — задержка, поставленная
шагом вместе со сменой состояния, обнулялась тем же сохранением.
Различает источник `app.OnRecordUpdateRequest(<коллекция>)`: оно поднимается
только на правку запросом, а код, пишущий мимо HTTP-слоя, под него не попадает.
## Приглашение завести владельца панели живёт полчаса
`apis/installer.go:31``systemSuperuser.NewStaticAuthToken(30 * time.Minute)`;
печатается только пока владельца нет (`needInstallerSuperuser`). Проверено
прогоном: при первом запуске строка со ссылкой в журнале есть, после заведения
владельца при следующем запуске её нет.
## Обязательность связи проверяется у записи, а не у колонки
`Required` у поля связи — правило **проверки записи при сохранении**, а не
ограничение таблицы. Шаг схемы, объявляющий колонку обязательной на базе, где уже
лежат строки с пустым значением, проходит **зелёным** и такие строки оставляет:
```
core/field_relation.go:156 ColumnType отдаёт TEXT DEFAULT '' NOT NULL — от Required не зависит
core/collection_validate.go ни одной проверки, читающей существующие строки
```
Проверено прогоном 2026-08-15 на копии хранилища во временном каталоге: строка с
пустым владельцем заведена до шага, шаг применён тем же кодом, что и на подъёме,
и вывод:
```
STEP 003 (Required=true) поверх ничьей записи: err=<nil>
ПОСЛЕ ШАГА: строка на месте, owner=""
Save остановленной ничьей записи: err=failed to update audio record: owner: cannot be blank.
```
Следствие для нас: оставленная строка становится **незакрываемой**. Захват идёт
сырым запросом мимо проверки и выдаёт её воркеру, а всякое сохранение отказывает —
включая то, которым ставится признак остановки. Искать такие строки надо запросом
до выкладки, а не прогоном самого шага: прогон чистую базу от грязной не
отличает. Цена решения записана в
[adr/ADR-2026-08-15-owner-required-by-schema.md](../adr/ADR-2026-08-15-owner-required-by-schema.md).
## Чего эта записка не узнала
- **Во что обходится потолок в 8 ГиБ на диске.** Число выбрано расчётом из
шестичасовой записи с запасом на видео, а не замером: настоящего распределения
длин у сервиса нет.
- **Как ведёт себя укладка файла в несколько гигабайт.** Самая длинная проверенная
запись — 9,6 МБ (десять минут mp3). Потоковую укладку это подтверждает, предел
— нет.
- **Поведение под одновременной правкой панели и конвейера в бою.** Проверено
тестом на одной машине, не живой нагрузкой.
- **Сколько строк с пустой связью выдерживает смена признака обязательности.**
Проверено на одной строке: суть наблюдения — сам факт отсутствия проверки, а не
её цена на объёме.