хранилище переехало с PocketBase на SQLite со своим каталогом файлов
- база своя: два пула, захват одним UPDATE ... RETURNING, шаги схемы на goose под файловым замком, одна миграция начальной схемы вместо семи прежних - транспорт переписан на net/http: свои слои, свой ограничитель частоты, отдача файла с проверкой владельца; панель /_/ и пространство /api/ исчезли - по находкам ревью: журнал не пишет путь под корнем приложения, ключ бюджета читается справа налево, узнавание известного идёт читающим пулом
This commit is contained in:
@@ -8,6 +8,11 @@ import (
|
||||
// ErrOwnerRequired — приём по HTTP дошёл до заведения задачи, а владельца ему не
|
||||
// назвали. Значение сентинельное: нести отказу нечего, а имя учётной записи в
|
||||
// него не кладётся никогда.
|
||||
//
|
||||
// Достижимого случая у него нет: узнавание заводит учётную запись само, и
|
||||
// предъявителя без неё под корнем приложения не бывает. Отдельной ветви ответа
|
||||
// он поэтому не получает — ветвь по умолчанию читает его как аварию сервиса,
|
||||
// каковой он и был бы.
|
||||
var ErrOwnerRequired = errors.New("owner is required to accept a record")
|
||||
|
||||
// ErrRecordUnreadable — присланную запись не удалось прочитать: источник
|
||||
@@ -31,6 +36,18 @@ var ErrRecordTooLarge = errors.New("uploaded record exceeds size limit")
|
||||
// человек увидел бы «не найдено» на своей записи, загруженной минуту назад.
|
||||
var ErrTextNotReady = errors.New("requested text view is not ready yet")
|
||||
|
||||
// ErrCopyNotReady — копии записи запрошенного вида у неё ещё нет. Состояние, а
|
||||
// не отсутствие, и код у него тот же, что у ненаписанного текста: пустой ответ
|
||||
// читался бы как пустой файл, а «не найдено» слилось бы с ответом на чужую и
|
||||
// неизвестную запись — человек увидел бы его на своей записи, загруженной
|
||||
// минуту назад.
|
||||
var ErrCopyNotReady = errors.New("requested file copy is not ready yet")
|
||||
|
||||
// ErrTooManyRequests — бюджет ограничителя частоты выбран. Признак заводится
|
||||
// затем, чтобы отказ ограничителя уходил той же формой тела, что и отказ
|
||||
// обработчика: он рождается слоем и до обработчика не доходит вовсе.
|
||||
var ErrTooManyRequests = errors.New("request rate budget is exhausted")
|
||||
|
||||
// ErrBadRequest — во входе запроса негодное значение: неизвестный вид текста,
|
||||
// нечитаемый ключ страницы, отрицательный размер. Отличается от ErrRecordUnreadable
|
||||
// тем, что негодна **просьба**, а не присланная запись.
|
||||
@@ -47,6 +64,11 @@ var ErrUnauthorized = errors.New("session is required")
|
||||
// отсутствующий адрес отсутствующей записью.
|
||||
var ErrNotFound = errors.New("address not found")
|
||||
|
||||
// ErrLoginNotAcceptable — логин негоден: пустой, из одних пробельных знаков,
|
||||
// длиннее предела или с управляющими знаками. Это не отказ хранилища, а негодный
|
||||
// ввод, и звать по нему учётную запись не надо.
|
||||
var ErrLoginNotAcceptable = errors.New("provider login is not acceptable")
|
||||
|
||||
type JobNotFoundError struct {
|
||||
State string
|
||||
Message string
|
||||
|
||||
@@ -2,6 +2,7 @@ package contract
|
||||
|
||||
import (
|
||||
"io"
|
||||
"time"
|
||||
|
||||
"git.vakhrushev.me/av/transcriber/internal/entity"
|
||||
)
|
||||
@@ -40,19 +41,24 @@ type FileRepository interface {
|
||||
StageEmpty(ext string) (WorkFile, error)
|
||||
// Localize выдаёт рабочую копию хранимого файла.
|
||||
Localize(fileID string) (WorkFile, error)
|
||||
// Create кладёт рабочую копию в хранилище под именем name и заводит запись о
|
||||
// файле. Имя задаёт сервис: умолчание хранилища, строящее его из имени
|
||||
// отправителя, не применяется.
|
||||
// Create кладёт рабочую копию в каталог данных под именем name и заводит
|
||||
// строку о файле. Имя задаёт сервис, и имя, данное отправителем, в него не
|
||||
// попадает: от него взято только расширение.
|
||||
//
|
||||
// recordID — запись, которой копия принадлежит: копии одной записи лежат её
|
||||
// подкаталогом, и имя этого подкаталога и есть идентификатор записи. Приём
|
||||
// знает его раньше, чем кладёт файл, потому что назначает сам.
|
||||
//
|
||||
// ownerID — владелец записи, которой файл принадлежит, и он обязателен:
|
||||
// колонка владельца пустого значения не принимает, пустой отвергается
|
||||
// схемой. Владелец лежит своей колонкой, а не выводится через запись: файл
|
||||
// переживает свою запись — шаг заводит его до сохранения, и потерянный
|
||||
// захват оставляет файл с владельцем и без ссылки.
|
||||
Create(name string, work WorkFile, meta FileMeta, ownerID string) (*entity.File, error)
|
||||
Create(recordID, name string, work WorkFile, meta FileMeta, ownerID string) (*entity.File, error)
|
||||
GetByID(id string) (*entity.File, error)
|
||||
// Open отдаёт содержимое хранимого файла потоком.
|
||||
Open(fileID string) (io.ReadCloser, error)
|
||||
// Open отдаёт содержимое хранимого файла потоком с перемоткой: отдача по
|
||||
// диапазону читает запрошенный кусок, а не файл целиком.
|
||||
Open(fileID string) (io.ReadSeekCloser, error)
|
||||
}
|
||||
|
||||
// AcquiredRecord — то, что отдаёт захват: идентификатор записи и признак
|
||||
@@ -74,8 +80,12 @@ type AcquiredRecord struct {
|
||||
// RecordCursor — положение в ленте записей, заданное **полным** ключом
|
||||
// сортировки. Одного времени мало: у записей, принятых одним запросом, оно
|
||||
// совпадает, и порядок между ними иначе не определён.
|
||||
//
|
||||
// Время лежит здесь значением времени, а не строкой: вид, каким оно уходит в
|
||||
// запрос, принадлежит хранилищу — сравнение там побайтово, и вид, собранный
|
||||
// транспортом, разошёлся бы с колонкой молча, обратив условие в постоянную ложь.
|
||||
type RecordCursor struct {
|
||||
CreatedAt string
|
||||
CreatedAt time.Time
|
||||
ID string
|
||||
}
|
||||
|
||||
@@ -154,7 +164,8 @@ type RecognitionRepository interface {
|
||||
// Submitted сохраняет адрес аудио и идентификатор заведённой операции. По
|
||||
// последнему повторный шаг узнаёт, что за эту запись уже заплачено.
|
||||
Submitted(id, sourceURI, externalID string) error
|
||||
// Finish отмечает завершение операции и кладёт сырой ответ вложением.
|
||||
// Finish отмечает завершение операции и кладёт сохранённый ответ отдельным
|
||||
// файлом в подкаталоге записи.
|
||||
Finish(id string, raw []byte) error
|
||||
GetByID(id string) (*entity.Recognition, error)
|
||||
// ReadRaw отдаёт сохранённый ответ провайдера. Зовётся только тогда, когда
|
||||
@@ -166,3 +177,31 @@ type RecognitionRepository interface {
|
||||
type RecordEventRepository interface {
|
||||
Append(event *entity.RecordEvent) error
|
||||
}
|
||||
|
||||
// Identity — то, чем доверенный источник называет пришедшего.
|
||||
//
|
||||
// Логин — ключ учётной записи, остальное берётся только при её заведении.
|
||||
type Identity struct {
|
||||
Login string
|
||||
Name string
|
||||
Email string
|
||||
}
|
||||
|
||||
// UserAccount — учётная запись сервиса, какой её видит транспорт: ключ и имя,
|
||||
// пригодное к показу. Логина у провайдера и адреса почты здесь нет: оба
|
||||
// принадлежат человеку, а не сервису, и наружу не выходят.
|
||||
type UserAccount struct {
|
||||
ID string
|
||||
Name string
|
||||
}
|
||||
|
||||
// UserRepository — учётные записи.
|
||||
//
|
||||
// Дом правила «найти по логину, а не найдя — завести» один, и он в хранилище, а
|
||||
// не в транспорте: второй способ представиться возьмёт этот же метод.
|
||||
type UserRepository interface {
|
||||
// EnsureUser находит учётную запись по логину у провайдера, а не найдя —
|
||||
// заводит её. Второе значение истинно только у заведённой: заведение —
|
||||
// событие, и владелец обязан видеть его строкой журнала.
|
||||
EnsureUser(identity Identity) (account *UserAccount, created bool, err error)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user