// 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 }