у записи появился владелец: чужую больше не отдают

- колонка `owner` связью с `users` в обеих коллекциях новым шагом схемы
  `202608140001`; чтение задачи сужено владельцем, и чужая, ничья и
  несуществующая дают один ответ; правило просмотра файлов сужено им же
- приём по HTTP берёт владельца из сессии, а предъявителя без учётной записи
  пользователя отвергает до чтения тела: позже пришлось бы убирать уложенный
  файл, а уборки файлов сервис не умеет. Выборка воркера владельцем не сужается
- удаление учётной записи с записями отвергается стражем, и вешает его сама
  сборка хранилища: сборка, забывшая его позвать, теряла защиту молча
This commit is contained in:
av
2026-08-14 12:18:11 +03:00
parent b7d4660aef
commit 8af8ec2e54
46 changed files with 2520 additions and 101 deletions
+5
View File
@@ -14,6 +14,11 @@ import (
// и запись о недоставке делает шаг, у которого задача под рукой.
var ErrDeliveryChannelDown = errors.New("delivery channel is down")
// ErrOwnerRequired — приём по HTTP дошёл до заведения задачи, а владельца ему не
// назвали. Значение сентинельное: нести отказу нечего, а имя учётной записи в
// него не кладётся никогда.
var ErrOwnerRequired = errors.New("owner is required to accept a record")
type JobNotFoundError struct {
State string
Message string
+21 -3
View File
@@ -36,9 +36,15 @@ type FileRepository interface {
// CreateLocal кладёт рабочую копию в хранилище под именем name и заводит
// запись о файле. Имя задаёт сервис: умолчание хранилища, строящее его из
// имени отправителя, не применяется.
CreateLocal(name string, work WorkFile) (*entity.File, error)
//
// ownerID — владелец записи, которой файл принадлежит; пустой значит «файл
// без владельца», и таков всякий файл записи, принятой ботом. Владелец
// лежит своей колонкой, а не выводится через задачу: ссылку на файл в
// задаче переставляет каждый шаг конвейера, и исходная копия после
// конвертации не связана с задачей ничем.
CreateLocal(name string, work WorkFile, ownerID string) (*entity.File, error)
// CreateRemote заводит запись о копии, лежащей во внешнем хранилище.
CreateRemote(objectKey string, size int64) (*entity.File, error)
CreateRemote(objectKey string, size int64, ownerID string) (*entity.File, error)
GetByID(id string) (*entity.File, error)
// Open отдаёт содержимое хранимого файла потоком.
Open(fileID string) (io.ReadCloser, error)
@@ -51,7 +57,19 @@ type TranscriptJobRepository interface {
// Пустой holder снимает эту условность и в конвейере не употребляется: все
// его шаги получают признак захвата от FindAndAcquire.
Save(job *entity.TranscribeJob, holder string) error
GetByID(id string) (*entity.TranscribeJob, error)
// GetByID отдаёт задачу, только если её владелец — ownerID. Чужая задача,
// ничья задача и несуществующая дают одну и ту же ошибку: по разнице
// ответов иначе перебирается список заведённых задач.
//
// Владелец здесь обязателен, и пустой ownerID не совпадает ни с чем —
// включая задачи без владельца. Правило записано со стороны спрашивающего:
// обязательность, которую держит одна лишь подпись метода, пустую строку
// пропускает, и вызывающий без учётной записи получил бы ровно множество
// записей бота.
//
// Второго читающего метода нет намеренно: он выбирался бы по
// внимательности вызывающего.
GetByID(id, ownerID string) (*entity.TranscribeJob, error)
// FindAndAcquire забирает задачу одним неделимым шагом и увеличивает число
// её попыток. Работы в состоянии нет — JobNotFoundError.
FindAndAcquire(state, acquisitionId string, rottingTime time.Time) (*entity.TranscribeJob, error)