diff --git a/cmd/gdu/app/app.go b/cmd/gdu/app/app.go index 5860453f1..1a27d30a4 100644 --- a/cmd/gdu/app/app.go +++ b/cmd/gdu/app/app.go @@ -72,6 +72,7 @@ type Flags struct { StoragePath string `yaml:"storage-path"` ReadFromStorage bool `yaml:"read-from-storage"` Summarize bool `yaml:"summarize"` + Top int `yaml:"top"` UseSIPrefix bool `yaml:"use-si-prefix"` NoPrefix bool `yaml:"no-prefix"` WriteConfig bool `yaml:"-"` @@ -242,6 +243,7 @@ func (a *App) createUI() (UI, error) { a.Flags.ConstGC, a.Flags.UseSIPrefix, a.Flags.NoPrefix, + a.Flags.Top, ) if a.Flags.NoUnicode { stdoutUI.UseOldProgressRunes() diff --git a/cmd/gdu/main.go b/cmd/gdu/main.go index b7ae04d49..b256f7b29 100644 --- a/cmd/gdu/main.go +++ b/cmd/gdu/main.go @@ -74,6 +74,7 @@ func init() { flags.BoolVarP(&af.NoProgress, "no-progress", "p", false, "Do not show progress in non-interactive mode") flags.BoolVarP(&af.NoUnicode, "no-unicode", "u", false, "Do not use Unicode symbols (for size bar)") flags.BoolVarP(&af.Summarize, "summarize", "s", false, "Show only a total in non-interactive mode") + flags.IntVarP(&af.Top, "top", "t", 0, "Show only top X largest files in non-interactive mode") flags.BoolVar(&af.UseSIPrefix, "si", false, "Show sizes with decimal SI prefixes (kB, MB, GB) instead of binary prefixes (KiB, MiB, GiB)") flags.BoolVar(&af.NoPrefix, "no-prefix", false, "Show sizes as raw numbers without any prefixes (SI or binary) in non-interactive mode") flags.BoolVar(&af.NoMouse, "no-mouse", false, "Do not use mouse") diff --git a/pkg/analyze/top.go b/pkg/analyze/top.go new file mode 100644 index 000000000..55814747c --- /dev/null +++ b/pkg/analyze/top.go @@ -0,0 +1,55 @@ +package analyze + +import ( + "sort" + + "github.com/dundee/gdu/v5/pkg/fs" +) + +// TopList is a list of top largest files +type TopList struct { + Count int + Items fs.Files + MinSize int64 +} + +// NewTopList creates new TopList +func NewTopList(count int) *TopList { + return &TopList{Count: count} +} + +// Add adds file to the list +func (tl *TopList) Add(file fs.Item) { + if len(tl.Items) < tl.Count { + tl.Items = append(tl.Items, file) + if file.GetSize() > tl.MinSize { + tl.MinSize = file.GetSize() + } + } else { + if file.GetSize() > tl.MinSize { + tl.Items = append(tl.Items, file) + tl.MinSize = file.GetSize() + if len(tl.Items) > tl.Count { + sort.Sort(fs.ByApparentSize(tl.Items)) + tl.Items = tl.Items[1:] + } + } + } +} + +func CollectTopFiles(dir fs.Item, count int) fs.Files { + topList := NewTopList(count) + walkDir(dir, topList) + sort.Sort(sort.Reverse(fs.ByApparentSize(topList.Items))) + return topList.Items +} + +func walkDir(dir fs.Item, topList *TopList) { + for _, item := range dir.GetFiles() { + if item.IsDir() { + walkDir(item, topList) + } else { + topList.Add(item) + } + } +} diff --git a/pkg/analyze/top_test.go b/pkg/analyze/top_test.go new file mode 100644 index 000000000..7d89cc635 --- /dev/null +++ b/pkg/analyze/top_test.go @@ -0,0 +1,38 @@ +package analyze + +import ( + "testing" + + "github.com/dundee/gdu/v5/internal/testdir" + "github.com/stretchr/testify/assert" +) + +func TestCollectTopFiles2(t *testing.T) { + fin := testdir.CreateTestDir() + defer fin() + + dir := CreateAnalyzer().AnalyzeDir( + "test_dir", func(_, _ string) bool { return false }, false, + ) + + topFiles := CollectTopFiles(dir, 2) + assert.Equal(t, 2, len(topFiles)) + assert.Equal(t, "file", topFiles[0].GetName()) + assert.Equal(t, int64(5), topFiles[0].GetSize()) + assert.Equal(t, "file2", topFiles[1].GetName()) + assert.Equal(t, int64(2), topFiles[1].GetSize()) +} + +func TestCollectTopFiles1(t *testing.T) { + fin := testdir.CreateTestDir() + defer fin() + + dir := CreateAnalyzer().AnalyzeDir( + "test_dir", func(_, _ string) bool { return false }, false, + ) + + topFiles := CollectTopFiles(dir, 1) + assert.Equal(t, 1, len(topFiles)) + assert.Equal(t, "file", topFiles[0].GetName()) + assert.Equal(t, int64(5), topFiles[0].GetSize()) +} diff --git a/stdout/stdout.go b/stdout/stdout.go index ced65fd53..4d00d3eba 100644 --- a/stdout/stdout.go +++ b/stdout/stdout.go @@ -26,6 +26,7 @@ type UI struct { blue *color.Color summarize bool noPrefix bool + top int } var ( @@ -45,6 +46,7 @@ func CreateStdoutUI( constGC bool, useSIPrefix bool, noPrefix bool, + top int, ) *UI { ui := &UI{ UI: &common.UI{ @@ -59,6 +61,7 @@ func CreateStdoutUI( output: output, summarize: summarize, noPrefix: noPrefix, + top: top, } ui.red = color.New(color.FgRed).Add(color.Bold) @@ -167,7 +170,9 @@ func (ui *UI) AnalyzePath(path string, _ fs.Item) error { wait.Wait() - if ui.summarize { + if ui.top > 0 { + ui.printTopFiles(dir) + } else if ui.summarize { ui.printTotalItem(dir) } else { ui.showDir(dir) @@ -187,7 +192,9 @@ func (ui *UI) ReadFromStorage(storagePath, path string) error { return err } - if ui.summarize { + if ui.top > 0 { + ui.printTopFiles(dir) + } else if ui.summarize { ui.printTotalItem(dir) } else { ui.showDir(dir) @@ -203,6 +210,13 @@ func (ui *UI) showDir(dir fs.Item) { } } +func (ui *UI) printTopFiles(file fs.Item) { + collected := analyze.CollectTopFiles(file, ui.top) + for _, file := range collected { + ui.printItemPath(file) + } +} + func (ui *UI) printTotalItem(file fs.Item) { var lineFormat string if ui.UseColors { @@ -256,6 +270,27 @@ func (ui *UI) printItem(file fs.Item) { } } +func (ui *UI) printItemPath(file fs.Item) { + var lineFormat string + if ui.UseColors { + lineFormat = "%20s %s\n" + } else { + lineFormat = "%9s %s\n" + } + + var size int64 + if ui.ShowApparentSize { + size = file.GetSize() + } else { + size = file.GetUsage() + } + + fmt.Fprintf(ui.output, + lineFormat, + ui.formatSize(size), + file.GetPath()) +} + // ReadAnalysis reads analysis report from JSON file func (ui *UI) ReadAnalysis(input io.Reader) error { var ( diff --git a/stdout/stdout_test.go b/stdout/stdout_test.go index cd76bd92f..95dad4934 100644 --- a/stdout/stdout_test.go +++ b/stdout/stdout_test.go @@ -25,7 +25,7 @@ func TestAnalyzePath(t *testing.T) { buff := make([]byte, 10) output := bytes.NewBuffer(buff) - ui := CreateStdoutUI(output, false, false, false, false, false, true, false, false) + ui := CreateStdoutUI(output, false, false, false, false, false, true, false, false, 0) ui.SetIgnoreDirPaths([]string{"/xxx"}) err := ui.AnalyzePath("test_dir", nil) assert.Nil(t, err) @@ -42,7 +42,7 @@ func TestShowSummary(t *testing.T) { buff := make([]byte, 10) output := bytes.NewBuffer(buff) - ui := CreateStdoutUI(output, true, false, true, false, true, false, false, false) + ui := CreateStdoutUI(output, true, false, true, false, true, false, false, false, 0) ui.SetIgnoreDirPaths([]string{"/xxx"}) err := ui.AnalyzePath("test_dir", nil) assert.Nil(t, err) @@ -59,7 +59,7 @@ func TestShowSummaryBw(t *testing.T) { buff := make([]byte, 10) output := bytes.NewBuffer(buff) - ui := CreateStdoutUI(output, false, false, false, false, true, false, false, false) + ui := CreateStdoutUI(output, false, false, false, false, true, false, false, false, 0) ui.SetIgnoreDirPaths([]string{"/xxx"}) err := ui.AnalyzePath("test_dir", nil) assert.Nil(t, err) @@ -69,6 +69,42 @@ func TestShowSummaryBw(t *testing.T) { assert.Contains(t, output.String(), "test_dir") } +func TestShowTop(t *testing.T) { + fin := testdir.CreateTestDir() + defer fin() + + buff := make([]byte, 10) + output := bytes.NewBuffer(buff) + + ui := CreateStdoutUI(output, true, false, true, false, true, false, false, false, 2) + ui.SetIgnoreDirPaths([]string{"/xxx"}) + err := ui.AnalyzePath("test_dir", nil) + assert.Nil(t, err) + err = ui.StartUILoop() + + assert.Nil(t, err) + assert.Contains(t, output.String(), "test_dir/nested/subnested/file") + assert.Contains(t, output.String(), "test_dir/nested/file2") +} + +func TestShowTopBw(t *testing.T) { + fin := testdir.CreateTestDir() + defer fin() + + buff := make([]byte, 10) + output := bytes.NewBuffer(buff) + + ui := CreateStdoutUI(output, false, false, false, false, true, false, false, false, 2) + ui.SetIgnoreDirPaths([]string{"/xxx"}) + err := ui.AnalyzePath("test_dir", nil) + assert.Nil(t, err) + err = ui.StartUILoop() + + assert.Nil(t, err) + assert.Contains(t, output.String(), "test_dir/nested/subnested/file") + assert.Contains(t, output.String(), "test_dir/nested/file2") +} + func TestAnalyzeSubdir(t *testing.T) { fin := testdir.CreateTestDir() defer fin() @@ -76,7 +112,7 @@ func TestAnalyzeSubdir(t *testing.T) { buff := make([]byte, 10) output := bytes.NewBuffer(buff) - ui := CreateStdoutUI(output, false, false, false, false, false, false, false, false) + ui := CreateStdoutUI(output, false, false, false, false, false, false, false, false, 0) ui.SetIgnoreDirPaths([]string{"/xxx"}) err := ui.AnalyzePath("test_dir/nested", nil) assert.Nil(t, err) @@ -93,7 +129,7 @@ func TestAnalyzePathWithColors(t *testing.T) { buff := make([]byte, 10) output := bytes.NewBuffer(buff) - ui := CreateStdoutUI(output, true, false, true, false, false, false, false, false) + ui := CreateStdoutUI(output, true, false, true, false, false, false, false, false, 0) ui.SetIgnoreDirPaths([]string{"/xxx"}) err := ui.AnalyzePath("test_dir/nested", nil) @@ -108,7 +144,7 @@ func TestAnalyzePathWoUnicode(t *testing.T) { buff := make([]byte, 10) output := bytes.NewBuffer(buff) - ui := CreateStdoutUI(output, false, true, true, false, false, false, false, false) + ui := CreateStdoutUI(output, false, true, true, false, false, false, false, false, 0) ui.UseOldProgressRunes() err := ui.AnalyzePath("test_dir/nested", nil) @@ -119,7 +155,7 @@ func TestAnalyzePathWoUnicode(t *testing.T) { func TestItemRows(t *testing.T) { output := bytes.NewBuffer(make([]byte, 10)) - ui := CreateStdoutUI(output, false, true, false, false, false, false, false, false) + ui := CreateStdoutUI(output, false, true, false, false, false, false, false, false, 0) ui.Analyzer = &testanalyze.MockedAnalyzer{} err := ui.AnalyzePath("test_dir", nil) @@ -133,7 +169,7 @@ func TestAnalyzePathWithProgress(t *testing.T) { output := bytes.NewBuffer(make([]byte, 10)) - ui := CreateStdoutUI(output, false, true, true, false, false, false, false, false) + ui := CreateStdoutUI(output, false, true, true, false, false, false, false, false, 0) ui.SetIgnoreDirPaths([]string{"/xxx"}) err := ui.AnalyzePath("test_dir", nil) @@ -144,7 +180,7 @@ func TestAnalyzePathWithProgress(t *testing.T) { func TestShowDevices(t *testing.T) { output := bytes.NewBuffer(make([]byte, 10)) - ui := CreateStdoutUI(output, false, true, false, false, false, false, false, false) + ui := CreateStdoutUI(output, false, true, false, false, false, false, false, false, 0) err := ui.ListDevices(getDevicesInfoMock()) assert.Nil(t, err) @@ -155,7 +191,7 @@ func TestShowDevices(t *testing.T) { func TestShowDevicesWithColor(t *testing.T) { output := bytes.NewBuffer(make([]byte, 10)) - ui := CreateStdoutUI(output, true, true, true, false, false, false, false, false) + ui := CreateStdoutUI(output, true, true, true, false, false, false, false, false, 0) err := ui.ListDevices(getDevicesInfoMock()) assert.Nil(t, err) @@ -169,7 +205,7 @@ func TestReadAnalysisWithColor(t *testing.T) { output := bytes.NewBuffer(make([]byte, 10)) - ui := CreateStdoutUI(output, true, true, true, false, false, false, false, false) + ui := CreateStdoutUI(output, true, true, true, false, false, false, false, false, 0) err = ui.ReadAnalysis(input) assert.Nil(t, err) @@ -182,7 +218,7 @@ func TestReadAnalysisBw(t *testing.T) { output := bytes.NewBuffer(make([]byte, 10)) - ui := CreateStdoutUI(output, false, false, false, false, false, false, false, false) + ui := CreateStdoutUI(output, false, false, false, false, false, false, false, false, 0) err = ui.ReadAnalysis(input) assert.Nil(t, err) @@ -195,7 +231,7 @@ func TestReadAnalysisWithWrongFile(t *testing.T) { output := bytes.NewBuffer(make([]byte, 10)) - ui := CreateStdoutUI(output, true, true, true, false, false, false, false, false) + ui := CreateStdoutUI(output, true, true, true, false, false, false, false, false, 0) err = ui.ReadAnalysis(input) assert.NotNil(t, err) @@ -207,7 +243,7 @@ func TestReadAnalysisWithSummarize(t *testing.T) { output := bytes.NewBuffer(make([]byte, 10)) - ui := CreateStdoutUI(output, false, false, false, false, true, false, false, false) + ui := CreateStdoutUI(output, false, false, false, false, true, false, false, false, 0) err = ui.ReadAnalysis(input) assert.Nil(t, err) @@ -222,7 +258,7 @@ func TestMaxInt(t *testing.T) { func TestFormatSize(t *testing.T) { output := bytes.NewBuffer(make([]byte, 10)) - ui := CreateStdoutUI(output, true, true, true, false, false, false, false, false) + ui := CreateStdoutUI(output, true, true, true, false, false, false, false, false, 0) assert.Contains(t, ui.formatSize(1), "B") assert.Contains(t, ui.formatSize(1<<10+1), "KiB") @@ -236,7 +272,7 @@ func TestFormatSize(t *testing.T) { func TestFormatSizeDec(t *testing.T) { output := bytes.NewBuffer(make([]byte, 10)) - ui := CreateStdoutUI(output, true, true, true, false, false, false, true, false) + ui := CreateStdoutUI(output, true, true, true, false, false, false, true, false, 0) assert.Contains(t, ui.formatSize(1), "B") assert.Contains(t, ui.formatSize(1<<10+1), "kB") @@ -250,7 +286,7 @@ func TestFormatSizeDec(t *testing.T) { func TestFormatSizeRaw(t *testing.T) { output := bytes.NewBuffer(make([]byte, 10)) - ui := CreateStdoutUI(output, true, true, true, false, false, false, true, true) + ui := CreateStdoutUI(output, true, true, true, false, false, false, true, true, 0) assert.Equal(t, ui.formatSize(1), "1") assert.Equal(t, ui.formatSize(1<<10+1), "1025")