- база своя: два пула, захват одним UPDATE ... RETURNING, шаги схемы на goose под файловым замком, одна миграция начальной схемы вместо семи прежних - транспорт переписан на net/http: свои слои, свой ограничитель частоты, отдача файла с проверкой владельца; панель /_/ и пространство /api/ исчезли - по находкам ревью: журнал не пишет путь под корнем приложения, ключ бюджета читается справа налево, узнавание известного идёт читающим пулом
101 lines
4.4 KiB
Go
101 lines
4.4 KiB
Go
package main
|
|
|
|
import (
|
|
"flag"
|
|
"fmt"
|
|
"log"
|
|
"os"
|
|
|
|
sqliterepo "git.vakhrushev.me/av/transcriber/internal/adapter/repo/sqlite"
|
|
"git.vakhrushev.me/av/transcriber/internal/config"
|
|
"git.vakhrushev.me/av/transcriber/internal/entity"
|
|
"git.vakhrushev.me/av/transcriber/internal/ident"
|
|
)
|
|
|
|
// stepResume — чем возврат в работу подписывается в журнале событий записи.
|
|
const stepResume = "resume"
|
|
|
|
// runResume возвращает остановленную запись в работу.
|
|
//
|
|
// Подкоманда встала на место панели владельца: панели у сервиса больше нет, а
|
|
// экраны правки записи приносят отдельные задачи. Из всего, что владелец делал
|
|
// панелью, отложить до экранов нельзя было одно — возврат остановленной записи.
|
|
//
|
|
// **Колонок подкоманда не пишет.** Перечень полей, которые возврат обязан
|
|
// сбросить — признак остановки, признак захвата и срок его протухания, число
|
|
// отказов, паузу и время входа в рубеж, — исполняет домен одним действием.
|
|
// Рука, забывшая любое из них, оставила бы запись либо невидимой для захвата,
|
|
// либо останавливаемой снова первым же захватом — молча, без единой строки.
|
|
//
|
|
// Событие журнала записи пишется с происхождением «человек»: иначе запись,
|
|
// побывавшая остановленной и вернувшаяся в работу, неотличима в журнале от
|
|
// записи, которую конвейер вёл без остановок, а происхождение события перестаёт
|
|
// различать что-либо.
|
|
func runResume(args []string) {
|
|
flags := flag.NewFlagSet("resume", flag.ExitOnError)
|
|
configPath := flags.String("c", "config.toml", "путь к файлу настроек")
|
|
if err := flags.Parse(args); err != nil {
|
|
os.Exit(2)
|
|
}
|
|
|
|
if flags.NArg() != 1 {
|
|
fmt.Fprint(os.Stderr, "укажи идентификатор записи: devtools resume [-c config.toml] <id>\n")
|
|
os.Exit(2)
|
|
}
|
|
|
|
recordID, ok := ident.Parse(flags.Arg(0))
|
|
if !ok {
|
|
log.Fatalf("идентификатор записи не читается: %q", flags.Arg(0))
|
|
}
|
|
|
|
cfg, err := config.LoadConfig(*configPath)
|
|
if err != nil {
|
|
log.Fatalf("настройки не читаются: %v", err)
|
|
}
|
|
if err := cfg.Storage.Validate(); err != nil {
|
|
log.Fatalf("настройки хранилища негодны: %v", err)
|
|
}
|
|
|
|
db, err := sqliterepo.Open(cfg.Storage.DataDir, sqliterepo.Settings{
|
|
BusyTimeoutMs: cfg.Storage.BusyTimeoutMs,
|
|
ReadConnections: cfg.Storage.ReadConnections,
|
|
})
|
|
if err != nil {
|
|
log.Fatalf("база не открывается: %v", err)
|
|
}
|
|
defer func() {
|
|
if err := db.Close(); err != nil {
|
|
log.Printf("база закрылась с отказом: %v", err)
|
|
}
|
|
}()
|
|
|
|
records := sqliterepo.NewAudioRecordRepository(db)
|
|
events := sqliterepo.NewRecordEventRepository(db)
|
|
|
|
record, err := records.Get(recordID)
|
|
if err != nil {
|
|
log.Fatalf("запись не читается: %v", err)
|
|
}
|
|
if !record.IsHalted() {
|
|
log.Fatalf("запись %s не остановлена: возвращать в работу нечего", recordID)
|
|
}
|
|
|
|
// Захват снимает сам домен, поэтому сохранение идёт **безусловным**: держателя
|
|
// у остановленной записи нет, и сверять признак захвата не с чем.
|
|
record.Resume()
|
|
if err := records.Save(record, ""); err != nil {
|
|
log.Fatalf("запись не сохраняется: %v", err)
|
|
}
|
|
|
|
if err := events.Append(&entity.RecordEvent{
|
|
RecordID: recordID,
|
|
Origin: entity.EventOriginHuman,
|
|
Step: stepResume,
|
|
Outcome: entity.EventOutcomeResumed,
|
|
}); err != nil {
|
|
log.Fatalf("событие журнала записи не сохраняется: %v", err)
|
|
}
|
|
|
|
log.Printf("запись %s возвращена в работу с рубежа %s", recordID, record.State)
|
|
}
|