~ knotless Search/
↑↓ move↵ open⇧↵ all results
0
0
main
147 lines · 3.3 KB Raw History
package systemdunit

import (
	"cmp"
	"fmt"
	"io/fs"
	"os"
	"path/filepath"
	"strings"
)

type KeyValue struct {
	Key string

	Value string
}

func KV(key, value string) KeyValue { return KeyValue{Key: key, Value: value} }

type Section struct {
	Name string

	Entries []KeyValue
}

func (s Section) With(key, value string) Section {
	s.Entries = append(append([]KeyValue(nil), s.Entries...), KV(key, value))
	return s
}

type DropIn struct {
	Name string

	Sections []Section
}

type Unit struct {
	Name string

	Sections []Section

	DropIns []DropIn
}

func renderSections(sections []Section) []byte {
	var b strings.Builder
	for i, section := range sections {
		if i > 0 {
			b.WriteString("\n")
		}
		b.WriteString("[")
		b.WriteString(section.Name)
		b.WriteString("]\n")
		for _, entry := range section.Entries {
			b.WriteString(entry.Key)
			b.WriteString("=")
			b.WriteString(entry.Value)
			b.WriteString("\n")
		}
	}
	return []byte(b.String())
}

func (u Unit) Render() []byte { return renderSections(u.Sections) }

func (d DropIn) Render() []byte { return renderSections(d.Sections) }

type File struct {
	Path string

	Content []byte

	Mode fs.FileMode
}

func Files(dir string, units ...Unit) []File {
	out := []File{}
	for _, unit := range units {
		out = append(out, File{Path: filepath.Join(dir, unit.Name), Content: unit.Render(), Mode: 0o644})
		for _, dropIn := range unit.DropIns {
			out = append(out, File{Path: filepath.Join(dir, unit.Name+".d", dropIn.Name), Content: dropIn.Render(), Mode: 0o644})
		}
	}
	return out
}

func target(root, path string) (string, error) {
	clean := filepath.Clean(path)
	if root == "" {
		if !filepath.IsAbs(clean) {
			return "", fmt.Errorf("systemdunit: %s is not an absolute path", path)
		}
		return clean, nil
	}
	if filepath.IsAbs(clean) {
		clean = strings.TrimPrefix(clean, string(filepath.Separator))
	}
	if clean == "." || clean == ".." || strings.HasPrefix(clean, ".."+string(filepath.Separator)) {
		return "", fmt.Errorf("systemdunit: %s escapes root %s", path, root)
	}
	return filepath.Join(root, clean), nil
}

func Install(root string, files []File) error {
	for _, file := range files {
		dest, err := target(root, file.Path)
		if err != nil {
			return err
		}
		mode := file.Mode
		mode = cmp.Or(mode, 0o644)
		if err := os.MkdirAll(filepath.Dir(dest), 0o755); err != nil {
			return fmt.Errorf("systemdunit: creating the directory of %s: %w", dest, err)
		}
		tmp, err := os.CreateTemp(filepath.Dir(dest), "."+filepath.Base(dest)+".tmp-*")
		if err != nil {
			return fmt.Errorf("systemdunit: creating a temporary file beside %s: %w", dest, err)
		}
		name := tmp.Name()
		remove := true
		defer func() {
			if remove {
				os.Remove(name)
			}
		}()
		if _, err := tmp.Write(file.Content); err != nil {
			tmp.Close()
			return fmt.Errorf("systemdunit: writing %s: %w", dest, err)
		}
		if err := tmp.Chmod(mode); err != nil {
			tmp.Close()
			return fmt.Errorf("systemdunit: setting the mode of %s: %w", dest, err)
		}
		if err := tmp.Sync(); err != nil {
			tmp.Close()
			return fmt.Errorf("systemdunit: syncing %s: %w", dest, err)
		}
		if err := tmp.Close(); err != nil {
			return fmt.Errorf("systemdunit: closing %s: %w", dest, err)
		}
		if err := os.Rename(name, dest); err != nil {
			return fmt.Errorf("systemdunit: renaming %s into place: %w", dest, err)
		}
		remove = false
	}
	return nil
}