приём и чтение записей сведены к одному контракту приложения
- адреса приложения переехали в своё пространство `/app/`, опрос готовности убран целиком: рубеж и причину остановки владелец узнаёт карточкой записи, текст — отдельным адресом названного вида - заведена единая точка отображения доменной ошибки и слой, приводящий к той же форме отказы библиотеки: тело несёт машиночитаемый код рядом с сообщением - у записи появились имя файла отправителя, длительность и размер своими колонками, а у ленты владельца — свой индекс: без него страница сканировала весь архив сервиса
This commit is contained in:
@@ -33,6 +33,14 @@ func (r *stubRecordRepo) Get(string) (*entity.AudioRecord, error) {
|
||||
return nil, errors.New("не зовётся этими проверками")
|
||||
}
|
||||
|
||||
func (r *stubRecordRepo) List(contract.RecordQuery) (*contract.RecordPage, error) {
|
||||
return &contract.RecordPage{}, nil
|
||||
}
|
||||
|
||||
func (r *stubRecordRepo) ResolveTopicNames(string, []string) (map[string]string, error) {
|
||||
return map[string]string{}, nil
|
||||
}
|
||||
|
||||
func (r *stubRecordRepo) FindAndAcquire([]entity.Stage) (*contract.AcquiredRecord, error) {
|
||||
return nil, r.err
|
||||
}
|
||||
|
||||
@@ -22,6 +22,16 @@ import (
|
||||
const (
|
||||
defaultAudioExt = "audio"
|
||||
|
||||
// maxExtLen — потолок длины расширения вместе с точкой.
|
||||
//
|
||||
// Сторож от патологии, а не перечень допустимого: расширения известных
|
||||
// форматов укладываются в пять знаков, и щедрый потолок ничего у отправителя
|
||||
// не отнимает. Он нужен против другого — имени `x.` с четырьмястами знаками
|
||||
// после точки: оно роняет заведение временного файла, и отправитель получает
|
||||
// `500` на входе, за который отвечает сам, а владелец сервиса — строку `ERROR`,
|
||||
// неотличимую от аварии хранилища.
|
||||
maxExtLen = 32
|
||||
|
||||
// Предел отказов. Число обратимо и живёт здесь одним местом; счётчик растёт
|
||||
// при захвате и обнуляется на шаге, завершившемся без отказа либо отложившем
|
||||
// работу.
|
||||
@@ -155,10 +165,15 @@ func (s *TranscribeService) CreateJobFromApi(ctx context.Context, file io.Reader
|
||||
}
|
||||
|
||||
func (s *TranscribeService) createRecord(ctx context.Context, r *entity.AudioRecord, file io.Reader, fileName string) (*entity.AudioRecord, error) {
|
||||
// Определяем расширение файла
|
||||
// Расширение приходит из имени, которое дал отправитель, и потому может быть
|
||||
// чем угодно. Длину назначает сервис: `filepath.Ext` режет по последней точке
|
||||
// и всё, что после неё, берёт дословно, а имя `x.` плюс четыреста знаков
|
||||
// роняет заведение временного файла — отправитель получал бы `500` на входе,
|
||||
// за который отвечает сам, и клал бы в журнал владельца строку `ERROR`,
|
||||
// неотличимую от аварии хранилища.
|
||||
ext := filepath.Ext(fileName)
|
||||
if ext == "" {
|
||||
ext = fmt.Sprintf(".%s", defaultAudioExt) // fallback если расширение не определено
|
||||
if ext == "" || len(ext) > maxExtLen {
|
||||
ext = fmt.Sprintf(".%s", defaultAudioExt)
|
||||
}
|
||||
|
||||
// Собственное имя записи: идентификатор с расширением. Имя, данное
|
||||
@@ -184,7 +199,11 @@ func (s *TranscribeService) createRecord(ctx context.Context, r *entity.AudioRec
|
||||
info, err := s.metaviewer.GetInfo(ctx, work.Path())
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to get file info", "error", err, "file_ext", ext)
|
||||
return nil, err
|
||||
// Признак заводится здесь, а не остаётся голой ошибкой источника
|
||||
// метаданных: причина отказа — присланная запись, а не сбой сервиса, и
|
||||
// без признака ветвь по умолчанию отдала бы `500`. «Файл негоден»
|
||||
// читалось бы как «сломался сервер», и человек не понял бы, что делать.
|
||||
return nil, fmt.Errorf("%w: %w", contract.ErrRecordUnreadable, err)
|
||||
}
|
||||
|
||||
size, err := work.Size()
|
||||
@@ -217,6 +236,23 @@ func (s *TranscribeService) createRecord(ctx context.Context, r *entity.AudioRec
|
||||
r.OriginalFileID = &fileRecord.Id
|
||||
r.StateEnteredAt = clock.Now()
|
||||
|
||||
// Имя, данное отправителем, доходит до самой записи — по нему человек узнаёт
|
||||
// её, пока заголовка нет. В имя файла хранилища и в журнал оно по-прежнему не
|
||||
// идёт: инвариант приватности не тронут, сужена только область его действия.
|
||||
//
|
||||
// Колонка заголовка остаётся пустой: приём заголовков не сочиняет, а
|
||||
// посчитанное языковой моделью название легло бы поверх имени, если бы они
|
||||
// делили одну колонку.
|
||||
if cleaned := entity.SanitizeOriginalFilename(fileName); cleaned != "" {
|
||||
r.OriginalFilename = &cleaned
|
||||
}
|
||||
|
||||
// Величины принятого — снимок с этой минуты. Со строкой файла они намеренно не
|
||||
// сверяются: там лежат величины сегодняшней копии, и уточнение длительности
|
||||
// меняет их, не трогая эти.
|
||||
r.DurationMs = &meta.DurationMs
|
||||
r.SizeBytes = &size
|
||||
|
||||
if err := s.repos.Records.Create(r); err != nil {
|
||||
s.logger.Error("Failed to create audio record", "error", err, "file_id", fileRecord.Id)
|
||||
return nil, err
|
||||
|
||||
Reference in New Issue
Block a user