Initial Go project built with Bubble Tea and Lip Gloss: - Live preview and management of /etc/update-motd.d scripts - Embedded presets (ubuntu-default, minimal, server-dashboard) - Bundled POSIX sh style scripts (brand header, sysinfo panel, footer) - Snapshot/restore safety around applying changes - CLI subcommands preview, apply, restore, list-presets - Unit tests for detect, sanitize, and apply logic
74 lines
2 KiB
Go
74 lines
2 KiB
Go
// Package motd enthält die Domain-Logik: Discovery der update-motd.d-Scripte,
|
|
// Diff/Plan von Soll-Zuständen, das Ausführen des Plans und die Preview.
|
|
package motd
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"regexp"
|
|
"sort"
|
|
|
|
"motd-assist/internal/assets"
|
|
)
|
|
|
|
// Script ist ein Eintrag in /etc/update-motd.d.
|
|
type Script struct {
|
|
Name string // Dateiname inkl. Nummern-Präfix, z.B. "00-header"
|
|
Path string // Absoluter Pfad
|
|
Enabled bool // Executable-Bit gesetzt (run-parts führt nur diese aus)
|
|
Bundled bool // von motd-assist installiert (Inhalt entspricht einer Vorlage)
|
|
}
|
|
|
|
// MotdDir liefert den Pfad des update-motd.d-Verzeichnisses unter root.
|
|
func MotdDir(root string) string {
|
|
return filepath.Join(root, "etc", "update-motd.d")
|
|
}
|
|
|
|
// run-parts im pam_motd-Kontext ignoriert Dateien mit Punkten; wir halten
|
|
// uns an dieselbe Namenskonvention ([A-Za-z0-9_-]).
|
|
var validName = regexp.MustCompile(`^[A-Za-z0-9_-]+$`)
|
|
|
|
// Discover listet alle Scripte im update-motd.d-Verzeichnis in
|
|
// run-parts-Reihenfolge (lexikalische Sortierung). Gebündelte Scripte
|
|
// werden per Inhaltsvergleich erkannt.
|
|
func Discover(root string) ([]Script, error) {
|
|
dir := MotdDir(root)
|
|
entries, err := os.ReadDir(dir)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("update-motd.d nicht lesbar: %w", err)
|
|
}
|
|
scripts := make([]Script, 0, len(entries))
|
|
for _, e := range entries {
|
|
if e.IsDir() || !validName.MatchString(e.Name()) {
|
|
continue
|
|
}
|
|
info, err := e.Info()
|
|
if err != nil {
|
|
continue
|
|
}
|
|
path := filepath.Join(dir, e.Name())
|
|
s := Script{
|
|
Name: e.Name(),
|
|
Path: path,
|
|
Enabled: info.Mode()&0o111 != 0,
|
|
}
|
|
if b, err := os.ReadFile(path); err == nil {
|
|
_, s.Bundled = assets.MatchScript(string(b))
|
|
}
|
|
scripts = append(scripts, s)
|
|
}
|
|
sort.Slice(scripts, func(i, j int) bool { return scripts[i].Name < scripts[j].Name })
|
|
return scripts, nil
|
|
}
|
|
|
|
// EnabledCount zählt die aktiven Scripte.
|
|
func EnabledCount(scripts []Script) int {
|
|
n := 0
|
|
for _, s := range scripts {
|
|
if s.Enabled {
|
|
n++
|
|
}
|
|
}
|
|
return n
|
|
}
|