Files
transcriber/internal/contract/repository.go
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

136 lines
8.8 KiB
Go

package contract
import (
"io"
"git.vakhrushev.me/av/transcriber/internal/entity"
)
// WorkFile — рабочая копия файла на диске: её просят шаги, отдающие файл
// внешней программе, потому что `ffmpeg` и `ffprobe` принимают имя аргументом.
//
// Заводится копия одним способом — репозиторием файлов, — и убирает её за собой
// Close. Каждый шаг, заводящий копию сам, повторял бы и обязанность прибрать, а
// забытая копия это шестичасовая запись во временном каталоге, о которой не
// узнает никто.
type WorkFile interface {
// Path — имя копии на диске, годное для внешней программы.
Path() string
// Size — длина копии в байтах на момент вызова.
Size() (int64, error)
// Close убирает копию. Зовётся на любом исходе, включая отказ.
Close() error
}
// FileMeta — что известно о копии сверх её содержимого.
type FileMeta struct {
// Format — расширение без точки, в нижнем регистре.
Format string
// DurationMs — длительность, если её удалось прочитать.
DurationMs int64
}
type FileRepository interface {
// Stage принимает содержимое потоком в рабочую копию с заданным
// расширением: по нему внешняя программа выбирает разбор. В память запись
// целиком не читается — расчётный потолок шесть часов.
Stage(ext string, content io.Reader) (WorkFile, error)
// StageEmpty заводит пустую рабочую копию с заданным расширением — под
// результат внешней программы, которая пишет по имени.
StageEmpty(ext string) (WorkFile, error)
// Localize выдаёт рабочую копию хранимого файла.
Localize(fileID string) (WorkFile, error)
// Create кладёт рабочую копию в хранилище под именем name и заводит запись о
// файле. Имя задаёт сервис: умолчание хранилища, строящее его из имени
// отправителя, не применяется.
//
// ownerID — владелец записи, которой файл принадлежит, и он обязателен:
// колонка владельца пустого значения не принимает, пустой отвергается
// схемой. Владелец лежит своей колонкой, а не выводится через запись: файл
// переживает свою запись — шаг заводит его до сохранения, и потерянный
// захват оставляет файл с владельцем и без ссылки.
Create(name string, work WorkFile, meta FileMeta, ownerID string) (*entity.File, error)
GetByID(id string) (*entity.File, error)
// Open отдаёт содержимое хранимого файла потоком.
Open(fileID string) (io.ReadCloser, error)
}
// AcquiredRecord — то, что отдаёт захват: идентификатор записи и признак
// **этого** захвата.
//
// Перечня колонок здесь нет намеренно. Захват, возвращавший колонки поимённо,
// требовал править их в четырёх местах сразу, и забытая колонка приезжала
// нулевой, а первое же сохранение писало этот ноль поверх значения. Колонки шаг
// читает обычным чтением.
type AcquiredRecord struct {
ID string
// Holder — значение, уникальное для каждого захвата. Запись результата
// условна по нему, а не по занятости записи: захват, перевыданный другому по
// протуханию срока или после снятия остановки человеком, обязан обратить
// запись первого в отказ.
Holder string
}
type AudioRecordRepository interface {
Create(record *entity.AudioRecord) error
// Save сохраняет запись, захват которой держит holder. Захват, доставшийся
// за время работы другому, даёт LostAcquisitionError и запись не проводит.
// Пустой holder снимает эту условность и в конвейере не употребляется: все
// его шаги получают признак захвата от FindAndAcquire.
Save(record *entity.AudioRecord, holder string) error
// GetByID отдаёт запись, только если её владелец — ownerID. Чужая запись,
// ничья и несуществующая дают одну и ту же ошибку: по разнице ответов иначе
// перебирается список заведённых записей.
//
// Владелец здесь обязателен, и пустой ownerID не совпадает ни с чем —
// включая записи без владельца. Правило записано со стороны спрашивающего:
// обязательность, которую держит одна лишь подпись метода, пустую строку
// пропускает.
GetByID(id, ownerID string) (*entity.AudioRecord, error)
// Get отдаёт запись без сужения владельцем: им пользуется конвейер, чья
// выборка владельцем не сужается.
Get(id string) (*entity.AudioRecord, error)
// FindAndAcquire забирает пригодную к работе запись одним неделимым шагом и
// увеличивает число её отказов. Отбор идёт по рабочим рубежам, паузе, сроку
// протухания захвата и отсутствию признака остановки; срок протухания
// приезжает с рубежом и пишется в саму запись.
//
// Работы нет — JobNotFoundError.
FindAndAcquire(stages []entity.Stage) (*AcquiredRecord, error)
}
// TextRepository — тексты записи. Пара «запись и вид» уникальна: повтор
// прерванного шага не заводит второй строки.
type TextRepository interface {
Put(recordID, kind, contents string) (*entity.Text, error)
GetByID(id string) (*entity.Text, error)
}
// StructureRepository — структура реплик записи. Пара «запись и версия разбора»
// уникальна по той же причине.
type StructureRepository interface {
Put(recordID string, version int, replicas []entity.Replica) (*entity.Structure, error)
GetByID(id string) (*entity.Structure, error)
}
// RecognitionRepository — попытка распознавания у внешнего провайдера.
type RecognitionRepository interface {
// Create заводит строку попытки **до** обращения к провайдеру: окно между
// его ответом и записью идентификатора — то место, где теряется оплаченное.
Create(recognition *entity.Recognition) error
// Submitted сохраняет адрес аудио и идентификатор заведённой операции. По
// последнему повторный шаг узнаёт, что за эту запись уже заплачено.
Submitted(id, sourceURI, externalID string) error
// Finish отмечает завершение операции и кладёт сырой ответ вложением.
Finish(id string, raw []byte) error
GetByID(id string) (*entity.Recognition, error)
// ReadRaw отдаёт сохранённый ответ провайдера. Зовётся только тогда, когда
// ответ нужен: шаг опроса читает строку попытки без него.
ReadRaw(id string) ([]byte, error)
}
// RecordEventRepository — журнал событий записи.
type RecordEventRepository interface {
Append(event *entity.RecordEvent) error
}