Files
transcriber/internal/adapter/repo/pocketbase/migrations.go
T
av 01cc31d45f хранилище, файлы записей и очередь переведены на встроенную PocketBase
- записи, метаданные и файлы съехались под один каталог данных; появилась
  панель владельца, а gin, goqu, goose и требование CGO ушли
- захват задачи стал одним запросом с RETURNING; заведены число попыток,
  состояние dead и нарастающая пауза вместо признака is_error
- имя файла в хранилище задаёт сервис и в журнал не идёт: вместе с
  идентификатором записи оно собирало бы ссылку на скачивание
2026-08-12 08:31:59 +03:00

123 lines
5.4 KiB
Go
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.
package pocketbase
import (
"github.com/pocketbase/pocketbase/core"
"github.com/pocketbase/pocketbase/migrations"
"git.vakhrushev.me/av/transcriber/internal/entity"
)
// Схема заводится версионированными шагами, и применённый шаг не переписывается
// — только новым шагом. Инвариант проекта перенесён дословно: хранилище считает
// применённое по имени файла шага.
//
// Шаг регистрируется в списке приложения при загрузке пакета, а накатывает его
// `apis.Serve` прежде, чем поднять сервер.
func init() {
migrations.Register(up202608110001, down202608110001, "202608110001_init.go")
}
func up202608110001(app core.App) error {
files := core.NewBaseCollection(FilesCollection)
files.Fields.Add(
// Сам файл. Защищённым поле не помечено намеренно: право прочитать
// запись даёт знание её идентификатора, и файл встаёт вровень с опросом
// готовности задачи, а не ниже.
//
// Потолок задан **числом**: нулём библиотека читает не «без предела», а
// своё умолчание в 5 МиБ, и на нём отваливалось бы всё длиннее пяти
// минут. Число выведено из расчётного потолка записи в шесть часов с
// запасом на видео; оно же стоит строкой в docs/database.md.
&core.FileField{Name: "file", MaxSelect: 1, MaxSize: entity.MaxRecordSize},
// Где лежит копия. Поле названо `location`, а не `storage`: последним
// словом зовут само хранилище, и третий смысл развёл бы одно слово по
// разным вещам.
&core.SelectField{
Name: "location",
Values: []string{entity.LocationLocal, entity.LocationS3},
MaxSelect: 1,
Required: true,
},
// Ключ объекта во внешнем хранилище; у местной копии пуст.
&core.TextField{Name: "object_key"},
&core.NumberField{Name: "size", OnlyInt: true},
&core.AutodateField{Name: "created", OnCreate: true},
&core.AutodateField{Name: "updated", OnCreate: true, OnUpdate: true},
)
if err := app.Save(files); err != nil {
return err
}
jobs := core.NewBaseCollection(JobsCollection)
jobs.Fields.Add(
// Перечень состояний закрыт схемой: задача, заведённая в панели руками,
// не должна попасть в выборку с состоянием, которого конвейер не знает.
&core.SelectField{
Name: "state",
Values: []string{
entity.StateCreated,
entity.StateConverted,
entity.StateTranscribe,
entity.StateDone,
entity.StateFailed,
entity.StateDead,
},
MaxSelect: 1,
Required: true,
},
&core.SelectField{
Name: "source",
Values: []string{entity.SourceUnknown, entity.SourceApi, entity.SourceTelegram},
MaxSelect: 1,
Required: true,
},
// Текущий файл задачи: шаг конвейера переставляет ссылку на свой
// результат.
// Обязательна: задача без записи не может пройти ни одного шага, и
// заведённая в панели руками она дошла бы до шага только затем, чтобы
// отказать. Компилятор этого не держит — держит схема.
&core.RelationField{
Name: "file",
CollectionId: files.Id,
MaxSelect: 1,
Required: true,
},
&core.TextField{Name: "error_text"},
&core.TextField{Name: "acquisition_id"},
&core.DateField{Name: "acquire_time"},
&core.DateField{Name: "delay_time"},
// Число попыток: растёт при каждом захвате, обнуляется на шаге,
// завершившемся без отказа.
&core.NumberField{Name: "attempts", OnlyInt: true, Min: ptr(0.0)},
&core.TextField{Name: "recognition_op_id"},
&core.EditorField{Name: "transcription_text"},
&core.NumberField{Name: "tg_chat_id", OnlyInt: true},
&core.NumberField{Name: "tg_reply_message_id", OnlyInt: true},
&core.AutodateField{Name: "created", OnCreate: true},
&core.AutodateField{Name: "updated", OnCreate: true, OnUpdate: true},
)
// Выборка воркера идёт по состоянию, паузе и сроку захвата — индекс по
// состоянию снимает полный перебор, который был у прежней таблицы.
jobs.AddIndex("idx_transcribe_jobs_state", false, "state", "")
return app.Save(jobs)
}
func down202608110001(app core.App) error {
// Порядок обратный порядку заведения: задачи ссылаются на файлы.
for _, name := range []string{JobsCollection, FilesCollection} {
collection, err := app.FindCollectionByNameOrId(name)
if err != nil {
continue
}
if err := app.Delete(collection); err != nil {
return err
}
}
return nil
}
func ptr[T any](v T) *T { return &v }