Шаги схемы вынесены в свой каталог, и сверка их снова видит
- шаги PocketBase переехали из файла в пакет internal/adapter/repo/pocketbase/migrations, файл на шаг с именем зарегистрированного шага; туда же имена коллекций, срок сессии — в provider.go - ключ migrations в docs/.docs.json наведён на этот каталог: прежнее значение указывало на несуществующий migrations/, и шаг гейта проходил зелёным при всякой правке схемы - app.go подключает пакет шагов явным пустым импортом: пропавшая ссылка на константы унесла бы регистрацию, и хранилище поднялось бы без коллекций
This commit is contained in:
@@ -114,9 +114,9 @@ transcriber/
|
|||||||
## Разработка
|
## Разработка
|
||||||
|
|
||||||
Схему двигают шаги миграций PocketBase на Go —
|
Схему двигают шаги миграций PocketBase на Go —
|
||||||
`internal/adapter/repo/pocketbase`. Непринятые шаги накатываются при подъёме
|
`internal/adapter/repo/pocketbase/migrations`, файл на шаг. Непринятые шаги
|
||||||
хранилища, прежде чем стартуют воркеры и сервер. Применённый шаг не
|
накатываются при подъёме хранилища, прежде чем стартуют воркеры и сервер.
|
||||||
переписывается: изменение — только новым файлом шага.
|
Применённый шаг не переписывается: изменение — только новым файлом шага.
|
||||||
|
|
||||||
Проверки перед коммитом — одной командой:
|
Проверки перед коммитом — одной командой:
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
{
|
{
|
||||||
"canon": 14,
|
"canon": 14,
|
||||||
"migrations": "migrations"
|
"migrations": "internal/adapter/repo/pocketbase/migrations"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -74,7 +74,8 @@
|
|||||||
| Распознаватель | `internal/adapter/recognizer/yandex` | Заливка в Object Storage и отложенное распознавание SpeechKit |
|
| Распознаватель | `internal/adapter/recognizer/yandex` | Заливка в Object Storage и отложенное распознавание SpeechKit |
|
||||||
| Отправитель Telegram | `internal/adapter/telegram` | Отправка текста, деление длинного по словам |
|
| Отправитель Telegram | `internal/adapter/telegram` | Отправка текста, деление длинного по словам |
|
||||||
| Репозитории | `internal/adapter/repo/pocketbase` | Задачи и файлы коллекциями хранилища; захват — сырым запросом |
|
| Репозитории | `internal/adapter/repo/pocketbase` | Задачи и файлы коллекциями хранилища; захват — сырым запросом |
|
||||||
| Панель владельца | там же, `panel.go` | Правка задачи в панели проходит те же правила перехода, что и правка из кода |
|
| Шаги схемы | `internal/adapter/repo/pocketbase/migrations` | Файл на шаг, имя файла — имя шага; там же имена коллекций |
|
||||||
|
| Панель владельца | `internal/adapter/repo/pocketbase`, `panel.go` | Правка задачи в панели проходит те же правила перехода, что и правка из кода |
|
||||||
|
|
||||||
<!-- канон: поведение → openspec/specs/pipeline; ещё НЕ переехало: спека заведена, но это в ней не описано -->
|
<!-- канон: поведение → openspec/specs/pipeline; ещё НЕ переехало: спека заведена, но это в ней не описано -->
|
||||||
|
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ htmx, а здесь решено делать SPA — и перенесённы
|
|||||||
| Форматирование исходников | `.golangci.yml` → `gofmt` |
|
| Форматирование исходников | `.golangci.yml` → `gofmt` |
|
||||||
| Подозрительные конструкции языка | `.golangci.yml` → `govet`, `staticcheck`, `ineffassign`, `unused` |
|
| Подозрительные конструкции языка | `.golangci.yml` → `govet`, `staticcheck`, `ineffassign`, `unused` |
|
||||||
| Секреты в коммите | `lefthook.yml` → `gitleaks git --staged` |
|
| Секреты в коммите | `lefthook.yml` → `gitleaks git --staged` |
|
||||||
| Раскладка документов, битые ссылки, миграция без правки `database.md` | `docs.py check` |
|
| Раскладка документов, битые ссылки, изменённый шаг схемы без правки `database.md` | `docs.py check`; каталог шагов задаёт ключ `migrations` в `docs/.docs.json` |
|
||||||
| Одно число версии Go в `go.mod`, `Dockerfile`, `CLAUDE.md` и `README.md` | `Taskfile.yml` → шаг `go-version` (`scripts/check-go-version.sh`) |
|
| Одно число версии Go в `go.mod`, `Dockerfile`, `CLAUDE.md` и `README.md` | `Taskfile.yml` → шаг `go-version` (`scripts/check-go-version.sh`) |
|
||||||
|
|
||||||
Не названное здесь место механизации означает, что проход по конвенциям будет
|
Не названное здесь место механизации означает, что проход по конвенциям будет
|
||||||
|
|||||||
@@ -8,8 +8,10 @@
|
|||||||
и разбора нет. Правила действуют на новый код; переписывание существующего —
|
и разбора нет. Правила действуют на новый код; переписывание существующего —
|
||||||
отдельная работа, и до неё расхождение читается как долг, а не как нарушение.
|
отдельная работа, и до неё расхождение читается как долг, а не как нарушение.
|
||||||
|
|
||||||
**Механизировано:** ничего. Ни правила линтера, ни теста-сканера под эти пункты
|
**Механизировано:** одно — сверка изменённого шага схемы с
|
||||||
в transcriber нет.
|
[../database.md](../database.md), шаг гейта `docs.py check`
|
||||||
|
([README.md](README.md), «Механизировано»). Под прочие пункты ни правила
|
||||||
|
линтера, ни теста-сканера в transcriber нет.
|
||||||
|
|
||||||
## Первичные ключи — ULID, не автоинкремент
|
## Первичные ключи — ULID, не автоинкремент
|
||||||
|
|
||||||
@@ -55,8 +57,9 @@
|
|||||||
лексикографическую сортировку TEXT, то есть хронологию (`ORDER BY created_at`).
|
лексикографическую сортировку TEXT, то есть хронологию (`ORDER BY created_at`).
|
||||||
Единая точка генерации — приложение, а не умолчание в схеме: так забытая
|
Единая точка генерации — приложение, а не умолчание в схеме: так забытая
|
||||||
вставка падает громко. Измерение длительности — не метка времени.
|
вставка падает громко. Измерение длительности — не метка времени.
|
||||||
- Миграции — шаги PocketBase на Go (`internal/adapter/repo/pocketbase`):
|
- Миграции — шаги PocketBase на Go
|
||||||
коллекции и их поля заводятся кодом. При изменении структуры обновляем схему
|
(`internal/adapter/repo/pocketbase/migrations`, файл на шаг): коллекции и их
|
||||||
|
поля заводятся кодом. При изменении структуры обновляем схему
|
||||||
[../database.md](../database.md) тем же изменением.
|
[../database.md](../database.md) тем же изменением.
|
||||||
- Время в **сыром запросе** кладётся и сравнивается тем же видом, каким
|
- Время в **сыром запросе** кладётся и сравнивается тем же видом, каким
|
||||||
хранилище пишет свои `created`/`updated`. Сравнение строк побайтово, и
|
хранилище пишет свои `created`/`updated`. Сравнение строк побайтово, и
|
||||||
|
|||||||
+11
-5
@@ -8,11 +8,17 @@
|
|||||||
CGO сборке не нужен.
|
CGO сборке не нужен.
|
||||||
|
|
||||||
Схему двигают **шаги миграций PocketBase** на Go, каталог
|
Схему двигают **шаги миграций PocketBase** на Go, каталог
|
||||||
`internal/adapter/repo/pocketbase`, файл шага — `migrations.go`. Шаг
|
`internal/adapter/repo/pocketbase/migrations`, файл на шаг и имя файла — имя
|
||||||
регистрируется при загрузке пакета, а накатывается при подъёме хранилища
|
шага. Шаг регистрируется при загрузке пакета, а накатывается при подъёме
|
||||||
(`pocketbase.New`), прежде чем стартуют воркеры и сервер. Применённый шаг не
|
хранилища (`pocketbase.New`), прежде чем стартуют воркеры и сервер. Применённый
|
||||||
переписывается — изменение только новым шагом: применённое хранилище считает по
|
шаг не переписывается — изменение только новым шагом: применённое хранилище
|
||||||
имени файла.
|
считает по имени шага.
|
||||||
|
|
||||||
|
Каталог у шагов свой, а не файл внутри пакета репозитория, и причина внешняя:
|
||||||
|
шаг гейта сверяет изменённые шаги схемы с правкой этого документа по **префиксу
|
||||||
|
пути** (`docs/.docs.json`, ключ `migrations`), а префикс наводится только на
|
||||||
|
каталог. Имена коллекций живут там же, рядом с шагом, который их заводит; пакет
|
||||||
|
репозитория берёт их оттуда.
|
||||||
|
|
||||||
**Идентификаторы** записей выдаёт хранилище — 15 знаков собственного алфавита.
|
**Идентификаторы** записей выдаёт хранилище — 15 знаков собственного алфавита.
|
||||||
Свои UUID остались только в **именах файлов**: имя, под которым запись ложится в
|
Свои UUID остались только в **именах файлов**: имя, под которым запись ложится в
|
||||||
|
|||||||
+2
-1
@@ -38,7 +38,8 @@
|
|||||||
- вырожденный ответ (пустой, усечённый, без ожидаемого поля) не превращает в
|
- вырожденный ответ (пустой, усечённый, без ожидаемого поля) не превращает в
|
||||||
успех молча.
|
успех молча.
|
||||||
|
|
||||||
**Репозиторий хранилища** (`internal/adapter/repo/pocketbase`):
|
**Репозиторий хранилища** (`internal/adapter/repo/pocketbase`; шаги схемы —
|
||||||
|
подпакетом `migrations`):
|
||||||
|
|
||||||
- список колонок совпадает во всех четырёх местах — `applyToRecord`,
|
- список колонок совпадает во всех четырёх местах — `applyToRecord`,
|
||||||
`recordToJob`, `acquireColumns`, `acquiredRow` — и в шаге схемы (инвариант
|
`recordToJob`, `acquireColumns`, `acquiredRow` — и в шаге схемы (инвариант
|
||||||
|
|||||||
@@ -10,13 +10,15 @@ import (
|
|||||||
|
|
||||||
pb "github.com/pocketbase/pocketbase"
|
pb "github.com/pocketbase/pocketbase"
|
||||||
"github.com/pocketbase/pocketbase/core"
|
"github.com/pocketbase/pocketbase/core"
|
||||||
)
|
|
||||||
|
|
||||||
// Имена коллекций. Они же — часть пути к файлу в раскладке хранилища и часть
|
// Шаги схемы регистрируются загрузкой своего пакета, а накатывает их
|
||||||
// адреса ссылки на него, поэтому меняются только новым шагом схемы.
|
// `RunAllMigrations` ниже. Импорт здесь пустой и явный, хотя соседние файлы
|
||||||
const (
|
// пакета и так берут оттуда имена коллекций: день, когда имена перестанут
|
||||||
FilesCollection = "files"
|
// читаться отсюда, унёс бы вместе с последней ссылкой и регистрацию — список
|
||||||
JobsCollection = "transcribe_jobs"
|
// шагов остался бы пустым, `RunAllMigrations` вернул бы `nil`, и приложение
|
||||||
|
// поднялось бы здоровым, но без коллекций. Отказ вылез бы не на старте, а на
|
||||||
|
// первом приёме записи.
|
||||||
|
_ "git.vakhrushev.me/av/transcriber/internal/adapter/repo/pocketbase/migrations"
|
||||||
)
|
)
|
||||||
|
|
||||||
// New создаёт приложение хранилища на заданном каталоге данных и приводит его в
|
// New создаёт приложение хранилища на заданном каталоге данных и приводит его в
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ import (
|
|||||||
|
|
||||||
"git.vakhrushev.me/av/transcriber/internal/contract"
|
"git.vakhrushev.me/av/transcriber/internal/contract"
|
||||||
"git.vakhrushev.me/av/transcriber/internal/entity"
|
"git.vakhrushev.me/av/transcriber/internal/entity"
|
||||||
|
|
||||||
|
"git.vakhrushev.me/av/transcriber/internal/adapter/repo/pocketbase/migrations"
|
||||||
)
|
)
|
||||||
|
|
||||||
// workFile — рабочая копия файла на диске. Живёт во временном каталоге
|
// workFile — рабочая копия файла на диске. Живёт во временном каталоге
|
||||||
@@ -82,7 +84,7 @@ func (repo *FileRepository) Stage(ext string, content io.Reader) (contract.WorkF
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (repo *FileRepository) Localize(fileID string) (contract.WorkFile, error) {
|
func (repo *FileRepository) Localize(fileID string) (contract.WorkFile, error) {
|
||||||
record, err := repo.app.FindRecordById(FilesCollection, fileID)
|
record, err := repo.app.FindRecordById(migrations.FilesCollection, fileID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to find file %s: %w", fileID, err)
|
return nil, fmt.Errorf("failed to find file %s: %w", fileID, err)
|
||||||
}
|
}
|
||||||
@@ -115,7 +117,7 @@ func (repo *FileRepository) Localize(fileID string) (contract.WorkFile, error) {
|
|||||||
// хранилище не попадает — путь к файлу читается в журнале, и инвариант
|
// хранилище не попадает — путь к файлу читается в журнале, и инвариант
|
||||||
// приватности этого не допускает. Свой суффикс хранилище допишет само.
|
// приватности этого не допускает. Свой суффикс хранилище допишет само.
|
||||||
func (repo *FileRepository) CreateLocal(name string, work contract.WorkFile) (*entity.File, error) {
|
func (repo *FileRepository) CreateLocal(name string, work contract.WorkFile) (*entity.File, error) {
|
||||||
collection, err := findCollection(repo.app, FilesCollection)
|
collection, err := findCollection(repo.app, migrations.FilesCollection)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -142,7 +144,7 @@ func (repo *FileRepository) CreateLocal(name string, work contract.WorkFile) (*e
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (repo *FileRepository) CreateRemote(objectKey string, size int64) (*entity.File, error) {
|
func (repo *FileRepository) CreateRemote(objectKey string, size int64) (*entity.File, error) {
|
||||||
collection, err := findCollection(repo.app, FilesCollection)
|
collection, err := findCollection(repo.app, migrations.FilesCollection)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -160,7 +162,7 @@ func (repo *FileRepository) CreateRemote(objectKey string, size int64) (*entity.
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (repo *FileRepository) GetByID(id string) (*entity.File, error) {
|
func (repo *FileRepository) GetByID(id string) (*entity.File, error) {
|
||||||
record, err := repo.app.FindRecordById(FilesCollection, id)
|
record, err := repo.app.FindRecordById(migrations.FilesCollection, id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to get file: %w", err)
|
return nil, fmt.Errorf("failed to get file: %w", err)
|
||||||
}
|
}
|
||||||
@@ -168,7 +170,7 @@ func (repo *FileRepository) GetByID(id string) (*entity.File, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (repo *FileRepository) Open(fileID string) (io.ReadCloser, error) {
|
func (repo *FileRepository) Open(fileID string) (io.ReadCloser, error) {
|
||||||
record, err := repo.app.FindRecordById(FilesCollection, fileID)
|
record, err := repo.app.FindRecordById(migrations.FilesCollection, fileID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to find file %s: %w", fileID, err)
|
return nil, fmt.Errorf("failed to find file %s: %w", fileID, err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,109 @@
|
|||||||
|
package migrations
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/pocketbase/pocketbase/core"
|
||||||
|
|
||||||
|
"git.vakhrushev.me/av/transcriber/internal/entity"
|
||||||
|
)
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
+1
-129
@@ -1,138 +1,12 @@
|
|||||||
package pocketbase
|
package migrations
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/pocketbase/pocketbase/core"
|
"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")
|
|
||||||
migrations.Register(up202608120001, down202608120001, "202608120001_oidc_login.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
|
|
||||||
}
|
|
||||||
|
|
||||||
// SessionDuration — сколько живёт сессия вошедшего, семь суток. Число выбрано
|
|
||||||
// решением владельца от 2026-08-12; умолчание библиотеки в пять суток не
|
|
||||||
// применяется, потому что оно никем не выбрано.
|
|
||||||
//
|
|
||||||
// Применяется оно не шагом схемы, а при каждом подъёме — вместе с настройками
|
|
||||||
// провайдера. Причина та же: применённый шаг не переписывается, и число,
|
|
||||||
// положенное туда, разошлось бы со сроком жизни куки при первой же правке —
|
|
||||||
// браузер получил бы новый срок, а хранилище продолжило выдавать прежний.
|
|
||||||
const SessionDuration = 7 * 24 * 60 * 60
|
|
||||||
|
|
||||||
// defaultAuthTokenDuration — умолчание библиотеки, к которому возвращает откат.
|
// defaultAuthTokenDuration — умолчание библиотеки, к которому возвращает откат.
|
||||||
const defaultAuthTokenDuration = 1209600
|
const defaultAuthTokenDuration = 1209600
|
||||||
|
|
||||||
@@ -248,5 +122,3 @@ func down202608120001(app core.App) error {
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func ptr[T any](v T) *T { return &v }
|
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
// Package migrations — шаги схемы хранилища и имена коллекций, которые они
|
||||||
|
// заводят.
|
||||||
|
//
|
||||||
|
// Схема заводится версионированными шагами, и применённый шаг не переписывается
|
||||||
|
// — только новым шагом. Инвариант проекта перенесён дословно: хранилище считает
|
||||||
|
// применённое по **имени шага**, а не по пути файла, поэтому имена в
|
||||||
|
// `Register` ниже не переносятся и не переименовываются, даже если файл переехал.
|
||||||
|
//
|
||||||
|
// Шаги лежат своим каталогом, а не файлом внутри пакета репозитория, и причина
|
||||||
|
// внешняя: сверка документов ловит изменённый шаг схемы при нетронутом
|
||||||
|
// `docs/database.md` по префиксу пути (`docs/.docs.json`, ключ `migrations`), а
|
||||||
|
// префикс наводится только на каталог. Пока шаги лежали файлом, наводить его
|
||||||
|
// было не на что, и проверка молчала на всякой правке схемы.
|
||||||
|
package migrations
|
||||||
|
|
||||||
|
import (
|
||||||
|
pbmigrations "github.com/pocketbase/pocketbase/migrations"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Имена коллекций живут здесь, рядом с шагом, который их заводит. Они же — часть
|
||||||
|
// пути к файлу в раскладке хранилища и часть адреса ссылки на него, поэтому
|
||||||
|
// меняются только новым шагом схемы.
|
||||||
|
const (
|
||||||
|
FilesCollection = "files"
|
||||||
|
JobsCollection = "transcribe_jobs"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Шаг регистрируется в списке приложения при загрузке пакета, а накатывает его
|
||||||
|
// `apis.Serve` прежде, чем поднять сервер.
|
||||||
|
func init() {
|
||||||
|
pbmigrations.Register(up202608110001, down202608110001, "202608110001_init.go")
|
||||||
|
pbmigrations.Register(up202608120001, down202608120001, "202608120001_oidc_login.go")
|
||||||
|
}
|
||||||
|
|
||||||
|
func ptr[T any](v T) *T { return &v }
|
||||||
@@ -2,6 +2,8 @@ package pocketbase
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/pocketbase/pocketbase/core"
|
"github.com/pocketbase/pocketbase/core"
|
||||||
|
|
||||||
|
"git.vakhrushev.me/av/transcriber/internal/adapter/repo/pocketbase/migrations"
|
||||||
)
|
)
|
||||||
|
|
||||||
// BindPanelRules подчиняет правку задачи в панели тем же правилам перехода, что
|
// BindPanelRules подчиняет правку задачи в панели тем же правилам перехода, что
|
||||||
@@ -20,7 +22,7 @@ import (
|
|||||||
// стиралась бы тем же сохранением, а число попыток мёртвой задачи — которое
|
// стиралась бы тем же сохранением, а число попыток мёртвой задачи — которое
|
||||||
// переход хранит намеренно — приходило бы владельцу нулём.
|
// переход хранит намеренно — приходило бы владельцу нулём.
|
||||||
func BindPanelRules(app core.App) {
|
func BindPanelRules(app core.App) {
|
||||||
app.OnRecordUpdateRequest(JobsCollection).BindFunc(func(e *core.RecordRequestEvent) error {
|
app.OnRecordUpdateRequest(migrations.JobsCollection).BindFunc(func(e *core.RecordRequestEvent) error {
|
||||||
original := e.Record.Original()
|
original := e.Record.Original()
|
||||||
if original == nil || original.GetString("state") == e.Record.GetString("state") {
|
if original == nil || original.GetString("state") == e.Record.GetString("state") {
|
||||||
return e.Next()
|
return e.Next()
|
||||||
|
|||||||
@@ -10,6 +10,17 @@ import (
|
|||||||
// как обобщённый OIDC и по нему же ищет настройку при обмене кода.
|
// как обобщённый OIDC и по нему же ищет настройку при обмене кода.
|
||||||
const ProviderName = "oidc"
|
const ProviderName = "oidc"
|
||||||
|
|
||||||
|
// SessionDuration — сколько живёт сессия вошедшего, семь суток. Число выбрано
|
||||||
|
// решением владельца от 2026-08-12; умолчание библиотеки в пять суток не
|
||||||
|
// применяется, потому что оно никем не выбрано.
|
||||||
|
//
|
||||||
|
// Применяется оно не шагом схемы, а при каждом подъёме — вместе с настройками
|
||||||
|
// провайдера, и потому живёт здесь, а не в каталоге шагов. Причина та же:
|
||||||
|
// применённый шаг не переписывается, и число, положенное туда, разошлось бы со
|
||||||
|
// сроком жизни куки при первой же правке — браузер получил бы новый срок, а
|
||||||
|
// хранилище продолжило выдавать прежний.
|
||||||
|
const SessionDuration = 7 * 24 * 60 * 60
|
||||||
|
|
||||||
// ProviderSettings — то, что приезжает из конфига и приводится к настройкам
|
// ProviderSettings — то, что приезжает из конфига и приводится к настройкам
|
||||||
// коллекции.
|
// коллекции.
|
||||||
type ProviderSettings struct {
|
type ProviderSettings struct {
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ import (
|
|||||||
|
|
||||||
"git.vakhrushev.me/av/transcriber/internal/contract"
|
"git.vakhrushev.me/av/transcriber/internal/contract"
|
||||||
"git.vakhrushev.me/av/transcriber/internal/entity"
|
"git.vakhrushev.me/av/transcriber/internal/entity"
|
||||||
|
|
||||||
|
"git.vakhrushev.me/av/transcriber/internal/adapter/repo/pocketbase/migrations"
|
||||||
)
|
)
|
||||||
|
|
||||||
type TranscriptJobRepository struct {
|
type TranscriptJobRepository struct {
|
||||||
@@ -23,7 +25,7 @@ func NewTranscriptJobRepository(app core.App) *TranscriptJobRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (repo *TranscriptJobRepository) Create(job *entity.TranscribeJob) error {
|
func (repo *TranscriptJobRepository) Create(job *entity.TranscribeJob) error {
|
||||||
collection, err := findCollection(repo.app, JobsCollection)
|
collection, err := findCollection(repo.app, migrations.JobsCollection)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -50,7 +52,7 @@ func (repo *TranscriptJobRepository) Create(job *entity.TranscribeJob) error {
|
|||||||
// LostAcquisitionError и результата не пишет.
|
// LostAcquisitionError и результата не пишет.
|
||||||
func (repo *TranscriptJobRepository) Save(job *entity.TranscribeJob, holder string) error {
|
func (repo *TranscriptJobRepository) Save(job *entity.TranscribeJob, holder string) error {
|
||||||
err := repo.app.RunInTransaction(func(txApp core.App) error {
|
err := repo.app.RunInTransaction(func(txApp core.App) error {
|
||||||
record, err := txApp.FindRecordById(JobsCollection, job.Id)
|
record, err := txApp.FindRecordById(migrations.JobsCollection, job.Id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to find transcribe job: %w", err)
|
return fmt.Errorf("failed to find transcribe job: %w", err)
|
||||||
}
|
}
|
||||||
@@ -77,7 +79,7 @@ func (repo *TranscriptJobRepository) Save(job *entity.TranscribeJob, holder stri
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (repo *TranscriptJobRepository) GetByID(id string) (*entity.TranscribeJob, error) {
|
func (repo *TranscriptJobRepository) GetByID(id string) (*entity.TranscribeJob, error) {
|
||||||
record, err := repo.app.FindRecordById(JobsCollection, id)
|
record, err := repo.app.FindRecordById(migrations.JobsCollection, id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to get transcribe job: %w", err)
|
return nil, fmt.Errorf("failed to get transcribe job: %w", err)
|
||||||
}
|
}
|
||||||
@@ -108,13 +110,13 @@ func (repo *TranscriptJobRepository) FindAndAcquire(state, acquisitionId string,
|
|||||||
now := types.NowDateTime()
|
now := types.NowDateTime()
|
||||||
|
|
||||||
query := repo.app.DB().NewQuery(`
|
query := repo.app.DB().NewQuery(`
|
||||||
UPDATE {{` + JobsCollection + `}}
|
UPDATE {{` + migrations.JobsCollection + `}}
|
||||||
SET acquisition_id = {:acquisition_id},
|
SET acquisition_id = {:acquisition_id},
|
||||||
acquire_time = {:now},
|
acquire_time = {:now},
|
||||||
attempts = attempts + 1,
|
attempts = attempts + 1,
|
||||||
updated = {:now}
|
updated = {:now}
|
||||||
WHERE id = (
|
WHERE id = (
|
||||||
SELECT id FROM {{` + JobsCollection + `}}
|
SELECT id FROM {{` + migrations.JobsCollection + `}}
|
||||||
WHERE state = {:state}
|
WHERE state = {:state}
|
||||||
AND (delay_time = '' OR delay_time IS NULL OR delay_time < {:now})
|
AND (delay_time = '' OR delay_time IS NULL OR delay_time < {:now})
|
||||||
AND (acquisition_id = '' OR acquisition_id IS NULL OR acquire_time < {:rotting})
|
AND (acquisition_id = '' OR acquisition_id IS NULL OR acquire_time < {:rotting})
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ import (
|
|||||||
|
|
||||||
"git.vakhrushev.me/av/transcriber/internal/contract"
|
"git.vakhrushev.me/av/transcriber/internal/contract"
|
||||||
"git.vakhrushev.me/av/transcriber/internal/entity"
|
"git.vakhrushev.me/av/transcriber/internal/entity"
|
||||||
|
|
||||||
|
"git.vakhrushev.me/av/transcriber/internal/adapter/repo/pocketbase/migrations"
|
||||||
)
|
)
|
||||||
|
|
||||||
// newTestApp поднимает хранилище на пустом каталоге и накатывает схему — тем же
|
// newTestApp поднимает хранилище на пустом каталоге и накатывает схему — тем же
|
||||||
@@ -136,7 +138,7 @@ func TestFindAndAcquire_RottenAcquisitionIsHandedOutAgain(t *testing.T) {
|
|||||||
|
|
||||||
// Задним числом — записью коллекции, то есть тем же слоем, который пишет
|
// Задним числом — записью коллекции, то есть тем же слоем, который пишет
|
||||||
// собственные времена хранилища.
|
// собственные времена хранилища.
|
||||||
record, err := app.FindRecordById(JobsCollection, job.Id)
|
record, err := app.FindRecordById(migrations.JobsCollection, job.Id)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
record.Set("acquire_time", types.NowDateTime().Add(-2*time.Hour))
|
record.Set("acquire_time", types.NowDateTime().Add(-2*time.Hour))
|
||||||
require.NoError(t, app.Save(record))
|
require.NoError(t, app.Save(record))
|
||||||
@@ -232,7 +234,7 @@ func TestSave_RefusesWriteFromLostAcquisition(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// Задача досталась другому, пока шаг работал.
|
// Задача досталась другому, пока шаг работал.
|
||||||
record, err := app.FindRecordById(JobsCollection, mine.Id)
|
record, err := app.FindRecordById(migrations.JobsCollection, mine.Id)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
record.Set("acquisition_id", "someone-else")
|
record.Set("acquisition_id", "someone-else")
|
||||||
require.NoError(t, app.Save(record))
|
require.NoError(t, app.Save(record))
|
||||||
@@ -279,7 +281,7 @@ func TestPanelRules_StateChangeByRequestClearsAcquisition(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.NotNil(t, acquired.AcquisitionID)
|
require.NotNil(t, acquired.AcquisitionID)
|
||||||
|
|
||||||
record, err := app.FindRecordById(JobsCollection, job.Id)
|
record, err := app.FindRecordById(migrations.JobsCollection, job.Id)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
record.Set("attempts", 5)
|
record.Set("attempts", 5)
|
||||||
record.Set("state", entity.StateDead)
|
record.Set("state", entity.StateDead)
|
||||||
@@ -360,7 +362,7 @@ func patchRecord(t *testing.T, app core.App, recordID, body string) {
|
|||||||
|
|
||||||
req := httptest.NewRequest(
|
req := httptest.NewRequest(
|
||||||
http.MethodPatch,
|
http.MethodPatch,
|
||||||
"/api/collections/"+JobsCollection+"/records/"+recordID,
|
"/api/collections/"+migrations.JobsCollection+"/records/"+recordID,
|
||||||
strings.NewReader(body),
|
strings.NewReader(body),
|
||||||
)
|
)
|
||||||
req.Header.Set("Content-Type", "application/json")
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import (
|
|||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
pbrepo "git.vakhrushev.me/av/transcriber/internal/adapter/repo/pocketbase"
|
pbrepo "git.vakhrushev.me/av/transcriber/internal/adapter/repo/pocketbase"
|
||||||
|
"git.vakhrushev.me/av/transcriber/internal/adapter/repo/pocketbase/migrations"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Проверки этого файла судят допуск: кого пускают к приёму и опросу, чем
|
// Проверки этого файла судят допуск: кого пускают к приёму и опросу, чем
|
||||||
@@ -33,11 +34,11 @@ func TestApiRequiresSession(t *testing.T) {
|
|||||||
|
|
||||||
// Ни файла, ни задачи: отказ наступает раньше, чем запись попадает в
|
// Ни файла, ни задачи: отказ наступает раньше, чем запись попадает в
|
||||||
// хранилище.
|
// хранилище.
|
||||||
files, err := env.app.FindAllRecords(pbrepo.FilesCollection)
|
files, err := env.app.FindAllRecords(migrations.FilesCollection)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Empty(t, files)
|
assert.Empty(t, files)
|
||||||
|
|
||||||
jobs, err := env.app.FindAllRecords(pbrepo.JobsCollection)
|
jobs, err := env.app.FindAllRecords(migrations.JobsCollection)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Empty(t, jobs)
|
assert.Empty(t, jobs)
|
||||||
})
|
})
|
||||||
@@ -63,7 +64,7 @@ func TestUnknownJobIsIndistinguishableWithoutSession(t *testing.T) {
|
|||||||
env.serve(created, createMultipartRequest(t, "test.mp3", []byte("audio")))
|
env.serve(created, createMultipartRequest(t, "test.mp3", []byte("audio")))
|
||||||
require.Equal(t, http.StatusCreated, created.Code)
|
require.Equal(t, http.StatusCreated, created.Code)
|
||||||
|
|
||||||
jobs, err := env.app.FindAllRecords(pbrepo.JobsCollection)
|
jobs, err := env.app.FindAllRecords(migrations.JobsCollection)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Len(t, jobs, 1)
|
require.Len(t, jobs, 1)
|
||||||
|
|
||||||
@@ -459,7 +460,7 @@ func TestProviderSecretRotationReachesStorage(t *testing.T) {
|
|||||||
func TestRecordFileIsProtected(t *testing.T) {
|
func TestRecordFileIsProtected(t *testing.T) {
|
||||||
app := newTestStorage(t)
|
app := newTestStorage(t)
|
||||||
|
|
||||||
files, err := app.FindCollectionByNameOrId(pbrepo.FilesCollection)
|
files, err := app.FindCollectionByNameOrId(migrations.FilesCollection)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
field, ok := files.Fields.GetByName("file").(*core.FileField)
|
field, ok := files.Fields.GetByName("file").(*core.FileField)
|
||||||
@@ -478,14 +479,14 @@ func TestRecordFileNeedsSession(t *testing.T) {
|
|||||||
env.serve(created, createMultipartRequest(t, "test.mp3", []byte("audio content")))
|
env.serve(created, createMultipartRequest(t, "test.mp3", []byte("audio content")))
|
||||||
require.Equal(t, http.StatusCreated, created.Code)
|
require.Equal(t, http.StatusCreated, created.Code)
|
||||||
|
|
||||||
files, err := env.app.FindAllRecords(pbrepo.FilesCollection)
|
files, err := env.app.FindAllRecords(migrations.FilesCollection)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Len(t, files, 1)
|
require.Len(t, files, 1)
|
||||||
|
|
||||||
names := files[0].GetStringSlice("file")
|
names := files[0].GetStringSlice("file")
|
||||||
require.Len(t, names, 1)
|
require.Len(t, names, 1)
|
||||||
|
|
||||||
link := "/api/files/" + pbrepo.FilesCollection + "/" + files[0].Id + "/" + names[0]
|
link := "/api/files/" + migrations.FilesCollection + "/" + files[0].Id + "/" + names[0]
|
||||||
|
|
||||||
anonymous := httptest.NewRecorder()
|
anonymous := httptest.NewRecorder()
|
||||||
env.mux.ServeHTTP(anonymous, httptest.NewRequest(http.MethodGet, link, nil))
|
env.mux.ServeHTTP(anonymous, httptest.NewRequest(http.MethodGet, link, nil))
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import (
|
|||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
pbrepo "git.vakhrushev.me/av/transcriber/internal/adapter/repo/pocketbase"
|
pbrepo "git.vakhrushev.me/av/transcriber/internal/adapter/repo/pocketbase"
|
||||||
|
"git.vakhrushev.me/av/transcriber/internal/adapter/repo/pocketbase/migrations"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Проверки этого файла проходят вход целиком — от увода к провайдеру до куки
|
// Проверки этого файла проходят вход целиком — от увода к провайдеру до куки
|
||||||
@@ -315,13 +316,13 @@ func TestRecordFileNeedsSessionAndToken(t *testing.T) {
|
|||||||
env.serve(created, createMultipartRequest(t, "test.mp3", []byte("audio content")))
|
env.serve(created, createMultipartRequest(t, "test.mp3", []byte("audio content")))
|
||||||
require.Equal(t, http.StatusCreated, created.Code)
|
require.Equal(t, http.StatusCreated, created.Code)
|
||||||
|
|
||||||
files, err := env.app.FindAllRecords(pbrepo.FilesCollection)
|
files, err := env.app.FindAllRecords(migrations.FilesCollection)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Len(t, files, 1)
|
require.Len(t, files, 1)
|
||||||
|
|
||||||
names := files[0].GetStringSlice("file")
|
names := files[0].GetStringSlice("file")
|
||||||
require.Len(t, names, 1)
|
require.Len(t, names, 1)
|
||||||
link := "/api/files/" + pbrepo.FilesCollection + "/" + files[0].Id + "/" + names[0]
|
link := "/api/files/" + migrations.FilesCollection + "/" + files[0].Id + "/" + names[0]
|
||||||
|
|
||||||
// Аноним не проходит.
|
// Аноним не проходит.
|
||||||
anonymous := httptest.NewRecorder()
|
anonymous := httptest.NewRecorder()
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import (
|
|||||||
|
|
||||||
"git.vakhrushev.me/av/transcriber/internal/adapter/recognizer"
|
"git.vakhrushev.me/av/transcriber/internal/adapter/recognizer"
|
||||||
pbrepo "git.vakhrushev.me/av/transcriber/internal/adapter/repo/pocketbase"
|
pbrepo "git.vakhrushev.me/av/transcriber/internal/adapter/repo/pocketbase"
|
||||||
|
"git.vakhrushev.me/av/transcriber/internal/adapter/repo/pocketbase/migrations"
|
||||||
"git.vakhrushev.me/av/transcriber/internal/contract"
|
"git.vakhrushev.me/av/transcriber/internal/contract"
|
||||||
"git.vakhrushev.me/av/transcriber/internal/entity"
|
"git.vakhrushev.me/av/transcriber/internal/entity"
|
||||||
"git.vakhrushev.me/av/transcriber/internal/service"
|
"git.vakhrushev.me/av/transcriber/internal/service"
|
||||||
@@ -232,7 +233,7 @@ func createMultipartRequestWithField(t *testing.T, field, fileName string, conte
|
|||||||
|
|
||||||
// storedFileNames отдаёт имена, под которыми файлы легли в хранилище.
|
// storedFileNames отдаёт имена, под которыми файлы легли в хранилище.
|
||||||
func storedFileNames(t *testing.T, env *testEnv) []string {
|
func storedFileNames(t *testing.T, env *testEnv) []string {
|
||||||
records, err := env.app.FindAllRecords(pbrepo.FilesCollection)
|
records, err := env.app.FindAllRecords(migrations.FilesCollection)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
var names []string
|
var names []string
|
||||||
@@ -244,14 +245,14 @@ func storedFileNames(t *testing.T, env *testEnv) []string {
|
|||||||
|
|
||||||
// countFiles считает записи о файлах.
|
// countFiles считает записи о файлах.
|
||||||
func countFiles(t *testing.T, env *testEnv) int {
|
func countFiles(t *testing.T, env *testEnv) int {
|
||||||
records, err := env.app.FindAllRecords(pbrepo.FilesCollection)
|
records, err := env.app.FindAllRecords(migrations.FilesCollection)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
return len(records)
|
return len(records)
|
||||||
}
|
}
|
||||||
|
|
||||||
// countJobs считает заведённые задачи расшифровки.
|
// countJobs считает заведённые задачи расшифровки.
|
||||||
func countJobs(t *testing.T, env *testEnv) int {
|
func countJobs(t *testing.T, env *testEnv) int {
|
||||||
records, err := env.app.FindAllRecords(pbrepo.JobsCollection)
|
records, err := env.app.FindAllRecords(migrations.JobsCollection)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
return len(records)
|
return len(records)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import (
|
|||||||
|
|
||||||
"git.vakhrushev.me/av/transcriber/internal/adapter/recognizer"
|
"git.vakhrushev.me/av/transcriber/internal/adapter/recognizer"
|
||||||
pbrepo "git.vakhrushev.me/av/transcriber/internal/adapter/repo/pocketbase"
|
pbrepo "git.vakhrushev.me/av/transcriber/internal/adapter/repo/pocketbase"
|
||||||
|
"git.vakhrushev.me/av/transcriber/internal/adapter/repo/pocketbase/migrations"
|
||||||
"git.vakhrushev.me/av/transcriber/internal/contract"
|
"git.vakhrushev.me/av/transcriber/internal/contract"
|
||||||
"git.vakhrushev.me/av/transcriber/internal/entity"
|
"git.vakhrushev.me/av/transcriber/internal/entity"
|
||||||
)
|
)
|
||||||
@@ -110,7 +111,7 @@ func newTelegramJob(t *testing.T, env *pipelineEnv) *entity.TranscribeJob {
|
|||||||
func clearDelay(t *testing.T, env *pipelineEnv, jobID string) {
|
func clearDelay(t *testing.T, env *pipelineEnv, jobID string) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
record, err := env.app.FindRecordById(pbrepo.JobsCollection, jobID)
|
record, err := env.app.FindRecordById(migrations.JobsCollection, jobID)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
record.Set("delay_time", "")
|
record.Set("delay_time", "")
|
||||||
require.NoError(t, env.app.Save(record))
|
require.NoError(t, env.app.Save(record))
|
||||||
@@ -121,7 +122,7 @@ func clearDelay(t *testing.T, env *pipelineEnv, jobID string) {
|
|||||||
func rotAcquisition(t *testing.T, env *pipelineEnv, jobID string) {
|
func rotAcquisition(t *testing.T, env *pipelineEnv, jobID string) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
record, err := env.app.FindRecordById(pbrepo.JobsCollection, jobID)
|
record, err := env.app.FindRecordById(migrations.JobsCollection, jobID)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
record.Set("acquire_time", types.NowDateTime().Add(-24*time.Hour))
|
record.Set("acquire_time", types.NowDateTime().Add(-24*time.Hour))
|
||||||
require.NoError(t, env.app.Save(record))
|
require.NoError(t, env.app.Save(record))
|
||||||
@@ -179,7 +180,7 @@ func TestDeadJobReturnsAfterStateEdit(t *testing.T) {
|
|||||||
}
|
}
|
||||||
require.Error(t, env.service.FindAndRunConversionJob())
|
require.Error(t, env.service.FindAndRunConversionJob())
|
||||||
|
|
||||||
record, err := env.app.FindRecordById(pbrepo.JobsCollection, job.Id)
|
record, err := env.app.FindRecordById(migrations.JobsCollection, job.Id)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
record.Set("state", entity.StateCreated)
|
record.Set("state", entity.StateCreated)
|
||||||
require.NoError(t, env.app.Save(record))
|
require.NoError(t, env.app.Save(record))
|
||||||
@@ -202,7 +203,7 @@ func TestFailedStepSchedulesRetryWithGrowingDelay(t *testing.T) {
|
|||||||
empty, err := env.fileRepo.CreateRemote("object-key", 1)
|
empty, err := env.fileRepo.CreateRemote("object-key", 1)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
record, err := env.app.FindRecordById(pbrepo.JobsCollection, job.Id)
|
record, err := env.app.FindRecordById(migrations.JobsCollection, job.Id)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
record.Set("file", empty.Id)
|
record.Set("file", empty.Id)
|
||||||
require.NoError(t, env.app.Save(record))
|
require.NoError(t, env.app.Save(record))
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import (
|
|||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
"git.vakhrushev.me/av/transcriber/internal/adapter/repo/pocketbase"
|
"git.vakhrushev.me/av/transcriber/internal/adapter/repo/pocketbase/migrations"
|
||||||
"git.vakhrushev.me/av/transcriber/internal/contract"
|
"git.vakhrushev.me/av/transcriber/internal/contract"
|
||||||
"git.vakhrushev.me/av/transcriber/internal/entity"
|
"git.vakhrushev.me/av/transcriber/internal/entity"
|
||||||
)
|
)
|
||||||
@@ -249,7 +249,7 @@ func TestCheckJobWritesNothingWhenAcquisitionLost(t *testing.T) {
|
|||||||
acquired, err := env.jobRepo.FindAndAcquire(entity.StateTranscribe, "mine", time.Now().Add(-time.Hour))
|
acquired, err := env.jobRepo.FindAndAcquire(entity.StateTranscribe, "mine", time.Now().Add(-time.Hour))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
record, err := env.app.FindRecordById(pocketbase.JobsCollection, job.Id)
|
record, err := env.app.FindRecordById(migrations.JobsCollection, job.Id)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
record.Set("acquisition_id", "someone-else")
|
record.Set("acquisition_id", "someone-else")
|
||||||
require.NoError(t, env.app.Save(record))
|
require.NoError(t, env.app.Save(record))
|
||||||
|
|||||||
Reference in New Issue
Block a user