ajhahn.de
← eeco
Go 66 lines
package selfupdate

import (
	"encoding/json"
	"errors"
	"fmt"
	"os"
	"path/filepath"

	"github.com/ajhahnde/eeco/internal/config"
)

// ledgerName is the reversibility record for binary swaps; sibling to
// state/hooks.json so the same shape applies.
const ledgerName = "binary.json"

// Ledger is the persisted state of the most recent binary swap. A swap
// is the one allowed write outside the workspace (Constraint 1), so
// every recorded field lets the operator reverse it by hand.
type Ledger struct {
	Installed   bool   `json:"installed"`
	FromVersion string `json:"from_version,omitempty"`
	ToVersion   string `json:"to_version,omitempty"`
	RunningPath string `json:"running_path,omitempty"`
	Backup      string `json:"backup,omitempty"`
	SHA256      string `json:"sha256,omitempty"`
	At          string `json:"at,omitempty"`
}

func ledgerPath(cfg *config.Config) string {
	return filepath.Join(cfg.Workspace, "state", ledgerName)
}

func writeLedger(cfg *config.Config, l Ledger) error {
	dir := filepath.Join(cfg.Workspace, "state")
	if err := os.MkdirAll(dir, 0o755); err != nil {
		return fmt.Errorf("binary ledger dir: %w", err)
	}
	b, err := json.MarshalIndent(l, "", "  ")
	if err != nil {
		return err
	}
	return os.WriteFile(ledgerPath(cfg), append(b, '\n'), 0o644)
}

// LoadLedger reads the current ledger record. Used by callers that
// surface "what was the last swap" (doctor probes, status digest).
// A missing file is not an error.
func LoadLedger(cfg *config.Config) (Ledger, error) {
	var l Ledger
	b, err := os.ReadFile(ledgerPath(cfg))
	if err != nil {
		if errors.Is(err, os.ErrNotExist) {
			return l, nil
		}
		return l, err
	}
	if len(b) == 0 {
		return l, nil
	}
	if err := json.Unmarshal(b, &l); err != nil {
		return Ledger{}, nil
	}
	return l, nil
}