diff options
Diffstat (limited to 'internal/file/file.go')
| -rw-r--r-- | internal/file/file.go | 197 |
1 files changed, 175 insertions, 22 deletions
diff --git a/internal/file/file.go b/internal/file/file.go index aed7b13..cf46f19 100644 --- a/internal/file/file.go +++ b/internal/file/file.go @@ -1,22 +1,189 @@ package file import ( + "bytes" "crypto/sha256" + "fmt" "log" "os" + "os/user" + "strconv" + "strings" + "text/template" "codeberg.org/snonux/gonf/internal/resource" ) -func Have(path, param string) error { - _ = resource.Register("File", path) +type File struct { + path string + param string + source string + user string + group string + mode os.FileMode +} + +type Option func(*File) + +func WithContent(content string) Option { + return func(f *File) { + f.param = content + f.source = "" + } +} + +func WithSource(source string) Option { + return func(f *File) { + if !strings.HasPrefix(source, "source://") { + source = "source://" + source + } + f.source = source + f.param = source + } +} + +func WithUser(user string) Option { + return func(f *File) { + f.user = user + } +} + +func WithGroup(group string) Option { + return func(f *File) { + f.group = group + } +} + +func WithMode(mode os.FileMode) Option { + return func(f *File) { + f.mode = mode + } +} + +func Have(path string, opts ...Option) error { + curr, err := user.Current() + if err != nil { + log.Fatalf("failed to get current user for default: %v", err) + } + + f := &File{ + path: path, + mode: 0o640, + user: curr.Username, + group: curr.Gid, + } + + for _, opt := range opts { + opt(f) + } + + return f.Apply() +} + +func (f *File) Apply() error { + _ = resource.Register("File", f.path) + + content, err := f.resolveContent() + if err != nil { + log.Fatalf("failed to resolve content for %s: %v", f.path, err) + } + + return f.have(content) +} + +func (f *File) resolveContent() ([]byte, error) { + var content []byte + var err error + + if strings.HasPrefix(f.param, "source://") { + sourcePath := strings.TrimPrefix(f.param, "source://") + content, err = os.ReadFile(sourcePath) + if err != nil { + return nil, fmt.Errorf("failed to read source file %s: %w", sourcePath, err) + } + } else { + content = []byte(f.param) + } + + if strings.HasSuffix(f.path, ".tmpl") || (strings.HasPrefix(f.param, "source://") && strings.HasSuffix(strings.TrimPrefix(f.param, "source://"), ".tmpl")) { + return f.applyTemplate(content) + } - content, err := resolveContent(param, path) + return content, nil +} + +func (f *File) applyTemplate(content []byte) ([]byte, error) { + data := make(map[string]string) + for _, env := range os.Environ() { + pair := strings.SplitN(env, "=", 2) + if len(pair) == 2 { + data[pair[0]] = pair[1] + } + } + data["Param"] = f.param + + tmpl, err := template.New("resource").Parse(string(content)) if err != nil { - log.Fatalf("failed to resolve content for %s: %v", path, err) + return nil, fmt.Errorf("template parse error: %w", err) + } + + var buf bytes.Buffer + if err := tmpl.Execute(&buf, data); err != nil { + return nil, fmt.Errorf("template execute error: %w", err) + } + return buf.Bytes(), nil +} + +func (f *File) have(content []byte) error { + log.Printf("processing file: %s", f.path) + existingChecksum := getChecksum(f.path) + newChecksum := sha256.Sum256(content) + log.Printf("computed checksum for new content: %x", newChecksum) + + tmpPath := f.path + ".tmp" + if err := writeTmpFile(tmpPath, content, f.mode); err != nil { + return err + } + + if err := updateFromTmp(tmpPath, f.path, existingChecksum != newChecksum); err != nil { + return err } - return have(path, content) + return f.applyAttributes() +} + +func (f *File) applyAttributes() error { + // Apply Mode + if err := os.Chmod(f.path, f.mode); err != nil { + return fmt.Errorf("failed to chmod %s to %v: %w", f.path, f.mode, err) + } + log.Printf("set mode %v for %s", f.mode, f.path) + + // Apply User and Group + uid, gid := -1, -1 + + if f.user != "" { + u, err := user.Lookup(f.user) + if err != nil { + return fmt.Errorf("failed to lookup user %s: %w", f.user, err) + } + uid, _ = strconv.Atoi(u.Uid) + } + + if f.group != "" { + gidInt, err := strconv.Atoi(f.group) + if err != nil { + return fmt.Errorf("group must be numeric for now: %s", f.group) + } + gid = gidInt + } + + if err := os.Chown(f.path, uid, gid); err != nil { + return fmt.Errorf("failed to chown %s to %s:%s: %w", f.path, f.user, f.group, err) + } + log.Printf("set owner %s:%s for %s", f.user, f.group, f.path) + + return nil } func getChecksum(path string) [32]byte { @@ -31,9 +198,9 @@ func getChecksum(path string) [32]byte { return checksum } -func writeTmpFile(tmpPath string, content []byte) error { - log.Printf("writing %d bytes to temporary file %s", len(content), tmpPath) - if err := os.WriteFile(tmpPath, content, 0o644); err != nil { +func writeTmpFile(tmpPath string, content []byte, mode os.FileMode) error { + log.Printf("writing %d bytes to temporary file %s with mode %v", len(content), tmpPath, mode) + if err := os.WriteFile(tmpPath, content, mode); err != nil { log.Printf("failed to write temporary file %s: %v", tmpPath, err) return err } @@ -61,17 +228,3 @@ func updateFromTmp(tmpPath, path string, checksumChanged bool) error { log.Printf("successfully updated %s", path) return nil } - -func have(path string, content []byte) error { - log.Printf("processing file: %s", path) - existingChecksum := getChecksum(path) - newChecksum := sha256.Sum256(content) - log.Printf("computed checksum for new content: %x", newChecksum) - - tmpPath := path + ".tmp" - if err := writeTmpFile(tmpPath, content); err != nil { - return err - } - - return updateFromTmp(tmpPath, path, existingChecksum != newChecksum) -} |
