Skip to content

Commit

Permalink
feat: added option to follow symlinks (#206)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
dundee authored Feb 5, 2023
1 parent 8bdce9a commit 5b9d8fd
Show file tree
Hide file tree
Showing 10 changed files with 169 additions and 6 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
12 changes: 8 additions & 4 deletions cmd/gdu/app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ type UI interface {
SetIgnoreDirPatterns(paths []string) error
SetIgnoreFromFile(ignoreFile string) error
SetIgnoreHidden(value bool)
SetFollowSymlinks(value bool)
StartUILoop() error
}

Expand All @@ -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"`
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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
}

Expand Down
15 changes: 15 additions & 0 deletions cmd/gdu/app/app_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
4 changes: 4 additions & 0 deletions cmd/gdu/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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/")
Expand Down
1 change: 1 addition & 0 deletions internal/common/analyze.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
5 changes: 5 additions & 0 deletions internal/common/ui.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
41 changes: 41 additions & 0 deletions internal/common/ui_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,51 @@ package common
import (
"testing"

"github.com/dundee/gdu/v5/pkg/fs"
"github.com/stretchr/testify/assert"
)

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
}
5 changes: 4 additions & 1 deletion internal/testanalyze/analyze.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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")
Expand Down
33 changes: 32 additions & 1 deletion pkg/analyze/dir.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ type ParallelAnalyzer struct {
doneChan common.SignalGroup
wait *WaitGroup
ignoreDir common.ShouldDirBeIgnored
followSymlinks bool
}

// CreateAnalyzer returns Analyzer
Expand All @@ -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
}
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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
}
58 changes: 58 additions & 0 deletions pkg/analyze/dir_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down

0 comments on commit 5b9d8fd

Please sign in to comment.