diff options
author | Daniel Milde <daniel@milde.cz> | 2023-02-05 23:16:50 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-02-05 23:16:50 +0100 |
commit | 5b9d8fd893819714fac85ee258c1dc218ab920fd (patch) | |
tree | a44c4dda0bf1542d38d8d7f783694322ea663499 | |
parent | 8bdce9a33d2e7e7bbe127dcdceee78237070c19f (diff) |
feat: added option to follow symlinks (#206)
* feat: follow symlinks in the analyzer
* feat: set followSymlinks
* added follow-symlinks parameter
* test for following symlinks
* test broken symlink
* refactor
* better doc
* test SetFollowSymlinks
* refactor
* readme updated
-rw-r--r-- | README.md | 1 | ||||
-rw-r--r-- | cmd/gdu/app/app.go | 12 | ||||
-rw-r--r-- | cmd/gdu/app/app_test.go | 15 | ||||
-rw-r--r-- | cmd/gdu/main.go | 4 | ||||
-rw-r--r-- | internal/common/analyze.go | 1 | ||||
-rw-r--r-- | internal/common/ui.go | 5 | ||||
-rw-r--r-- | internal/common/ui_test.go | 41 | ||||
-rw-r--r-- | internal/testanalyze/analyze.go | 5 | ||||
-rw-r--r-- | pkg/analyze/dir.go | 33 | ||||
-rw-r--r-- | pkg/analyze/dir_test.go | 58 |
10 files changed, 169 insertions, 6 deletions
@@ -74,6 +74,7 @@ Flags: --config-file string Read config from file (default is $HOME/.gdu.yaml) -g, --const-gc Enable memory garbage collection during analysis with constant level set by GOGC --enable-profiling Enable collection of profiling data and provide it on http://localhost:6060/debug/pprof/ + -L, --follow-symlinks Follow symlinks for files, i.e. show the size of the file to which symlink points to (symlinks to directories are not followed) -h, --help help for gdu -i, --ignore-dirs strings Absolute paths to ignore (separated by comma) (default [/proc,/dev,/sys,/run]) -I, --ignore-dirs-pattern strings Absolute path patterns to ignore (separated by comma) diff --git a/cmd/gdu/app/app.go b/cmd/gdu/app/app.go index bd6440f..63afcd8 100644 --- a/cmd/gdu/app/app.go +++ b/cmd/gdu/app/app.go @@ -34,6 +34,7 @@ type UI interface { SetIgnoreDirPatterns(paths []string) error SetIgnoreFromFile(ignoreFile string) error SetIgnoreHidden(value bool) + SetFollowSymlinks(value bool) StartUILoop() error } @@ -57,6 +58,7 @@ type Flags struct { NoProgress bool `yaml:"no-progress"` NoCross bool `yaml:"no-cross"` NoHidden bool `yaml:"no-hidden"` + FollowSymlinks bool `yaml:"follow-symlinks"` Profiling bool `yaml:"profiling"` ConstGC bool `yaml:"const-gc"` Summarize bool `yaml:"summarize"` @@ -203,10 +205,7 @@ func (a *App) createUI() (UI, error) { a.Flags.ConstGC, a.Flags.UseSIPrefix, ) - return ui, nil - } - - if a.Flags.NonInteractive || !a.Istty { + } else if a.Flags.NonInteractive || !a.Istty { ui = stdout.CreateStdoutUI( a.Writer, !a.Flags.NoColor && a.Istty, @@ -261,6 +260,11 @@ func (a *App) createUI() (UI, error) { } tview.Styles.BorderColor = tcell.ColorDefault } + + if a.Flags.FollowSymlinks { + ui.SetFollowSymlinks(true) + } + return ui, nil } diff --git a/cmd/gdu/app/app_test.go b/cmd/gdu/app/app_test.go index 746550e..947f453 100644 --- a/cmd/gdu/app/app_test.go +++ b/cmd/gdu/app/app_test.go @@ -47,6 +47,21 @@ func TestAnalyzePath(t *testing.T) { assert.Nil(t, err) } +func TestFollowSymlinks(t *testing.T) { + fin := testdir.CreateTestDir() + defer fin() + + out, err := runApp( + &Flags{LogFile: "/dev/null", FollowSymlinks: true}, + []string{"test_dir"}, + false, + testdev.DevicesInfoGetterMock{}, + ) + + assert.Contains(t, out, "nested") + assert.Nil(t, err) +} + func TestAnalyzePathProfiling(t *testing.T) { fin := testdir.CreateTestDir() defer fin() diff --git a/cmd/gdu/main.go b/cmd/gdu/main.go index b0c9471..5ac5e19 100644 --- a/cmd/gdu/main.go +++ b/cmd/gdu/main.go @@ -49,6 +49,10 @@ func init() { flags.StringSliceVarP(&af.IgnoreDirPatterns, "ignore-dirs-pattern", "I", []string{}, "Absolute path patterns to ignore (separated by comma)") flags.StringVarP(&af.IgnoreFromFile, "ignore-from", "X", "", "Read absolute path patterns to ignore from file") flags.BoolVarP(&af.NoHidden, "no-hidden", "H", false, "Ignore hidden directories (beginning with dot)") + flags.BoolVarP( + &af.FollowSymlinks, "follow-symlinks", "L", false, + "Follow symlinks for files, i.e. show the size of the file to which symlink points to (symlinks to directories are not followed)", + ) flags.BoolVarP(&af.NoCross, "no-cross", "x", false, "Do not cross filesystem boundaries") flags.BoolVarP(&af.ConstGC, "const-gc", "g", false, "Enable memory garbage collection during analysis with constant level set by GOGC") flags.BoolVar(&af.Profiling, "enable-profiling", false, "Enable collection of profiling data and provide it on http://localhost:6060/debug/pprof/") diff --git a/internal/common/analyze.go b/internal/common/analyze.go index 152bd5e..bb08b3c 100644 --- a/internal/common/analyze.go +++ b/internal/common/analyze.go @@ -15,6 +15,7 @@ type ShouldDirBeIgnored func(name, path string) bool // Analyzer is type for dir analyzing function type Analyzer interface { AnalyzeDir(path string, ignore ShouldDirBeIgnored, constGC bool) fs.Item + SetFollowSymlinks(bool) GetProgressChan() chan CurrentProgress GetDone() SignalGroup ResetProgress() diff --git a/internal/common/ui.go b/internal/common/ui.go index 0553bba..d094a67 100644 --- a/internal/common/ui.go +++ b/internal/common/ui.go @@ -19,6 +19,11 @@ type UI struct { ConstGC bool } +// SetFollowSymlinks sets whether symlinks to files should be followed +func (ui *UI) SetFollowSymlinks(v bool) { + ui.Analyzer.SetFollowSymlinks(v) +} + // binary multiplies prefixes (IEC) const ( _ = iota diff --git a/internal/common/ui_test.go b/internal/common/ui_test.go index 3a6c28a..e669a61 100644 --- a/internal/common/ui_test.go +++ b/internal/common/ui_test.go @@ -3,6 +3,7 @@ package common import ( "testing" + "github.com/dundee/gdu/v5/pkg/fs" "github.com/stretchr/testify/assert" ) @@ -10,3 +11,43 @@ func TestFormatNumber(t *testing.T) { res := FormatNumber(1234567890) assert.Equal(t, "1,234,567,890", res) } + +func TestSetFollowSymlinks(t *testing.T) { + ui := UI{ + Analyzer: &MockedAnalyzer{}, + } + ui.SetFollowSymlinks(true) + + assert.Equal(t, true, ui.Analyzer.(*MockedAnalyzer).FollowSymlinks) +} + +type MockedAnalyzer struct { + FollowSymlinks bool +} + +// AnalyzeDir returns dir with files with different size exponents +func (a *MockedAnalyzer) AnalyzeDir( + path string, ignore ShouldDirBeIgnored, enableGC bool, +) fs.Item { + return nil +} + +// GetProgressChan returns always Done +func (a *MockedAnalyzer) GetProgressChan() chan CurrentProgress { + return make(chan CurrentProgress) +} + +// GetDone returns always Done +func (a *MockedAnalyzer) GetDone() SignalGroup { + c := make(SignalGroup) + defer c.Broadcast() + return c +} + +// ResetProgress does nothing +func (a *MockedAnalyzer) ResetProgress() {} + +// SetFollowSymlinks does nothing +func (a *MockedAnalyzer) SetFollowSymlinks(v bool) { + a.FollowSymlinks = v +} diff --git a/internal/testanalyze/analyze.go b/internal/testanalyze/analyze.go index cd61cc7..89ca4cf 100644 --- a/internal/testanalyze/analyze.go +++ b/internal/testanalyze/analyze.go @@ -70,7 +70,7 @@ func (a *MockedAnalyzer) GetProgressChan() chan common.CurrentProgress { return make(chan common.CurrentProgress) } -// GetDoneChan returns always Done +// GetDone returns always Done func (a *MockedAnalyzer) GetDone() common.SignalGroup { c := make(common.SignalGroup) defer c.Broadcast() @@ -80,6 +80,9 @@ func (a *MockedAnalyzer) GetDone() common.SignalGroup { // ResetProgress does nothing func (a *MockedAnalyzer) ResetProgress() {} +// SetFollowSymlinks does nothing +func (a *MockedAnalyzer) SetFollowSymlinks(v bool) {} + // RemoveItemFromDirWithErr returns error func RemoveItemFromDirWithErr(dir fs.Item, file fs.Item) error { return errors.New("Failed") diff --git a/pkg/analyze/dir.go b/pkg/analyze/dir.go index 08c0b43..eedce4d 100644 --- a/pkg/analyze/dir.go +++ b/pkg/analyze/dir.go @@ -22,6 +22,7 @@ type ParallelAnalyzer struct { doneChan common.SignalGroup wait *WaitGroup ignoreDir common.ShouldDirBeIgnored + followSymlinks bool } // CreateAnalyzer returns Analyzer @@ -39,12 +40,17 @@ func CreateAnalyzer() *ParallelAnalyzer { } } +// SetFollowSymlinks sets whether symlink to files should be followed +func (a *ParallelAnalyzer) SetFollowSymlinks(v bool) { + a.followSymlinks = v +} + // GetProgressChan returns channel for getting progress func (a *ParallelAnalyzer) GetProgressChan() chan common.CurrentProgress { return a.progressOutChan } -// GetDoneChan returns channel for checking when analysis is done +// GetDone returns channel for checking when analysis is done func (a *ParallelAnalyzer) GetDone() common.SignalGroup { return a.doneChan } @@ -130,8 +136,18 @@ func (a *ParallelAnalyzer) processDir(path string) *Dir { info, err = f.Info() if err != nil { log.Print(err.Error()) + dir.Flag = '!' continue } + if a.followSymlinks && info.Mode()&os.ModeSymlink != 0 { + err = followSymlink(entryPath, &info) + if err != nil { + log.Print(err.Error()) + dir.Flag = '!' + continue + } + } + file = &File{ Name: name, Flag: getFlag(info), @@ -204,3 +220,18 @@ func getFlag(f os.FileInfo) rune { return ' ' } } + +func followSymlink(path string, f *os.FileInfo) error { + target, err := filepath.EvalSymlinks(path) + if err != nil { + return err + } + tInfo, err := os.Lstat(target) + if err != nil { + return err + } + if !tInfo.IsDir() { + *f = tInfo + } + return nil +} diff --git a/pkg/analyze/dir_test.go b/pkg/analyze/dir_test.go index 61ac0d8..89f6f86 100644 --- a/pkg/analyze/dir_test.go +++ b/pkg/analyze/dir_test.go @@ -133,6 +133,64 @@ func TestHardlink(t *testing.T) { assert.Equal(t, 'H', dir.Files[0].(*Dir).Files[1].GetFlag()) } +func TestFollowSymlink(t *testing.T) { + fin := testdir.CreateTestDir() + defer fin() + + err := os.Mkdir("test_dir/empty", 0644) + assert.Nil(t, err) + + err = os.Symlink("./file2", "test_dir/nested/file3") + assert.Nil(t, err) + + analyzer := CreateAnalyzer() + analyzer.SetFollowSymlinks(true) + dir := analyzer.AnalyzeDir( + "test_dir", func(_, _ string) bool { return false }, false, + ).(*Dir) + analyzer.GetDone().Wait() + dir.UpdateStats(make(fs.HardLinkedItems)) + + sort.Sort(sort.Reverse(dir.Files)) + + assert.Equal(t, int64(9+4096*4), dir.Size) + assert.Equal(t, 7, dir.ItemCount) + + // test file3 + assert.Equal(t, "nested", dir.Files[0].GetName()) + assert.Equal(t, "file3", dir.Files[0].(*Dir).Files[1].GetName()) + assert.Equal(t, int64(2), dir.Files[0].(*Dir).Files[1].GetSize()) + assert.Equal(t, ' ', dir.Files[0].(*Dir).Files[1].GetFlag()) + + assert.Equal(t, 'e', dir.Files[1].GetFlag()) +} + +func TestBrokenSymlinkSkipped(t *testing.T) { + fin := testdir.CreateTestDir() + defer fin() + + err := os.Mkdir("test_dir/empty", 0644) + assert.Nil(t, err) + + err = os.Symlink("xxx", "test_dir/nested/file3") + assert.Nil(t, err) + + analyzer := CreateAnalyzer() + analyzer.SetFollowSymlinks(true) + dir := analyzer.AnalyzeDir( + "test_dir", func(_, _ string) bool { return false }, false, + ).(*Dir) + analyzer.GetDone().Wait() + dir.UpdateStats(make(fs.HardLinkedItems)) + + sort.Sort(sort.Reverse(dir.Files)) + + assert.Equal(t, int64(7+4096*4), dir.Size) + assert.Equal(t, 6, dir.ItemCount) + + assert.Equal(t, '!', dir.Files[0].GetFlag()) +} + func BenchmarkAnalyzeDir(b *testing.B) { fin := testdir.CreateTestDir() defer fin() |