- убраны клиент бота, транспорт обновлений, отправитель сообщений, сборка входа при старте, секция настроек и зависимость go-telegram-bot-api; из конвейера ушла доставка ответа отправителю — исход виден опросом готовности. Колонки адресата и значение источника остались в схеме: применённые шаги не переписываются - шаг 202608140003 запрещает пустого владельца у аудиозаписи и у файла; существующие строки он не проверяет, и это принято сознательно — искать их надо запросом до выкладки - ревью нашло два пред-существующих дефекта, оба закрыты: пустой второй ответ распознавателя стирал сохранённую расшифровку, а пустая расшифровка перестала быть заметной вместе с убранной доставкой. Попутно поднят golang.org/x/image до v0.45.0 — красный шаг vulns, воспроизводился и на чистом master
9.0 KiB
PocketBase: умолчания, которые ломают штатный сценарий
Наблюдения, снятые по ходу задачи pocketbase-storage уже на своём коде. От
записки разведки отличаются предметом: та мерила, что даёт
панель, эта — что библиотека делает молча, если её не переубедить.
Наблюдения нашлись ревью, а не чтением документации, и все об одном роде промаха: объявление библиотеки выглядит как «ограничения нет» либо «ограничение есть», а значит обратное.
Как снималось
Версия 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.
Чего эта записка не узнала
- Во что обходится потолок в 8 ГиБ на диске. Число выбрано расчётом из шестичасовой записи с запасом на видео, а не замером: настоящего распределения длин у сервиса нет.
- Как ведёт себя укладка файла в несколько гигабайт. Самая длинная проверенная запись — 9,6 МБ (десять минут mp3). Потоковую укладку это подтверждает, предел — нет.
- Поведение под одновременной правкой панели и конвейера в бою. Проверено тестом на одной машине, не живой нагрузкой.
- Сколько строк с пустой связью выдерживает смена признака обязательности. Проверено на одной строке: суть наблюдения — сам факт отсутствия проверки, а не её цена на объёме.