1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
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
}