package installmeta import ( "fmt" "os" "os/user" "path/filepath" "strconv" "strings" "time" toml "github.com/pelletier/go-toml" ) const ( DefaultDir = "/etc/banger" DefaultPath = DefaultDir + "/install.toml" DefaultService = "bangerd.service" DefaultRootHelperService = "bangerd-root.service" DefaultSocketPath = "/run/banger/bangerd.sock" DefaultRootHelperRuntimeDir = "/run/banger-root" DefaultRootHelperSocketPath = DefaultRootHelperRuntimeDir + "/bangerd-root.sock" ) type Metadata struct { OwnerUser string `toml:"owner_user"` OwnerUID int `toml:"owner_uid"` OwnerGID int `toml:"owner_gid"` OwnerHome string `toml:"owner_home"` InstalledAt time.Time `toml:"installed_at"` Version string `toml:"version,omitempty"` Commit string `toml:"commit,omitempty"` BuiltAt string `toml:"built_at,omitempty"` } func LookupOwner(name string) (Metadata, error) { name = strings.TrimSpace(name) if name == "" { return Metadata{}, fmt.Errorf("owner username is required") } entry, err := user.Lookup(name) if err != nil { return Metadata{}, err } uid, err := strconv.Atoi(entry.Uid) if err != nil { return Metadata{}, fmt.Errorf("parse owner uid %q: %w", entry.Uid, err) } gid, err := strconv.Atoi(entry.Gid) if err != nil { return Metadata{}, fmt.Errorf("parse owner gid %q: %w", entry.Gid, err) } home := strings.TrimSpace(entry.HomeDir) if home == "" || !filepath.IsAbs(home) { return Metadata{}, fmt.Errorf("owner %q has invalid home directory %q", name, entry.HomeDir) } return Metadata{ OwnerUser: name, OwnerUID: uid, OwnerGID: gid, OwnerHome: home, }, nil } func Load(path string) (Metadata, error) { if strings.TrimSpace(path) == "" { path = DefaultPath } data, err := os.ReadFile(path) if err != nil { return Metadata{}, err } var meta Metadata if err := toml.Unmarshal(data, &meta); err != nil { return Metadata{}, err } if err := meta.Validate(); err != nil { return Metadata{}, err } return meta, nil } func Save(path string, meta Metadata) error { if strings.TrimSpace(path) == "" { path = DefaultPath } if err := meta.Validate(); err != nil { return err } if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { return err } data, err := toml.Marshal(meta) if err != nil { return err } return os.WriteFile(path, data, 0o644) } // UpdateBuildInfo refreshes only the Version / Commit / BuiltAt // fields on the install metadata, preserving everything else // (OwnerUser/UID/GID/Home and the original InstalledAt timestamp). // Used by `banger update` to record what's running after a // successful binary swap; the install identity is unchanged so // re-running `banger system install` is not required. // // Errors when path doesn't exist or can't be parsed — `banger // update` runs in system mode where install.toml IS the source of // truth; a missing file means we shouldn't be updating at all. func UpdateBuildInfo(path, version, commit, builtAt string) error { if strings.TrimSpace(path) == "" { path = DefaultPath } meta, err := Load(path) if err != nil { return err } meta.Version = strings.TrimSpace(version) meta.Commit = strings.TrimSpace(commit) meta.BuiltAt = strings.TrimSpace(builtAt) return Save(path, meta) } func (m Metadata) Validate() error { if strings.TrimSpace(m.OwnerUser) == "" { return fmt.Errorf("install metadata missing owner_user") } if m.OwnerUID < 0 { return fmt.Errorf("install metadata has invalid owner_uid %d", m.OwnerUID) } if m.OwnerGID < 0 { return fmt.Errorf("install metadata has invalid owner_gid %d", m.OwnerGID) } if strings.TrimSpace(m.OwnerHome) == "" || !filepath.IsAbs(m.OwnerHome) { return fmt.Errorf("install metadata has invalid owner_home %q", m.OwnerHome) } return nil }