хранилище, файлы записей и очередь переведены на встроенную PocketBase

- записи, метаданные и файлы съехались под один каталог данных; появилась
  панель владельца, а gin, goqu, goose и требование CGO ушли
- захват задачи стал одним запросом с RETURNING; заведены число попыток,
  состояние dead и нарастающая пауза вместо признака is_error
- имя файла в хранилище задаёт сервис и в журнал не идёт: вместе с
  идентификатором записи оно собирало бы ссылку на скачивание
This commit is contained in:
av
2026-08-12 08:31:59 +03:00
parent 09cedc4e61
commit 01cc31d45f
55 changed files with 5238 additions and 1235 deletions
+33 -26
View File
@@ -85,16 +85,27 @@ Telegram отправителю.
Отсюда возможен выход за пределы каталога хранения — запись файла туда, куда
путь не предполагался.
- **Путь на диске** `filepath.Join(cfg.Storage.Path, fileId + ext)`, где
`fileId` наш UUID, а **`ext` берётся из имени файла отправителя** через
`filepath.Ext`. Расширение в путь попадает без проверки списком; `filepath.Ext`
режет по последней точке и не пропускает разделитель каталогов, но это
единственное, что стоит между входом и именем файла.
- **Путь на диске** выбирает хранилище:
`data/storage/<коллекция>/<запись>/<имя>`. **Имя задаёт сервис**
`<uuid><расширение>`, — а умолчание PocketBase, строящее имя из имени
отправителя, не применяется: имя отправителя в хранилище не попадает.
Расширение берётся из имени отправителя через `filepath.Ext` без проверки
списком; `filepath.Ext` режет по последней точке и не пропускает разделитель
каталогов, но это единственное, что стоит между входом и именем файла.
- **Ключ объекта в Object Storage** — то же имя файла, то есть UUID с
расширением. Бакет один на все записи, префикса по пользователю нет.
- **Каталог** один и плоский: `data/files` целиком, вложенности нет.
- **Идентификатор задачи** — UUID v4. Он же единственное, что защищает
`GET /api/status/:id`.
- **Ссылка на файл** — `/api/files/<коллекция>/<запись>/<имя>`. Поле файла не
помечено защищённым, поэтому ссылка сама по себе и есть право пройти по ней, а
отзыва у неё нет. Отсюда запрет: **имя файла в хранилище в журнал не пишется**
— иначе строка журнала вместе с идентификатором записи собирала бы ссылку
целиком и работала бы бессрочно. В журнал идёт расширение своим полем.
- **Идентификатор задачи** — 15 знаков, выдаёт хранилище. Он же единственное,
что защищает `GET /api/status/:id`.
- **Поверхность самого хранилища.** Вместе с переводом наружу выходят
`/api/collections/...`, `/api/logs`, `/api/backups`, `/api/settings`,
`/api/crons` и панель `/_/`. Правила доступа коллекций оставлены пустыми, то
есть доступны они только владельцу панели; проверено прогоном — записи отдают
`403`, служебные разделы `401`.
Целевой периметр добавляет сюда три вещи, и все три — от новых задач:
@@ -103,15 +114,7 @@ Telegram отправителю.
поиск отдавал бы чужую расшифровку тому, кто угадал или добыл тот же файл, и
заодно сообщал бы, что запись у кого-то уже есть.
- **Файлы фрагментов** (`long-audio-chunking`) ложатся рядом с исходным в тот же
плоский каталог — раскладка `data/files` меняется, и это необратимо.
- **Раскладку выбирает PocketBase** (`pocketbase-storage`), и плоского каталога
не остаётся вовсе: файл ложится в
`pb_data/storage/<коллекция>/<запись>/<имя>_<10 случайных символов>` рядом с
файлом атрибутов. Имя, данное отправителем, в путь при этом попадает — сегодня
от него берётся только расширение. Файл уходит не с диска напрямую, а по ссылке
вида `/api/files/<коллекция>/<запись>/<имя>`; закрытым он становится, только если
поле помечено защищённым, и тогда нужен отдельный файловый токен. Замер —
[research/pocketbase.md](research/pocketbase.md).
плоский каталог — раскладка каталога данных меняется, и это необратимо.
- **Имя отправляемого документа** (`long-text-delivery`) собирается из
идентификатора задачи: имя, данное пользователем, в него не попадает.
@@ -180,10 +183,14 @@ Telegram отправителю.
5. **Статистика потребления** (`usage-accounting`). Текста записей не содержит,
но говорит, кто и когда пользовался сервисом и сколько; страница расхода
открыта только владельцу.
6. **Пароль суперпользователя панели** (`pocketbase-storage`). Открывает все
записи, все файлы и всех пользователей разом, то есть стоит вровень с самым
чувствительным из списка выше. Второй секрет после токенов пользователей,
который лежит **не в конфигурации**: его отпечаток хранит сама база.
6. **Пароль владельца от панели.** Открывает все записи, все файлы и всех
пользователей разом, то есть стоит вровень с самым чувствительным из списка
выше. Второй секрет после токенов пользователей, который лежит **не в
конфигурации**: его отпечаток хранит сама база, а задаёт пароль сам владелец
по приглашению, которое сервис печатает в журнал при первом запуске. У
приглашения тридцать минут жизни, и после того как владелец заведён, оно не
печатается вовсе — иначе строка журнала отдавала бы панель всякому его
читателю навсегда.
Тексты расшифровок в логи не пишутся — логируется длина текста и
идентификаторы. Имя файла, данное отправителем, из журнала приёма убрано
@@ -194,10 +201,10 @@ Telegram отправителю.
инварианта приватности из [../CLAUDE.md](../CLAUDE.md), а не незакрытый остаток.
Расширение берётся из имени отправителя дословно (`filepath.Ext`), поэтому имя
`запись.тайное-слово` отдаёт `тайное-слово`, а `Разговор с Петровым 11.08`
`08`. Оно стоит в собственном имени файла на диске, а путь к файлу логируется.
Читает этот журнал владелец сервиса. Нормализация расширения на диске — отдельная
работа, задачи на неё пока нет: формат имени файла объявлен необратимым и меняется
решением человека.
`08`. В журнал оно идёт собственным полем, а не в составе имени файла: по нему
прослеживается путь записи. Читает этот журнал владелец сервиса. Нормализация
расширения в хранилище — отдельная работа, задачи на неё пока нет: формат имени
файла объявлен необратимым и меняется решением человека.
**Наружу хвост не выходит.** Метки метрик (`file_extension` у
`transcriber_input_file_size_bytes`, `source_format` у
@@ -236,7 +243,7 @@ Telegram отправителю.
замер: `research/` пуст, потолок длины стоит открытым вопросом
`architecture.md`, «Долгие записи», — а квот нет и не будет: решено считать расход и показывать его владельцу, а не отказывать
(цель `usage-stats`). Перебравшего останавливает разговор или отзыв доступа в
Authelia. Рост каталога `data/files` при этом ничем не наблюдается —
Authelia. Рост каталога данных при этом ничем не наблюдается —
открытый вопрос `architecture.md`.
- **Перерасход денег на внешних сервисах.** Распознавание и языковая модель
оплачиваются по факту; потолка на пользователя нет по тому же решению.