98 lines
3.7 KiB
Go
98 lines
3.7 KiB
Go
package runner
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"io/fs"
|
|
"os"
|
|
"path/filepath"
|
|
|
|
"gopkg.in/yaml.v3"
|
|
|
|
"textmachine/platform/internal/ingest"
|
|
)
|
|
|
|
// artifacts.go: the engine's FILE channels, as opposed to its command channels.
|
|
//
|
|
// The bank is the one read-out the engine publishes as a file rather than on stdout (D39.122): it
|
|
// lives beside the project database, is written atomically, and can therefore be read while a run is
|
|
// going — which is exactly what a signing stop needs, since the stop IS a live run waiting.
|
|
|
|
// ErrNoBank is a book that has no bank read-out yet: it has never been through a run that produced
|
|
// one. An ordinary state and not a failure — and the caller saves NOTHING on it: an empty bank
|
|
// written over a real one would erase a read-out the engine has simply not made yet.
|
|
var ErrNoBank = errors.New("runner: this book has no bank read-out")
|
|
|
|
// maxBank bounds the read-out — three orders of magnitude above any real bank, so it refuses a file
|
|
// that is not one rather than sizing one that is.
|
|
const maxBank = 256 << 20
|
|
|
|
// ReadBank decodes a book's whole-bank read-out.
|
|
func ReadBank(workdir string) (ingest.Bank, error) {
|
|
db, err := projectDB(workdir)
|
|
if err != nil {
|
|
return ingest.Bank{}, err
|
|
}
|
|
f, err := os.Open(db + ".bank.json")
|
|
if errors.Is(err, fs.ErrNotExist) {
|
|
return ingest.Bank{}, ErrNoBank
|
|
}
|
|
if err != nil {
|
|
return ingest.Bank{}, fmt.Errorf("runner: open bank read-out: %w", err)
|
|
}
|
|
defer f.Close()
|
|
raw, err := io.ReadAll(io.LimitReader(f, maxBank+1))
|
|
if err != nil {
|
|
return ingest.Bank{}, fmt.Errorf("runner: read bank read-out: %w", err)
|
|
}
|
|
if len(raw) > maxBank {
|
|
return ingest.Bank{}, errors.New("runner: the bank read-out is larger than this platform will read")
|
|
}
|
|
return ingest.DecodeBank(raw)
|
|
}
|
|
|
|
// projectDB asks the book's own configuration where its project database is.
|
|
//
|
|
// ⚠ This READS `book.yaml` and does not contradict D39.110 §2b: that decision is about who OWNS the
|
|
// file — the platform renders one starting copy and never edits it again — not about whether the
|
|
// path it declares may be followed. The sidecars are named after `project_db` by the engine, so
|
|
// there is no second place to learn the name from, and the alternative (globbing the directory for
|
|
// `*.bank.json`) would guess where a configuration states.
|
|
//
|
|
// The path is resolved against the BOOK's directory, which is how the engine resolves every relative
|
|
// path in that file.
|
|
func projectDB(workdir string) (string, error) {
|
|
f, err := os.Open(filepath.Join(workdir, ConfigFile))
|
|
if err != nil {
|
|
return "", fmt.Errorf("runner: open book configuration: %w", err)
|
|
}
|
|
defer f.Close()
|
|
raw, err := io.ReadAll(io.LimitReader(f, maxBookConfig+1))
|
|
if err != nil {
|
|
return "", fmt.Errorf("runner: read book configuration: %w", err)
|
|
}
|
|
var cfg struct {
|
|
ProjectDB string `yaml:"project_db"`
|
|
BookID string `yaml:"book_id"`
|
|
}
|
|
if err := yaml.Unmarshal(raw, &cfg); err != nil {
|
|
return "", fmt.Errorf("runner: read book configuration: %w", err)
|
|
}
|
|
if cfg.ProjectDB == "" {
|
|
// ⚠ The key is OPTIONAL to the engine, which defaults to `<book_id>.db` beside the file
|
|
// (backend/internal/config/book.go). Refusing here read the bank as missing on every
|
|
// deployment whose operator used the template as written, since it carries no `project_db`.
|
|
if cfg.BookID == "" {
|
|
return "", errors.New("runner: the book configuration names neither project_db nor book_id")
|
|
}
|
|
return filepath.Join(workdir, cfg.BookID+".db"), nil
|
|
}
|
|
if filepath.IsAbs(cfg.ProjectDB) {
|
|
return cfg.ProjectDB, nil
|
|
}
|
|
return filepath.Join(workdir, cfg.ProjectDB), nil
|
|
}
|
|
|
|
// maxBookConfig bounds what will be read as a book configuration — a page of keys.
|
|
const maxBookConfig = 1 << 20
|