summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--TODO.md19
-rw-r--r--examples/examples.go42
-rw-r--r--internal/resource/dir/dir.go66
-rw-r--r--internal/resource/dir/dir_test.go3
-rw-r--r--internal/resource/dir/source.go11
-rw-r--r--internal/resource/file/file.go65
-rw-r--r--internal/resource/file/file_test.go2
-rw-r--r--internal/resource/link/link.go42
-rw-r--r--internal/resource/link/link_test.go14
-rw-r--r--internal/resource/opt/opt.go156
10 files changed, 263 insertions, 157 deletions
diff --git a/TODO.md b/TODO.md
index 807ebe6..33a5e9a 100644
--- a/TODO.md
+++ b/TODO.md
@@ -1,22 +1,3 @@
-## More ideas:
-
-Dont have to write:
-
-file.Have("file.txt", file.WithOwner("paul"))
-
-instead, have:
-
-file.Have("file.txt", WithOwner("paul"))
-
-and have all With... methods in a separate package which we import directly with *.
-
-for this, we need to make the With... functions more generic, based on interfaces and we have here for example
-
-type Owner interface {
- SetOwner(owner string)
-}
-
-and if there is no interface match, error!
# TODO — Features needed to replace the dotfiles Rexfile
This document lists the features `gonf` needs before it can replace the
diff --git a/examples/examples.go b/examples/examples.go
index 6b97ab3..b350576 100644
--- a/examples/examples.go
+++ b/examples/examples.go
@@ -6,42 +6,44 @@ import (
"codeberg.org/snonux/gonf/internal/resource/dir"
"codeberg.org/snonux/gonf/internal/resource/file"
"codeberg.org/snonux/gonf/internal/resource/link"
+ . "codeberg.org/snonux/gonf/internal/resource/opt"
)
func Run() error {
// 1. Regular file with content
- file.Have("/tmp/gonf_hello.txt", file.WithContent("Hello World!"))
+ file.Have("/tmp/gonf_hello.txt", WithContent("Hello World!"))
// 2. Regular file from a source template
- file.Have("/tmp/gonf_example.conf", file.WithSource("assets/testfiles/test.tmpl"))
+ file.Have("/tmp/gonf_example.conf", WithSource("assets/testfiles/test.tmpl"))
// 3. Regular file with specific mode and owner
file.Have(
"/tmp/gonf_secret.txt",
- file.WithContent("top secret"),
- file.WithMode(0o600),
+ WithContent("top secret"),
+ WithMode(0o600),
)
// 4. A directory
- dir.Have("/tmp/gonf_dir", dir.WithMode(0o755))
+ dir.Have("/tmp/gonf_dir", WithMode(0o755))
// 5. A symlink
- link.Have("/tmp/gonf_link", link.IsSymlink("/tmp/gonf_hello.txt"))
+ link.Have("/tmp/gonf_link", WithSymlink("/tmp/gonf_hello.txt"))
// 6. A hardlink
- link.Have("/tmp/gonf_hardlink", link.IsHardlink("/tmp/gonf_hello.txt"))
+ link.Have("/tmp/gonf_hardlink", WithHardlink("/tmp/gonf_hello.txt"))
- // 7. Ensuring something is absent
- file.Have("/tmp/gonf_old.txt", file.IsAbsent())
- file.Absent("/tmp/gonf_old.txt") // Alternative way
+ // 7. Ensuring something is absent (two equivalent styles; each resource
+ // path may only be declared once per run, so they use distinct paths)
+ file.Have("/tmp/gonf_old.txt", IsAbsent())
+ file.Absent("/tmp/gonf_old_alt.txt") // Alternative way
// 8. A directory tree copied from source, reconciled, with a distinct
// file mode from the directory's own mode
dir.Have(
"/tmp/gonf_dir_from_source",
- dir.WithSource("assets/testfiles"),
- dir.WithPrune(),
- dir.WithFileMode(0o644),
+ WithSource("assets/testfiles"),
+ WithPrune(),
+ WithFileMode(0o644),
)
// 9. Recursively removing a directory tree. Each resource path can only
@@ -49,21 +51,23 @@ func Run() error {
// (rather than reusing #8's path) to give WithPrune's recursive removal
// something real to demonstrate.
_ = os.MkdirAll("/tmp/gonf_stale_dir/nested", 0o755)
- dir.Have("/tmp/gonf_stale_dir", dir.IsAbsent(), dir.WithPrune())
- dir.Absent("/tmp/gonf_stale_dir", dir.WithPrune()) // Alternative way
+ dir.Have("/tmp/gonf_stale_dir", IsAbsent(), WithPrune())
+ _ = os.MkdirAll("/tmp/gonf_stale_dir_alt/nested", 0o755)
+ dir.Absent("/tmp/gonf_stale_dir_alt", WithPrune()) // Alternative way
// 10. Non-recursively removing an empty directory
_ = os.Mkdir("/tmp/gonf_stale_empty_dir", 0o755)
- dir.Have("/tmp/gonf_stale_empty_dir", dir.IsAbsent())
+ dir.Have("/tmp/gonf_stale_empty_dir", IsAbsent())
// 11. Ensuring a symlink is absent
_ = os.Symlink("/tmp/gonf_hello.txt", "/tmp/gonf_stale_link")
- link.Have("/tmp/gonf_stale_link", link.IsAbsent())
- link.Absent("/tmp/gonf_stale_link") // Alternative way
+ link.Have("/tmp/gonf_stale_link", IsAbsent())
+ _ = os.Symlink("/tmp/gonf_hello.txt", "/tmp/gonf_stale_link_alt")
+ link.Absent("/tmp/gonf_stale_link_alt") // Alternative way
// 12. Ensuring a hardlink is absent
_ = os.Link("/tmp/gonf_hello.txt", "/tmp/gonf_stale_hardlink")
- link.Have("/tmp/gonf_stale_hardlink", link.IsAbsent())
+ link.Have("/tmp/gonf_stale_hardlink", IsAbsent())
return nil
}
diff --git a/internal/resource/dir/dir.go b/internal/resource/dir/dir.go
index 9c34486..8be4395 100644
--- a/internal/resource/dir/dir.go
+++ b/internal/resource/dir/dir.go
@@ -8,6 +8,7 @@ import (
"strconv"
"codeberg.org/snonux/gonf/internal/resource"
+ "codeberg.org/snonux/gonf/internal/resource/opt"
)
type Dir struct {
@@ -21,51 +22,28 @@ type Dir struct {
absent bool
}
-type Option func(*Dir)
+// SetSource implements opt.Sourced.
+func (d *Dir) SetSource(source string) { d.source = source }
-func WithSource(source string) Option {
- return func(d *Dir) {
- d.source = source
- }
-}
+// SetOwner implements opt.Owner.
+func (d *Dir) SetOwner(user string) { d.user = user }
-func WithUser(user string) Option {
- return func(d *Dir) {
- d.user = user
- }
-}
+// SetGroup implements opt.Grouped.
+func (d *Dir) SetGroup(group string) { d.group = group }
-func WithGroup(group string) Option {
- return func(d *Dir) {
- d.group = group
- }
-}
+// SetMode implements opt.Moded (the directory's own mode).
+func (d *Dir) SetMode(mode os.FileMode) { d.mode = mode }
-func WithMode(mode os.FileMode) Option {
- return func(d *Dir) {
- d.mode = mode
- }
-}
+// SetFileMode implements opt.FileModed (mode for files copied from source).
+func (d *Dir) SetFileMode(mode os.FileMode) { d.fileMode = mode }
-func WithFileMode(mode os.FileMode) Option {
- return func(d *Dir) {
- d.fileMode = mode
- }
-}
+// SetPrune implements opt.Prunable.
+func (d *Dir) SetPrune() { d.prune = true }
-func WithPrune() Option {
- return func(d *Dir) {
- d.prune = true
- }
-}
-
-func IsAbsent() Option {
- return func(d *Dir) {
- d.absent = true
- }
-}
+// SetAbsent implements opt.Absentable.
+func (d *Dir) SetAbsent() { d.absent = true }
-func build(path string, opts ...Option) (*Dir, error) {
+func build(path string, opts ...opt.Option) (*Dir, error) {
curr, err := user.Current()
if err != nil {
return nil, fmt.Errorf("failed to get current user for default: %w", err)
@@ -79,8 +57,8 @@ func build(path string, opts ...Option) (*Dir, error) {
group: curr.Gid,
}
- for _, opt := range opts {
- opt(d)
+ for _, o := range opts {
+ o(d)
}
return d, nil
@@ -200,7 +178,7 @@ func applyAttributesTo(path string, mode os.FileMode, usr, group string) error {
// Ensure builds and applies the directory resource described by opts,
// without registering it.
-func Ensure(path string, opts ...Option) error {
+func Ensure(path string, opts ...opt.Option) error {
d, err := build(path, opts...)
if err != nil {
return err
@@ -208,7 +186,7 @@ func Ensure(path string, opts ...Option) error {
return d.apply()
}
-func Have(path string, opts ...Option) resource.Resource {
+func Have(path string, opts ...opt.Option) resource.Resource {
d, err := build(path, opts...)
if err != nil {
log.Fatalf("failed to apply directory resource %s: %v", path, err)
@@ -223,7 +201,7 @@ func Have(path string, opts ...Option) resource.Resource {
return res
}
-func Absent(path string, opts ...Option) resource.Resource {
- opts = append(opts, IsAbsent())
+func Absent(path string, opts ...opt.Option) resource.Resource {
+ opts = append(opts, opt.IsAbsent())
return Have(path, opts...)
}
diff --git a/internal/resource/dir/dir_test.go b/internal/resource/dir/dir_test.go
index ca01535..5ffb5cf 100644
--- a/internal/resource/dir/dir_test.go
+++ b/internal/resource/dir/dir_test.go
@@ -6,6 +6,7 @@ import (
"testing"
"codeberg.org/snonux/gonf/internal/resource/file"
+ . "codeberg.org/snonux/gonf/internal/resource/opt"
)
func TestHaveDirectoryCreate(t *testing.T) {
@@ -296,7 +297,7 @@ func TestSourceCopyParamMatchesSingleFilePath(t *testing.T) {
}
singleTarget := filepath.Join(tmp, "single.conf")
- if err := file.Ensure(singleTarget, file.WithSource(sourcePath)); err != nil {
+ if err := file.Ensure(singleTarget, WithSource(sourcePath)); err != nil {
t.Fatal(err)
}
viaFile, err := os.ReadFile(singleTarget)
diff --git a/internal/resource/dir/source.go b/internal/resource/dir/source.go
index 8f9b73a..2e5ed98 100644
--- a/internal/resource/dir/source.go
+++ b/internal/resource/dir/source.go
@@ -9,6 +9,7 @@ import (
"codeberg.org/snonux/gonf/internal/resource/file"
"codeberg.org/snonux/gonf/internal/resource/link"
+ "codeberg.org/snonux/gonf/internal/resource/opt"
)
// copySourceTree mirrors d.source into d.path, dispatching each entry by
@@ -63,7 +64,7 @@ func copySourceSymlink(sourcePath, target string) error {
if err != nil {
return fmt.Errorf("failed to read symlink %s: %w", sourcePath, err)
}
- return link.Ensure(target, link.IsSymlink(rawTarget))
+ return link.Ensure(target, opt.WithSymlink(rawTarget))
}
// copySourceFile delegates writing a single copied file to the file
@@ -73,10 +74,10 @@ func copySourceSymlink(sourcePath, target string) error {
// dir needs no special-casing of its own.
func copySourceFile(d *Dir, sourcePath, target string) error {
return file.Ensure(target,
- file.WithSource(sourcePath),
- file.WithMode(d.fileMode),
- file.WithUser(d.user),
- file.WithGroup(d.group),
+ opt.WithSource(sourcePath),
+ opt.WithMode(d.fileMode),
+ opt.WithOwner(d.user),
+ opt.WithGroup(d.group),
)
}
diff --git a/internal/resource/file/file.go b/internal/resource/file/file.go
index 257c407..6395403 100644
--- a/internal/resource/file/file.go
+++ b/internal/resource/file/file.go
@@ -11,6 +11,7 @@ import (
"text/template"
"codeberg.org/snonux/gonf/internal/resource"
+ "codeberg.org/snonux/gonf/internal/resource/opt"
)
type File struct {
@@ -23,47 +24,33 @@ type File struct {
absent bool
}
-type Option func(*File)
-
-func WithContent(content string) Option {
- return func(f *File) {
- f.content = content
- f.source = ""
- }
+// SetContent implements opt.Contented. Setting literal content clears any
+// previously configured source, as the two are mutually exclusive.
+func (f *File) SetContent(content string) {
+ f.content = content
+ f.source = ""
}
-func WithSource(source string) Option {
- return func(f *File) {
- f.source = source
- f.content = ""
- }
+// SetSource implements opt.Sourced. Setting a source clears any previously
+// configured literal content, as the two are mutually exclusive.
+func (f *File) SetSource(source string) {
+ f.source = source
+ f.content = ""
}
-func WithUser(user string) Option {
- return func(f *File) {
- f.user = user
- }
-}
+// SetOwner implements opt.Owner.
+func (f *File) SetOwner(user string) { f.user = user }
-func WithGroup(group string) Option {
- return func(f *File) {
- f.group = group
- }
-}
+// SetGroup implements opt.Grouped.
+func (f *File) SetGroup(group string) { f.group = group }
-func WithMode(mode os.FileMode) Option {
- return func(f *File) {
- f.mode = mode
- }
-}
+// SetMode implements opt.Moded.
+func (f *File) SetMode(mode os.FileMode) { f.mode = mode }
-func IsAbsent() Option {
- return func(f *File) {
- f.absent = true
- }
-}
+// SetAbsent implements opt.Absentable.
+func (f *File) SetAbsent() { f.absent = true }
-func build(path string, opts ...Option) (*File, error) {
+func build(path string, opts ...opt.Option) (*File, error) {
curr, err := user.Current()
if err != nil {
return nil, fmt.Errorf("failed to get current user for default: %w", err)
@@ -76,8 +63,8 @@ func build(path string, opts ...Option) (*File, error) {
group: curr.Gid,
}
- for _, opt := range opts {
- opt(f)
+ for _, o := range opts {
+ o(f)
}
return f, nil
@@ -222,7 +209,7 @@ func ensureAbsent(path string) error {
// Ensure builds and applies the file resource described by opts, without
// registering it. Used by other resource packages (e.g. dir) to write an
// individual file without it becoming its own top-level resource.
-func Ensure(path string, opts ...Option) error {
+func Ensure(path string, opts ...opt.Option) error {
f, err := build(path, opts...)
if err != nil {
return err
@@ -230,7 +217,7 @@ func Ensure(path string, opts ...Option) error {
return f.apply()
}
-func Have(path string, opts ...Option) resource.Resource {
+func Have(path string, opts ...opt.Option) resource.Resource {
f, err := build(path, opts...)
if err != nil {
log.Fatalf("failed to apply file resource %s: %v", path, err)
@@ -245,7 +232,7 @@ func Have(path string, opts ...Option) resource.Resource {
return res
}
-func Absent(path string, opts ...Option) resource.Resource {
- opts = append(opts, IsAbsent())
+func Absent(path string, opts ...opt.Option) resource.Resource {
+ opts = append(opts, opt.IsAbsent())
return Have(path, opts...)
}
diff --git a/internal/resource/file/file_test.go b/internal/resource/file/file_test.go
index b3b7711..49deb4c 100644
--- a/internal/resource/file/file_test.go
+++ b/internal/resource/file/file_test.go
@@ -5,6 +5,8 @@ import (
"path/filepath"
"strings"
"testing"
+
+ . "codeberg.org/snonux/gonf/internal/resource/opt"
)
func TestGetChecksum(t *testing.T) {
diff --git a/internal/resource/link/link.go b/internal/resource/link/link.go
index 3133514..f9c6c8d 100644
--- a/internal/resource/link/link.go
+++ b/internal/resource/link/link.go
@@ -6,6 +6,7 @@ import (
"os"
"codeberg.org/snonux/gonf/internal/resource"
+ "codeberg.org/snonux/gonf/internal/resource/opt"
)
type kind int
@@ -23,32 +24,25 @@ type Link struct {
absent bool
}
-type Option func(*Link)
-
-func IsSymlink(target string) Option {
- return func(l *Link) {
- l.kind = symlinkKind
- l.target = target
- }
+// SetSymlink implements opt.Linkable.
+func (l *Link) SetSymlink(target string) {
+ l.kind = symlinkKind
+ l.target = target
}
-func IsHardlink(target string) Option {
- return func(l *Link) {
- l.kind = hardlinkKind
- l.target = target
- }
+// SetHardlink implements opt.Linkable.
+func (l *Link) SetHardlink(target string) {
+ l.kind = hardlinkKind
+ l.target = target
}
-func IsAbsent() Option {
- return func(l *Link) {
- l.absent = true
- }
-}
+// SetAbsent implements opt.Absentable.
+func (l *Link) SetAbsent() { l.absent = true }
-func build(path string, opts ...Option) *Link {
+func build(path string, opts ...opt.Option) *Link {
l := &Link{path: path}
- for _, opt := range opts {
- opt(l)
+ for _, o := range opts {
+ o(l)
}
return l
}
@@ -87,11 +81,11 @@ func (l *Link) resourceType() string {
// Ensure builds and applies the link resource described by opts, without
// registering it. Used by other resource packages (e.g. dir) to recreate an
// individual symlink without it becoming its own top-level resource.
-func Ensure(path string, opts ...Option) error {
+func Ensure(path string, opts ...opt.Option) error {
return build(path, opts...).apply()
}
-func Have(path string, opts ...Option) resource.Resource {
+func Have(path string, opts ...opt.Option) resource.Resource {
l := build(path, opts...)
res := resource.Register(l.resourceType(), l.path)
@@ -102,8 +96,8 @@ func Have(path string, opts ...Option) resource.Resource {
return res
}
-func Absent(path string, opts ...Option) resource.Resource {
- opts = append(opts, IsAbsent())
+func Absent(path string, opts ...opt.Option) resource.Resource {
+ opts = append(opts, opt.IsAbsent())
return Have(path, opts...)
}
diff --git a/internal/resource/link/link_test.go b/internal/resource/link/link_test.go
index 6f1b1bc..a892527 100644
--- a/internal/resource/link/link_test.go
+++ b/internal/resource/link/link_test.go
@@ -4,6 +4,8 @@ import (
"os"
"path/filepath"
"testing"
+
+ . "codeberg.org/snonux/gonf/internal/resource/opt"
)
func TestHaveSymlinkCreateAndIdempotent(t *testing.T) {
@@ -14,7 +16,7 @@ func TestHaveSymlinkCreateAndIdempotent(t *testing.T) {
t.Fatal(err)
}
- Have(link, IsSymlink(target))
+ Have(link, WithSymlink(target))
got, err := os.Readlink(link)
if err != nil {
t.Fatalf("readlink: %v", err)
@@ -45,7 +47,7 @@ func TestHaveSymlinkRepoints(t *testing.T) {
t.Fatal(err)
}
- Have(link, IsSymlink(newT))
+ Have(link, WithSymlink(newT))
got, err := os.Readlink(link)
if err != nil {
t.Fatal(err)
@@ -66,7 +68,7 @@ func TestHaveSymlinkMovesRealFileAside(t *testing.T) {
t.Fatal(err)
}
- Have(link, IsSymlink(target))
+ Have(link, WithSymlink(target))
got, err := os.Readlink(link)
if err != nil {
@@ -88,7 +90,7 @@ func TestHaveHardlinkCreateAndIdempotent(t *testing.T) {
t.Fatal(err)
}
- Have(link, IsHardlink(target))
+ Have(link, WithHardlink(target))
ti, err := os.Stat(target)
if err != nil {
@@ -121,7 +123,7 @@ func TestHaveHardlinkMovesRealFileAside(t *testing.T) {
t.Fatal(err)
}
- Have(link, IsHardlink(target))
+ Have(link, WithHardlink(target))
ti, err := os.Stat(target)
if err != nil {
@@ -142,7 +144,7 @@ func TestHaveHardlinkMovesRealFileAside(t *testing.T) {
func TestHaveHardlinkMissingTarget(t *testing.T) {
dir := t.TempDir()
link := filepath.Join(dir, "link")
- if err := Ensure(link, IsHardlink(filepath.Join(dir, "nope"))); err == nil {
+ if err := Ensure(link, WithHardlink(filepath.Join(dir, "nope"))); err == nil {
t.Error("expected error when hardlink target does not exist")
}
}
diff --git a/internal/resource/opt/opt.go b/internal/resource/opt/opt.go
new file mode 100644
index 0000000..23f076d
--- /dev/null
+++ b/internal/resource/opt/opt.go
@@ -0,0 +1,156 @@
+// Package opt provides interface-based, resource-agnostic configuration
+// options shared by the file, dir, and link resource packages.
+//
+// It is designed to be dot-imported at call sites so options read as bare
+// WithMode(...) / WithOwner(...) calls regardless of the resource type:
+//
+// import (
+// "codeberg.org/snonux/gonf/internal/resource/file"
+// . "codeberg.org/snonux/gonf/internal/resource/opt"
+// )
+//
+// file.Have("file.txt", WithOwner("paul"), WithMode(0o755))
+//
+// Each option targets a small capability interface (Owner, Moded, ...). A
+// resource implements only the setters it supports. Applying an option to a
+// resource that lacks the matching interface (e.g. WithPrune on a file) is a
+// programming error in the resource declaration and aborts the program via
+// log.Fatalf.
+package opt
+
+import (
+ "log"
+ "os"
+)
+
+// Option configures a resource. It is applied to the concrete resource value
+// (e.g. *file.File) during construction.
+type Option func(any)
+
+// Capability interfaces. A resource implements only the setters it supports.
+type (
+ Owner interface{ SetOwner(string) }
+ Grouped interface{ SetGroup(string) }
+ Moded interface{ SetMode(os.FileMode) }
+ Sourced interface{ SetSource(string) }
+ Contented interface{ SetContent(string) }
+ FileModed interface{ SetFileMode(os.FileMode) }
+ Prunable interface{ SetPrune() }
+ Absentable interface{ SetAbsent() }
+ Linkable interface {
+ SetSymlink(target string)
+ SetHardlink(target string)
+ }
+)
+
+// WithOwner sets the owning user of the resource.
+func WithOwner(owner string) Option {
+ return func(t any) {
+ r, ok := t.(Owner)
+ if !ok {
+ log.Fatalf("%T does not support WithOwner", t)
+ }
+ r.SetOwner(owner)
+ }
+}
+
+// WithGroup sets the owning group of the resource.
+func WithGroup(group string) Option {
+ return func(t any) {
+ r, ok := t.(Grouped)
+ if !ok {
+ log.Fatalf("%T does not support WithGroup", t)
+ }
+ r.SetGroup(group)
+ }
+}
+
+// WithMode sets the resource's own file mode.
+func WithMode(mode os.FileMode) Option {
+ return func(t any) {
+ r, ok := t.(Moded)
+ if !ok {
+ log.Fatalf("%T does not support WithMode", t)
+ }
+ r.SetMode(mode)
+ }
+}
+
+// WithSource sets the source path the resource is populated from.
+func WithSource(source string) Option {
+ return func(t any) {
+ r, ok := t.(Sourced)
+ if !ok {
+ log.Fatalf("%T does not support WithSource", t)
+ }
+ r.SetSource(source)
+ }
+}
+
+// WithContent sets literal content for the resource.
+func WithContent(content string) Option {
+ return func(t any) {
+ r, ok := t.(Contented)
+ if !ok {
+ log.Fatalf("%T does not support WithContent", t)
+ }
+ r.SetContent(content)
+ }
+}
+
+// WithFileMode sets the mode applied to regular files copied from a source
+// tree (distinct from the resource's own mode).
+func WithFileMode(mode os.FileMode) Option {
+ return func(t any) {
+ r, ok := t.(FileModed)
+ if !ok {
+ log.Fatalf("%T does not support WithFileMode", t)
+ }
+ r.SetFileMode(mode)
+ }
+}
+
+// WithPrune enables reconciliation of extra destination entries during a
+// source copy, and recursive removal during IsAbsent().
+func WithPrune() Option {
+ return func(t any) {
+ r, ok := t.(Prunable)
+ if !ok {
+ log.Fatalf("%T does not support WithPrune", t)
+ }
+ r.SetPrune()
+ }
+}
+
+// IsAbsent marks the resource for removal.
+func IsAbsent() Option {
+ return func(t any) {
+ r, ok := t.(Absentable)
+ if !ok {
+ log.Fatalf("%T does not support IsAbsent", t)
+ }
+ r.SetAbsent()
+ }
+}
+
+// WithSymlink makes the resource a symbolic link pointing at target.
+func WithSymlink(target string) Option {
+ return func(t any) {
+ r, ok := t.(Linkable)
+ if !ok {
+ log.Fatalf("%T does not support WithSymlink", t)
+ }
+ r.SetSymlink(target)
+ }
+}
+
+// WithHardlink makes the resource a hard link pointing at target.
+func WithHardlink(target string) Option {
+ return func(t any) {
+ r, ok := t.(Linkable)
+ if !ok {
+ log.Fatalf("%T does not support WithHardlink", t)
+ }
+ r.SetHardlink(target)
+ }
+}