summaryrefslogtreecommitdiff
path: root/internal/resource/link/hardlink.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/resource/link/hardlink.go')
-rw-r--r--internal/resource/link/hardlink.go59
1 files changed, 59 insertions, 0 deletions
diff --git a/internal/resource/link/hardlink.go b/internal/resource/link/hardlink.go
new file mode 100644
index 0000000..7307344
--- /dev/null
+++ b/internal/resource/link/hardlink.go
@@ -0,0 +1,59 @@
+package link
+
+import (
+ "fmt"
+ "log"
+ "os"
+ "syscall"
+)
+
+// ensureHardlink ensures l.path is a hard link to l.target.
+//
+// Idempotency and clobber policy:
+// - already the same inode as the target: nothing to do.
+// - a different file/link is in the way: it is renamed to "<path>.old"
+// before the link is created (matching the symlink resource's behavior).
+func ensureHardlink(l *Link) error {
+ log.Printf("processing hardlink: %s -> %s", l.path, l.target)
+
+ if l.target == "" {
+ return fmt.Errorf("hardlink %s has no target", l.path)
+ }
+
+ targetInfo, err := os.Stat(l.target)
+ if err != nil {
+ return fmt.Errorf("failed to stat hardlink target %s: %w", l.target, err)
+ }
+
+ if info, err := os.Lstat(l.path); err == nil {
+ if sameInode(info, targetInfo) {
+ log.Printf("hardlink %s already links to %s", l.path, l.target)
+ return nil
+ }
+ old := l.path + ".old"
+ log.Printf("%s already exists, renaming to %s", l.path, old)
+ if err := os.Rename(l.path, old); err != nil {
+ return fmt.Errorf("failed to move existing %s aside: %w", l.path, err)
+ }
+ } else if !os.IsNotExist(err) {
+ return fmt.Errorf("failed to stat %s: %w", l.path, err)
+ }
+
+ if err := os.Link(l.target, l.path); err != nil {
+ return fmt.Errorf("failed to create hardlink %s -> %s: %w", l.path, l.target, err)
+ }
+
+ log.Printf("created hardlink %s -> %s", l.path, l.target)
+ return nil
+}
+
+// sameInode reports whether two FileInfos refer to the same underlying inode
+// (same device and inode number), i.e. they are already hard-linked.
+func sameInode(a, b os.FileInfo) bool {
+ as, aok := a.Sys().(*syscall.Stat_t)
+ bs, bok := b.Sys().(*syscall.Stat_t)
+ if !aok || !bok {
+ return false
+ }
+ return as.Dev == bs.Dev && as.Ino == bs.Ino
+}