From 3b951cf9e5521db2c3bc7c2b76aaa3b801cd13e9 Mon Sep 17 00:00:00 2001 From: Michael Woolnough <130465766+mjkw31@users.noreply.github.com> Date: Tue, 17 Dec 2024 12:53:58 +0000 Subject: [PATCH] Generate 'fake' directory entries for deleted directories. (#119) * Output 'fake' entries for non-existent directories to maintain the tree for files in those dirs that may have been stat'd succesfully before the deletion --- stat/paths_test.go | 45 +++++++++++++++++++++++++++++++++++++++++++++ stat/stat.go | 37 +++++++++++++++++++++++++++++++++++++ 2 files changed, 82 insertions(+) diff --git a/stat/paths_test.go b/stat/paths_test.go index f635b993..20f9488c 100644 --- a/stat/paths_test.go +++ b/stat/paths_test.go @@ -26,6 +26,7 @@ package stat import ( + "fmt" "io" "io/fs" "os" @@ -208,6 +209,50 @@ func TestPaths(t *testing.T) { So(string(output), ShouldContainSubstring, "\t1\t") So(string(output), ShouldContainSubstring, "\tf\t") }) + + Convey("can stat files and non-existent directories", func() { + dir := t.TempDir() + + existingDir := filepath.Join(dir, "existingDir") + "/" + notExistingDir := filepath.Join(dir, "notExistingDir") + "/" + existingFile := filepath.Join(dir, "existingFile") + + err := os.Mkdir(existingDir, 0755) + So(err, ShouldBeNil) + + f, err := os.Create(existingFile) + So(err, ShouldBeNil) + + _, err = f.Write([]byte{0}) + So(err, ShouldBeNil) + + err = f.Close() + So(err, ShouldBeNil) + + r := strings.NewReader(strconv.Quote(existingDir) + "\n" + + strconv.Quote(notExistingDir) + "\n" + + strconv.Quote(filepath.Join(dir, "notExistingFile")) + "\n" + + strconv.Quote(existingFile) + "\n") + + stats := make([]string, 0, 3) + + err = p.AddOperation("file", func(absPath string, info fs.FileInfo) error { + stats = append(stats, fmt.Sprintf("%s\t%d", absPath, info.Size())) + + return nil + }) + + So(err, ShouldBeNil) + + err = p.Scan(r) + So(err, ShouldBeNil) + + So(stats, ShouldResemble, []string{ + existingDir + "\t4096", + notExistingDir + "\t0", + existingFile + "\t1", + }) + }) }) } diff --git a/stat/stat.go b/stat/stat.go index acf7fb67..40835119 100644 --- a/stat/stat.go +++ b/stat/stat.go @@ -26,8 +26,11 @@ package stat import ( + "errors" "io/fs" "os" + "path/filepath" + "strings" "syscall" "time" @@ -132,9 +135,43 @@ func (s *StatterWithTimeout) Lstat(path string) (info fs.FileInfo, err error) { } } +type fakeDir struct { + name string + syscall.Stat_t +} + +func (f *fakeDir) Name() string { + return f.name +} + +func (fakeDir) Size() int64 { + return 0 +} + +func (fakeDir) ModTime() time.Time { + return time.Time{} +} + +func (fakeDir) Mode() fs.FileMode { + return 0 +} + +func (fakeDir) IsDir() bool { + return true +} + +func (f *fakeDir) Sys() any { + return &f.Stat_t +} + // doLstat does the actual Lstat call and sends results on the given channels. func (s *StatterWithTimeout) doLstat(path string, infoCh chan fs.FileInfo, errCh chan error) { info, err := s.lstat(path) + if errors.Is(err, fs.ErrNotExist) && strings.HasSuffix(path, "/") { + err = nil + info = &fakeDir{name: filepath.Base(path)} + } + if err == nil { stat, ok := info.Sys().(*syscall.Stat_t) if ok {