summaryrefslogtreecommitdiff
path: root/internal
diff options
context:
space:
mode:
Diffstat (limited to 'internal')
-rw-r--r--internal/file/content.go52
-rw-r--r--internal/file/file.go197
-rw-r--r--internal/file/file_test.go29
3 files changed, 199 insertions, 79 deletions
diff --git a/internal/file/content.go b/internal/file/content.go
deleted file mode 100644
index 2bb787d..0000000
--- a/internal/file/content.go
+++ /dev/null
@@ -1,52 +0,0 @@
-package file
-
-import (
- "bytes"
- "fmt"
- "os"
- "strings"
- "text/template"
-)
-
-func resolveContent(param, targetPath string) ([]byte, error) {
- var content []byte
- var err error
-
- if strings.HasPrefix(param, "source://") {
- sourcePath := strings.TrimPrefix(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(param)
- }
-
- if strings.HasSuffix(targetPath, ".tmpl") || (strings.HasPrefix(param, "source://") && strings.HasSuffix(strings.TrimPrefix(param, "source://"), ".tmpl")) {
- return applyTemplate(content, param)
- }
-
- return content, nil
-}
-
-func applyTemplate(content []byte, param string) ([]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"] = param
-
- tmpl, err := template.New("resource").Parse(string(content))
- if err != nil {
- 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
-}
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)
-}
diff --git a/internal/file/file_test.go b/internal/file/file_test.go
index e41d7dc..a9397ba 100644
--- a/internal/file/file_test.go
+++ b/internal/file/file_test.go
@@ -31,7 +31,7 @@ func TestWriteTmpFile(t *testing.T) {
tmpPath := filepath.Join(dir, "test.tmp")
content := []byte("temp content")
- if err := writeTmpFile(tmpPath, content); err != nil {
+ if err := writeTmpFile(tmpPath, content, 0o644); err != nil {
t.Fatalf("unexpected error: %v", err)
}
@@ -101,7 +101,7 @@ func TestHaveStringCreateNewFile(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "new.txt")
- if err := Have(path, "hello world"); err != nil {
+ if err := Have(path, WithContent("hello world")); err != nil {
t.Fatalf("unexpected error: %v", err)
}
@@ -114,12 +114,31 @@ func TestHaveStringCreateNewFile(t *testing.T) {
}
}
+func TestHaveMode(t *testing.T) {
+ dir := t.TempDir()
+ path := filepath.Join(dir, "mode.txt")
+ mode := os.FileMode(0o600)
+
+ if err := Have(path, WithContent("mode test"), WithMode(mode)); err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+
+ info, err := os.Stat(path)
+ if err != nil {
+ t.Fatal(err)
+ }
+ // Mask to check only permission bits
+ if info.Mode().Perm() != mode {
+ t.Errorf("expected mode %v, got %v", mode, info.Mode().Perm())
+ }
+}
+
func TestHaveSourceFile(t *testing.T) {
dir := t.TempDir()
sourcePath := filepath.Join("..", "..", "assets", "testfiles", "test.txt")
targetPath := filepath.Join(dir, "target.txt")
- if err := Have(targetPath, "source://"+sourcePath); err != nil {
+ if err := Have(targetPath, WithSource(sourcePath)); err != nil {
t.Fatalf("unexpected error: %v", err)
}
@@ -138,7 +157,7 @@ func TestHaveTemplateFile(t *testing.T) {
sourcePath := filepath.Join("..", "..", "assets", "testfiles", "test.tmpl")
targetPath := filepath.Join(dir, "target.conf")
- if err := Have(targetPath, "source://"+sourcePath); err != nil {
+ if err := Have(targetPath, WithSource(sourcePath)); err != nil {
t.Fatalf("unexpected error: %v", err)
}
@@ -147,7 +166,7 @@ func TestHaveTemplateFile(t *testing.T) {
t.Fatalf("reading file: %v", err)
}
- expectedParam := "source://" + sourcePath
+ expectedParam := sourcePath
if !strings.Contains(string(got), expectedParam) {
t.Errorf("expected content to contain Param %q, got %q", expectedParam, string(got))
}