summaryrefslogtreecommitdiff
path: root/internal/file/hardlink.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/file/hardlink.go')
-rw-r--r--internal/file/hardlink.go59
1 files changed, 59 insertions, 0 deletions
diff --git a/internal/file/hardlink.go b/internal/file/hardlink.go
new file mode 100644
index 0000000..8913674
--- /dev/null
+++ b/internal/file/hardlink.go
@@ -0,0 +1,59 @@
+package file
+
+import (
+ "fmt"
+ "log"
+ "os"
+ "syscall"
+)
+
+// haveHardlink ensures f.path is a hard link to f.hardlinkTarget.
+//
+// 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 (f *File) haveHardlink() error {
+ log.Printf("processing hardlink: %s -> %s", f.path, f.hardlinkTarget)
+
+ if f.hardlinkTarget == "" {
+ return fmt.Errorf("hardlink %s has no target", f.path)
+ }
+
+ targetInfo, err := os.Stat(f.hardlinkTarget)
+ if err != nil {
+ return fmt.Errorf("failed to stat hardlink target %s: %w", f.hardlinkTarget, err)
+ }
+
+ if info, err := os.Lstat(f.path); err == nil {
+ if sameInode(info, targetInfo) {
+ log.Printf("hardlink %s already links to %s", f.path, f.hardlinkTarget)
+ return nil
+ }
+ old := f.path + ".old"
+ log.Printf("%s already exists, renaming to %s", f.path, old)
+ if err := os.Rename(f.path, old); err != nil {
+ return fmt.Errorf("failed to move existing %s aside: %w", f.path, err)
+ }
+ } else if !os.IsNotExist(err) {
+ return fmt.Errorf("failed to stat %s: %w", f.path, err)
+ }
+
+ if err := os.Link(f.hardlinkTarget, f.path); err != nil {
+ return fmt.Errorf("failed to create hardlink %s -> %s: %w", f.path, f.hardlinkTarget, err)
+ }
+
+ log.Printf("created hardlink %s -> %s", f.path, f.hardlinkTarget)
+ 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
+}