diff options
author | Daniel Milde <daniel@milde.cz> | 2021-11-07 23:08:47 +0100 |
---|---|---|
committer | Daniel Milde <daniel@milde.cz> | 2021-11-07 23:08:47 +0100 |
commit | 8edb037d8365744a8d8ac9a27f83ceee04bf16dd (patch) | |
tree | d24d13a513ce81dfb932097755110a3bd8d990b9 | |
parent | 4dfbeded97cb39619896d4d1fb5e4b795b8962ee (diff) |
show info about hard-linked files
closes #95
-rw-r--r-- | internal/testanalyze/analyze.go | 13 | ||||
-rw-r--r-- | pkg/analyze/dir.go | 3 | ||||
-rw-r--r-- | pkg/analyze/dir_test.go | 28 | ||||
-rw-r--r-- | pkg/analyze/file.go | 38 | ||||
-rw-r--r-- | pkg/analyze/file_test.go | 10 | ||||
-rw-r--r-- | report/export.go | 1 | ||||
-rw-r--r-- | stdout/stdout.go | 4 | ||||
-rw-r--r-- | stdout/stdout_test.go | 2 | ||||
-rw-r--r-- | tui/actions.go | 20 | ||||
-rw-r--r-- | tui/actions_test.go | 41 | ||||
-rw-r--r-- | tui/exec_test.go | 2 | ||||
-rw-r--r-- | tui/sort_test.go | 16 | ||||
-rw-r--r-- | tui/tui.go | 2 |
13 files changed, 127 insertions, 53 deletions
diff --git a/internal/testanalyze/analyze.go b/internal/testanalyze/analyze.go index ac38372..76e01df 100644 --- a/internal/testanalyze/analyze.go +++ b/internal/testanalyze/analyze.go @@ -22,7 +22,7 @@ func (a *MockedAnalyzer) AnalyzeDir(path string, ignore analyze.ShouldDirBeIgnor BasePath: ".", ItemCount: 12, } - file := &analyze.Dir{ + dir2 := &analyze.Dir{ File: &analyze.File{ Name: "aaa", Usage: 1e12 + 1, @@ -30,9 +30,8 @@ func (a *MockedAnalyzer) AnalyzeDir(path string, ignore analyze.ShouldDirBeIgnor Mtime: time.Date(2021, 8, 27, 22, 23, 27, 0, time.UTC), Parent: dir, }, - ItemCount: 5, } - file2 := &analyze.Dir{ + dir3 := &analyze.Dir{ File: &analyze.File{ Name: "bbb", Usage: 1e9 + 1, @@ -40,9 +39,8 @@ func (a *MockedAnalyzer) AnalyzeDir(path string, ignore analyze.ShouldDirBeIgnor Mtime: time.Date(2021, 8, 27, 22, 23, 26, 0, time.UTC), Parent: dir, }, - ItemCount: 3, } - file3 := &analyze.Dir{ + dir4 := &analyze.Dir{ File: &analyze.File{ Name: "ccc", Usage: 1e6 + 1, @@ -50,16 +48,15 @@ func (a *MockedAnalyzer) AnalyzeDir(path string, ignore analyze.ShouldDirBeIgnor Mtime: time.Date(2021, 8, 27, 22, 23, 25, 0, time.UTC), Parent: dir, }, - ItemCount: 2, } - file4 := &analyze.File{ + file := &analyze.File{ Name: "ddd", Usage: 1e3 + 1, Size: 1e3 + 2, Mtime: time.Date(2021, 8, 27, 22, 23, 24, 0, time.UTC), Parent: dir, } - dir.Files = analyze.Files{file, file2, file3, file4} + dir.Files = analyze.Files{dir2, dir3, dir4, file} return dir } diff --git a/pkg/analyze/dir.go b/pkg/analyze/dir.go index 387549d..f91ddda 100644 --- a/pkg/analyze/dir.go +++ b/pkg/analyze/dir.go @@ -79,9 +79,6 @@ func (a *ParallelAnalyzer) AnalyzeDir(path string, ignore ShouldDirBeIgnored) *D dir.BasePath = filepath.Dir(path) a.wait.Wait() - links := make(AlreadyCountedHardlinks, 10) - dir.UpdateStats(links) - a.doneChan <- struct{}{} // finish updateProgress here a.doneChan <- struct{}{} // and there diff --git a/pkg/analyze/dir_test.go b/pkg/analyze/dir_test.go index 7bd282d..874de8c 100644 --- a/pkg/analyze/dir_test.go +++ b/pkg/analyze/dir_test.go @@ -22,13 +22,12 @@ func TestAnalyzeDir(t *testing.T) { analyzer := CreateAnalyzer() dir := analyzer.AnalyzeDir("test_dir", func(_, _ string) bool { return false }) - c := analyzer.GetProgressChan() - progress := <-c + progress := <-analyzer.GetProgressChan() assert.GreaterOrEqual(t, progress.TotalSize, int64(0)) analyzer.ResetProgress() - done := analyzer.GetDoneChan() - <-done + <-analyzer.GetDoneChan() + dir.UpdateStats(make(HardLinkedItems)) // test dir info assert.Equal(t, "test_dir", dir.Name) @@ -71,7 +70,11 @@ func TestFlags(t *testing.T) { err = os.Symlink("test_dir/nested/file2", "test_dir/nested/file3") assert.Nil(t, err) - dir := CreateAnalyzer().AnalyzeDir("test_dir", func(_, _ string) bool { return false }) + analyzer := CreateAnalyzer() + dir := analyzer.AnalyzeDir("test_dir", func(_, _ string) bool { return false }) + <-analyzer.GetDoneChan() + dir.UpdateStats(make(HardLinkedItems)) + sort.Sort(dir.Files) assert.Equal(t, int64(28+4096*4), dir.Size) @@ -93,7 +96,10 @@ func TestHardlink(t *testing.T) { err := os.Link("test_dir/nested/file2", "test_dir/nested/file3") assert.Nil(t, err) - dir := CreateAnalyzer().AnalyzeDir("test_dir", func(_, _ string) bool { return false }) + analyzer := CreateAnalyzer() + dir := analyzer.AnalyzeDir("test_dir", func(_, _ string) bool { return false }) + <-analyzer.GetDoneChan() + dir.UpdateStats(make(HardLinkedItems)) assert.Equal(t, int64(7+4096*3), dir.Size) // file2 and file3 are counted just once for size assert.Equal(t, 6, dir.ItemCount) // but twice for item count @@ -115,7 +121,10 @@ func TestErr(t *testing.T) { assert.Nil(t, err) }() - dir := CreateAnalyzer().AnalyzeDir("test_dir", func(_, _ string) bool { return false }) + analyzer := CreateAnalyzer() + dir := analyzer.AnalyzeDir("test_dir", func(_, _ string) bool { return false }) + <-analyzer.GetDoneChan() + dir.UpdateStats(make(HardLinkedItems)) assert.Equal(t, "test_dir", dir.GetName()) assert.Equal(t, 2, dir.ItemCount) @@ -131,5 +140,8 @@ func BenchmarkAnalyzeDir(b *testing.B) { b.ResetTimer() - CreateAnalyzer().AnalyzeDir("test_dir", func(_, _ string) bool { return false }) + analyzer := CreateAnalyzer() + dir := analyzer.AnalyzeDir("test_dir", func(_, _ string) bool { return false }) + <-analyzer.GetDoneChan() + dir.UpdateStats(make(HardLinkedItems)) } diff --git a/pkg/analyze/file.go b/pkg/analyze/file.go index c07e382..209ce2c 100644 --- a/pkg/analyze/file.go +++ b/pkg/analyze/file.go @@ -7,8 +7,8 @@ import ( "time" ) -// AlreadyCountedHardlinks holds all files with hardlinks that have already been counted -type AlreadyCountedHardlinks map[uint64]bool +// HardLinkedItems maps inode number to array of all hard linked items +type HardLinkedItems map[uint64]Files // Item is fs item (file or dir) type Item interface { @@ -22,8 +22,9 @@ type Item interface { GetMtime() time.Time GetItemCount() int GetParent() *Dir + GetMultiLinkedInode() uint64 EncodeJSON(writer io.Writer, topLevel bool) error - getItemStats(links AlreadyCountedHardlinks) (int, int64, int64) + getItemStats(linkedItems HardLinkedItems) (int, int64, int64) } // File struct @@ -91,21 +92,26 @@ func (f *File) GetItemCount() int { return 1 } -func (f *File) alreadyCounted(links AlreadyCountedHardlinks) bool { +// GetMultiLinkedInode returns inode number of multilinked file +func (f *File) GetMultiLinkedInode() uint64 { + return f.Mli +} + +func (f *File) alreadyCounted(linkedItems HardLinkedItems) bool { mli := f.Mli + counted := false if mli > 0 { - if !links[mli] { - links[mli] = true - return false + if _, ok := linkedItems[mli]; ok { + f.Flag = 'H' + counted = true } - f.Flag = 'H' - return true + linkedItems[mli] = append(linkedItems[mli], f) } - return false + return counted } -func (f *File) getItemStats(links AlreadyCountedHardlinks) (int, int64, int64) { - if f.alreadyCounted(links) { +func (f *File) getItemStats(linkedItems HardLinkedItems) (int, int64, int64) { + if f.alreadyCounted(linkedItems) { return 1, 0, 0 } return 1, f.GetSize(), f.GetUsage() @@ -142,18 +148,18 @@ func (f *Dir) GetPath() string { return filepath.Join(f.Parent.GetPath(), f.Name) } -func (f *Dir) getItemStats(links AlreadyCountedHardlinks) (int, int64, int64) { - f.UpdateStats(links) +func (f *Dir) getItemStats(linkedItems HardLinkedItems) (int, int64, int64) { + f.UpdateStats(linkedItems) return f.ItemCount, f.GetSize(), f.GetUsage() } // UpdateStats recursively updates size and item count -func (f *Dir) UpdateStats(links AlreadyCountedHardlinks) { +func (f *Dir) UpdateStats(linkedItems HardLinkedItems) { totalSize := int64(4096) totalUsage := int64(4096) var itemCount int for _, entry := range f.Files { - count, size, usage := entry.getItemStats(links) + count, size, usage := entry.getItemStats(linkedItems) totalSize += size totalUsage += usage itemCount += count diff --git a/pkg/analyze/file_test.go b/pkg/analyze/file_test.go index b41819c..48cdc51 100644 --- a/pkg/analyze/file_test.go +++ b/pkg/analyze/file_test.go @@ -378,3 +378,13 @@ func TestUpdateStats(t *testing.T) { assert.Equal(t, int64(4096+5), dir.Size) assert.Equal(t, 42, dir.GetMtime().Minute()) } + +func TestGetMultiLinkedInode(t *testing.T) { + file := &File{ + Name: "xxx", + Mli: 5, + } + + assert.Equal(t, uint64(5), file.GetMultiLinkedInode()) + +} diff --git a/report/export.go b/report/export.go index 0d9f226..432e921 100644 --- a/report/export.go +++ b/report/export.go @@ -87,6 +87,7 @@ func (ui *UI) AnalyzePath(path string, _ *analyze.Dir) error { defer wait.Done() defer debug.SetGCPercent(debug.SetGCPercent(-1)) dir = ui.Analyzer.AnalyzeDir(path, ui.CreateIgnoreFunc()) + dir.UpdateStats(make(analyze.HardLinkedItems, 10)) }() wait.Wait() diff --git a/stdout/stdout.go b/stdout/stdout.go index ab627c5..9be08eb 100644 --- a/stdout/stdout.go +++ b/stdout/stdout.go @@ -142,6 +142,7 @@ func (ui *UI) AnalyzePath(path string, _ *analyze.Dir) error { defer wait.Done() defer debug.SetGCPercent(debug.SetGCPercent(-1)) dir = ui.Analyzer.AnalyzeDir(path, ui.CreateIgnoreFunc()) + dir.UpdateStats(make(analyze.HardLinkedItems, 10)) }() wait.Wait() @@ -246,8 +247,7 @@ func (ui *UI) ReadAnalysis(input io.Reader) error { } runtime.GC() - links := make(analyze.AlreadyCountedHardlinks, 10) - dir.UpdateStats(links) + dir.UpdateStats(make(analyze.HardLinkedItems, 10)) if ui.ShowProgress { doneChan <- struct{}{} diff --git a/stdout/stdout_test.go b/stdout/stdout_test.go index 89a86fc..5441441 100644 --- a/stdout/stdout_test.go +++ b/stdout/stdout_test.go @@ -109,8 +109,6 @@ func TestItemRows(t *testing.T) { err := ui.AnalyzePath("test_dir", nil) assert.Nil(t, err) - assert.Contains(t, output.String(), "GiB") - assert.Contains(t, output.String(), "MiB") assert.Contains(t, output.String(), "KiB") } diff --git a/tui/actions.go b/tui/actions.go index f5b3c0a..1e82d77 100644 --- a/tui/actions.go +++ b/tui/actions.go @@ -62,14 +62,13 @@ func (ui *UI) AnalyzePath(path string, parentDir *analyze.Dir) error { currentDir.Parent = parentDir parentDir.Files = parentDir.Files.RemoveByName(currentDir.Name) parentDir.Files.Append(currentDir) - - links := make(analyze.AlreadyCountedHardlinks, 10) - ui.topDir.UpdateStats(links) } else { ui.topDirPath = path ui.topDir = currentDir } + ui.topDir.UpdateStats(ui.linkedItems) + ui.app.QueueUpdateDraw(func() { ui.currentDir = currentDir ui.showDir() @@ -119,7 +118,7 @@ func (ui *UI) ReadAnalysis(input io.Reader) error { ui.topDirPath = ui.currentDir.GetPath() ui.topDir = ui.currentDir - links := make(analyze.AlreadyCountedHardlinks, 10) + links := make(analyze.HardLinkedItems, 10) ui.topDir.UpdateStats(links) ui.app.QueueUpdateDraw(func() { @@ -303,6 +302,8 @@ func (ui *UI) showInfo() { numberColor = "[::b]" } + linesCount := 12 + text := tview.NewTextView().SetDynamicColors(true) text.SetBorder(true).SetBorderPadding(2, 2, 2, 2) text.SetBorderColor(tcell.ColorDefault) @@ -320,13 +321,22 @@ func (ui *UI) showInfo() { content += numberColor + ui.formatSize(selectedFile.GetSize(), false, true) content += fmt.Sprintf(" (%s%d[-::] B)", numberColor, selectedFile.GetSize()) + "\n" + if selectedFile.GetMultiLinkedInode() > 0 { + linkedItems := ui.linkedItems[selectedFile.GetMultiLinkedInode()] + linesCount += 2 + len(linkedItems) + content += "\nHard-linked files:\n" + for _, linkedItem := range linkedItems { + content += "\t" + linkedItem.GetPath() + "\n" + } + } + text.SetText(content) flex := tview.NewFlex(). AddItem(nil, 0, 1, false). AddItem(tview.NewFlex().SetDirection(tview.FlexRow). AddItem(nil, 0, 1, false). - AddItem(text, 13, 1, false). + AddItem(text, linesCount, 1, false). AddItem(nil, 0, 1, false), 80, 1, false). AddItem(nil, 0, 1, false) diff --git a/tui/actions_test.go b/tui/actions_test.go index 5fca310..c4880a1 100644 --- a/tui/actions_test.go +++ b/tui/actions_test.go @@ -330,6 +330,47 @@ func TestShowInfoBW(t *testing.T) { assert.True(t, ui.pages.HasPage("info")) } +func TestShowInfoWithHardlinks(t *testing.T) { + fin := testdir.CreateTestDir() + defer fin() + simScreen := testapp.CreateSimScreen(50, 50) + defer simScreen.Fini() + + app := testapp.CreateMockedApp(true) + ui := CreateUI(app, simScreen, &bytes.Buffer{}, false, true) + ui.done = make(chan struct{}) + err := ui.AnalyzePath("test_dir", nil) + assert.Nil(t, err) + + <-ui.done // wait for analyzer + + for _, f := range ui.app.(*testapp.MockedApp).UpdateDraws { + f() + } + + nested := ui.currentDir.Files[0].(*analyze.Dir) + subnested := nested.Files[1].(*analyze.Dir) + file := subnested.Files[0].(*analyze.File) + file2 := nested.Files[0].(*analyze.File) + file.Mli = 1 + file2.Mli = 1 + + ui.currentDir.UpdateStats(ui.linkedItems) + + assert.Equal(t, "test_dir", ui.currentDir.Name) + + ui.keyPressed(tcell.NewEventKey(tcell.KeyRight, 'l', 0)) + ui.keyPressed(tcell.NewEventKey(tcell.KeyRune, 'i', 0)) + ui.table.Select(2, 0) + ui.keyPressed(tcell.NewEventKey(tcell.KeyRune, 'i', 0)) + + assert.True(t, ui.pages.HasPage("info")) + + ui.keyPressed(tcell.NewEventKey(tcell.KeyRune, 'q', 0)) + + assert.False(t, ui.pages.HasPage("info")) +} + func TestShowInfoWithoutCurrentDir(t *testing.T) { fin := testdir.CreateTestDir() defer fin() diff --git a/tui/exec_test.go b/tui/exec_test.go index 6588da9..72393ae 100644 --- a/tui/exec_test.go +++ b/tui/exec_test.go @@ -6,7 +6,7 @@ import ( "github.com/stretchr/testify/assert" ) -func TestExec(t *testing.T) { +func TestExecute(t *testing.T) { err := Execute("true", []string{}, []string{}) assert.Nil(t, err) diff --git a/tui/sort_test.go b/tui/sort_test.go index 43e4568..3257f1a 100644 --- a/tui/sort_test.go +++ b/tui/sort_test.go @@ -24,9 +24,9 @@ func TestSortByApparentSizeAsc(t *testing.T) { assert.Equal(t, 4, ui.table.GetRowCount()) assert.Contains(t, ui.table.GetCell(0, 0).Text, "ddd") - assert.Contains(t, ui.table.GetCell(1, 0).Text, "ccc") + assert.Contains(t, ui.table.GetCell(1, 0).Text, "aaa") assert.Contains(t, ui.table.GetCell(2, 0).Text, "bbb") - assert.Contains(t, ui.table.GetCell(3, 0).Text, "aaa") + assert.Contains(t, ui.table.GetCell(3, 0).Text, "ccc") } func TestAnalyzeBySize(t *testing.T) { @@ -44,9 +44,9 @@ func TestSortBySizeAsc(t *testing.T) { assert.Equal(t, 4, ui.table.GetRowCount()) assert.Contains(t, ui.table.GetCell(0, 0).Text, "ddd") - assert.Contains(t, ui.table.GetCell(1, 0).Text, "ccc") + assert.Contains(t, ui.table.GetCell(1, 0).Text, "aaa") assert.Contains(t, ui.table.GetCell(2, 0).Text, "bbb") - assert.Contains(t, ui.table.GetCell(3, 0).Text, "aaa") + assert.Contains(t, ui.table.GetCell(3, 0).Text, "ccc") } func TestAnalyzeByName(t *testing.T) { @@ -83,10 +83,10 @@ func TestAnalyzeByItemCountAsc(t *testing.T) { ui := getAnalyzedPathWithSorting("itemCount", "asc", false) assert.Equal(t, 4, ui.table.GetRowCount()) - assert.Contains(t, ui.table.GetCell(0, 0).Text, "ddd") - assert.Contains(t, ui.table.GetCell(1, 0).Text, "ccc") - assert.Contains(t, ui.table.GetCell(2, 0).Text, "bbb") - assert.Contains(t, ui.table.GetCell(3, 0).Text, "aaa") + assert.Contains(t, ui.table.GetCell(0, 0).Text, "aaa") + assert.Contains(t, ui.table.GetCell(1, 0).Text, "bbb") + assert.Contains(t, ui.table.GetCell(2, 0).Text, "ccc") + assert.Contains(t, ui.table.GetCell(3, 0).Text, "ddd") } func TestAnalyzeByMtime(t *testing.T) { @@ -69,6 +69,7 @@ type UI struct { remover func(*analyze.Dir, analyze.Item) error emptier func(*analyze.Dir, analyze.Item) error exec func(argv0 string, argv []string, envv []string) error + linkedItems analyze.HardLinkedItems } // CreateUI creates the whole UI app @@ -87,6 +88,7 @@ func CreateUI(app common.TermApplication, screen tcell.Screen, output io.Writer, remover: analyze.RemoveItemFromDir, emptier: analyze.EmptyFileFromDir, exec: Execute, + linkedItems: make(analyze.HardLinkedItems, 10), } ui.resetSorting() |