diff options
Diffstat (limited to 'internal/scan')
| -rw-r--r-- | internal/scan/fileid_other.go | 10 | ||||
| -rw-r--r-- | internal/scan/fileid_unix.go | 19 | ||||
| -rw-r--r-- | internal/scan/scan.go | 29 | ||||
| -rw-r--r-- | internal/scan/scan_test.go | 26 |
4 files changed, 76 insertions, 8 deletions
diff --git a/internal/scan/fileid_other.go b/internal/scan/fileid_other.go new file mode 100644 index 0000000..503a9b4 --- /dev/null +++ b/internal/scan/fileid_other.go @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +//go:build !unix + +package scan + +import "io/fs" + +// fileID reports no identity where the platform has no inodes. +func fileID(fs.FileInfo) (dev, ino uint64) { return 0, 0 } diff --git a/internal/scan/fileid_unix.go b/internal/scan/fileid_unix.go new file mode 100644 index 0000000..e29066a --- /dev/null +++ b/internal/scan/fileid_unix.go @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +//go:build unix + +package scan + +import ( + "io/fs" + "syscall" +) + +// fileID returns the device and inode info was read from. +func fileID(info fs.FileInfo) (dev, ino uint64) { + st, ok := info.Sys().(*syscall.Stat_t) + if !ok { + return 0, 0 + } + return uint64(st.Dev), uint64(st.Ino) +} diff --git a/internal/scan/scan.go b/internal/scan/scan.go index 7fe9e92..77cca66 100644 --- a/internal/scan/scan.go +++ b/internal/scan/scan.go @@ -24,6 +24,26 @@ type File struct { Size int64 ModTime time.Time Mode fs.FileMode + + // Dev and Ino identify the file on its filesystem; both are 0 where the + // platform reports neither. + Dev, Ino uint64 +} + +// NewFile builds the File for path, at rel under the root, from its Lstat +// info. +func NewFile(path, rel string, info fs.FileInfo) File { + dev, ino := fileID(info) + return File{ + Path: path, + Rel: rel, + Name: filepath.Base(path), + Size: info.Size(), + ModTime: info.ModTime(), + Mode: info.Mode(), + Dev: dev, + Ino: ino, + } } // Reason is why an entry was not returned as a File. @@ -217,14 +237,7 @@ func (w *walker) walk(dir, relDir string, depth int, entries []os.DirEntry) erro w.result.Skipped = append(w.result.Skipped, Skipped{Rel: rel, Reason: TooBig}) continue } - w.result.Files = append(w.result.Files, File{ - Path: path, - Rel: rel, - Name: name, - Size: info.Size(), - ModTime: info.ModTime(), - Mode: info.Mode(), - }) + w.result.Files = append(w.result.Files, NewFile(path, rel, info)) } return nil } diff --git a/internal/scan/scan_test.go b/internal/scan/scan_test.go index a245893..e9aeb73 100644 --- a/internal/scan/scan_test.go +++ b/internal/scan/scan_test.go @@ -293,3 +293,29 @@ func TestTooBig(t *testing.T) { t.Errorf("MaxSize 0 skipped files: %v", skipped(r)) } } + +// TestFilesCarryInode: a walked file carries its device and inode, which a +// rename keeps. +func TestFilesCarryInode(t *testing.T) { + root := tree(t) + p := filepath.Join(root, "a.txt") + if err := os.WriteFile(p, []byte("x"), 0o644); err != nil { + t.Fatal(err) + } + old := now.Add(-time.Hour) + os.Chtimes(p, old, old) + first, err := Walk(root, Options{Now: now}) + if err != nil || len(first.Files) != 1 { + t.Fatalf("walk: %v %+v", err, first) + } + if first.Files[0].Ino == 0 { + t.Fatal("no inode on a unix filesystem") + } + if err := os.Rename(p, filepath.Join(root, "b.txt")); err != nil { + t.Fatal(err) + } + second, _ := Walk(root, Options{Now: now}) + if len(second.Files) != 1 || second.Files[0].Ino != first.Files[0].Ino || second.Files[0].Dev != first.Files[0].Dev { + t.Errorf("rename changed the identity: %+v then %+v", first.Files[0], second.Files) + } +} |
