summaryrefslogtreecommitdiff
path: root/internal/resource/dir/dir.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/resource/dir/dir.go')
-rw-r--r--internal/resource/dir/dir.go224
1 files changed, 224 insertions, 0 deletions
diff --git a/internal/resource/dir/dir.go b/internal/resource/dir/dir.go
new file mode 100644
index 0000000..0024c6c
--- /dev/null
+++ b/internal/resource/dir/dir.go
@@ -0,0 +1,224 @@
+package dir
+
+import (
+ "fmt"
+ "log"
+ "os"
+ "os/user"
+ "strconv"
+
+ "codeberg.org/snonux/gonf/internal/resource"
+)
+
+type Dir struct {
+ path string
+ source string
+ user string
+ group string
+ mode os.FileMode // this directory's own mode, default 0o750
+ fileMode os.FileMode // mode for regular files copied from source, default 0o640
+ prune bool // reconciles extra dest files during a source copy, and recursive-remove during IsAbsent()
+ absent bool
+}
+
+type Option func(*Dir)
+
+func WithSource(source string) Option {
+ return func(d *Dir) {
+ d.source = source
+ }
+}
+
+func WithUser(user string) Option {
+ return func(d *Dir) {
+ d.user = user
+ }
+}
+
+func WithGroup(group string) Option {
+ return func(d *Dir) {
+ d.group = group
+ }
+}
+
+func WithMode(mode os.FileMode) Option {
+ return func(d *Dir) {
+ d.mode = mode
+ }
+}
+
+func WithFileMode(mode os.FileMode) Option {
+ return func(d *Dir) {
+ d.fileMode = mode
+ }
+}
+
+func WithPrune() Option {
+ return func(d *Dir) {
+ d.prune = true
+ }
+}
+
+func IsAbsent() Option {
+ return func(d *Dir) {
+ d.absent = true
+ }
+}
+
+func build(path string, opts ...Option) (*Dir, error) {
+ curr, err := user.Current()
+ if err != nil {
+ return nil, fmt.Errorf("failed to get current user for default: %w", err)
+ }
+
+ d := &Dir{
+ path: path,
+ mode: 0o750,
+ fileMode: 0o640,
+ user: curr.Username,
+ group: curr.Gid,
+ }
+
+ for _, opt := range opts {
+ opt(d)
+ }
+
+ return d, nil
+}
+
+// apply performs the idempotent OS work for d without registering a
+// resource.
+func (d *Dir) apply() error {
+ if d.absent {
+ return ensureAbsent(d)
+ }
+
+ if err := ensureDirectorySelf(d); err != nil {
+ return err
+ }
+
+ if d.source == "" {
+ return nil
+ }
+
+ if err := copySourceTree(d); err != nil {
+ return err
+ }
+
+ if d.prune {
+ return pruneTree(d)
+ }
+
+ return nil
+}
+
+// ensureDirectorySelf ensures d.path exists as a directory with the desired
+// mode and ownership. It is idempotent: an existing directory only has its
+// attributes re-enforced, and an existing non-directory is an error.
+func ensureDirectorySelf(d *Dir) error {
+ log.Printf("processing directory: %s", d.path)
+
+ info, err := os.Lstat(d.path)
+ switch {
+ case err == nil:
+ if !info.IsDir() {
+ return fmt.Errorf("%s exists and is not a directory", d.path)
+ }
+ log.Printf("directory %s already exists", d.path)
+
+ case os.IsNotExist(err):
+ log.Printf("creating directory %s with mode %v", d.path, d.mode)
+ if err := os.MkdirAll(d.path, d.mode); err != nil {
+ return fmt.Errorf("failed to create directory %s: %w", d.path, err)
+ }
+
+ default:
+ return fmt.Errorf("failed to stat %s: %w", d.path, err)
+ }
+
+ return applyAttributesTo(d.path, d.mode, d.user, d.group)
+}
+
+// ensureAbsent removes d.path if it exists. It is idempotent: a missing path
+// is not an error. By default non-empty directories are not removed;
+// combine with WithPrune() to remove a directory and its contents
+// recursively.
+func ensureAbsent(d *Dir) error {
+ log.Printf("ensuring absent: %s", d.path)
+
+ remove := os.Remove
+ if d.prune {
+ remove = os.RemoveAll
+ }
+
+ if err := remove(d.path); err != nil {
+ if os.IsNotExist(err) {
+ log.Printf("%s already absent", d.path)
+ return nil
+ }
+ return fmt.Errorf("failed to remove %s: %w", d.path, err)
+ }
+
+ log.Printf("removed %s", d.path)
+ return nil
+}
+
+// applyAttributesTo is dir's own small chmod/chown helper, deliberately not
+// shared with the file package so the two packages' attribute-application
+// behavior can evolve independently.
+func applyAttributesTo(path string, mode os.FileMode, usr, group string) error {
+ if err := os.Chmod(path, mode); err != nil {
+ return fmt.Errorf("failed to chmod %s to %v: %w", path, mode, err)
+ }
+ log.Printf("set mode %v for %s", mode, path)
+
+ uid, gid := -1, -1
+
+ if usr != "" {
+ u, err := user.Lookup(usr)
+ if err != nil {
+ return fmt.Errorf("failed to lookup user %s: %w", usr, err)
+ }
+ uid, _ = strconv.Atoi(u.Uid)
+ }
+
+ if group != "" {
+ gidInt, err := strconv.Atoi(group)
+ if err != nil {
+ return fmt.Errorf("group must be numeric for now: %s", group)
+ }
+ gid = gidInt
+ }
+
+ if err := os.Chown(path, uid, gid); err != nil {
+ return fmt.Errorf("failed to chown %s to %s:%s: %w", path, usr, group, err)
+ }
+ log.Printf("set owner %s:%s for %s", usr, group, path)
+
+ return nil
+}
+
+// Ensure builds and applies the directory resource described by opts,
+// without registering it.
+func Ensure(path string, opts ...Option) error {
+ d, err := build(path, opts...)
+ if err != nil {
+ return err
+ }
+ return d.apply()
+}
+
+func Have(path string, opts ...Option) resource.Resource {
+ d, err := build(path, opts...)
+ if err != nil {
+ log.Fatalf("failed to apply directory resource %s: %v", path, err)
+ }
+
+ res := resource.Register("Directory", d.path)
+
+ if err := d.apply(); err != nil {
+ log.Fatalf("failed to apply directory resource %s: %v", path, err)
+ }
+
+ return res
+}