summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDaniel Milde <daniel@milde.cz>2023-02-05 23:16:50 +0100
committerGitHub <noreply@github.com>2023-02-05 23:16:50 +0100
commit5b9d8fd893819714fac85ee258c1dc218ab920fd (patch)
treea44c4dda0bf1542d38d8d7f783694322ea663499
parent8bdce9a33d2e7e7bbe127dcdceee78237070c19f (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.md1
-rw-r--r--cmd/gdu/app/app.go12
-rw-r--r--cmd/gdu/app/app_test.go15
-rw-r--r--cmd/gdu/main.go4
-rw-r--r--internal/common/analyze.go1
-rw-r--r--internal/common/ui.go5
-rw-r--r--internal/common/ui_test.go41
-rw-r--r--internal/testanalyze/analyze.go5
-rw-r--r--pkg/analyze/dir.go33
-rw-r--r--pkg/analyze/dir_test.go58
10 files changed, 169 insertions, 6 deletions
diff --git a/README.md b/README.md
index 5114342..ac0bbc8 100644
--- a/README.md
+++ b/README.md
@@ -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()