Tracking down hard disk space consumption with Go

Hard Links

By the way, this simple version of the byte counter is not totally accurate. Additional hard links – that is, additional inodes pointing to existing files – are counted twice. However, this can be corrected with a little effort. You need to create a huge table and store the hard disk number and its inode for each file already encountered. If an entry with the same values is then found, it has to be a hard link that should only be counted once.

The call to the min heap Add() in line 43 registers the entry that was just found. The entry will be immediately dropped by the heap unless it belongs in the top five due to its size. All told, duDir() starting in line 14 does all the nitty gritty work of traversing files and counting their bytes to return a hash map to the main program that then goes ahead and displays the result. In fact, the main program calling duDir() receives two results: first, the total number of bytes used under the analyzed directory, and second, the duTotal hash table, which isn't a return value of the function, but it acts as one, because the parameter is passed to the function as a pointer and is modified by the function.

For the Main Course

The main program in Listing 4 first checks whether the user has given it a directory to analyze on the command line and complains otherwise. It then calls duDir() from Listing 3 with an initially empty duTotal map that assigns duEntry type counters to each of the top-level directories. The function scans the hard disk and passes the compiled results back. If there are subsequently more than 10 top directories, the min heap filters out the biggest 10 starting in line 23. The subsequent sort function then arranges them in alphabetical order starting in line 36, before the call to ui() in line 40 passes the whole thing to the terminal UI for display.

Listing 4

duview.go

01 package main
02
03 import (
04   "fmt"
05   "os"
06   "sort"
07   "strings"
08 )
09
10 func main() {
11   if len(os.Args) != 2 {
12     fmt.Printf("usage: %s dir\n", os.Args[0])
13     os.Exit(1)
14   }
15
16   topDir := os.Args[1]
17   duTotal := map[string]duEntry{}
18   bytes, err := duDir(topDir, &duTotal)
19   if err != nil {
20     panic(err)
21   }
22
23   topn := NewTopN(10)
24   for dir, due := range duTotal {
25     if strings.Count(dir, "/") > 1 {
26       continue
27     }
28    topn.Add(FsEntry{path: dir, used: due.used})
29   }
30
31   lines := []FsEntry{}
32   topn.Iterate(func(e FsEntry) {
33     lines = append(lines, e)
34   })
35
36   sort.Slice(lines, func(i, j int) bool {
37     return lines[i].path < lines[j].path
38   })
39
40   ui(topDir, bytes, lines, duTotal)
41 }

Terminal Scribbles

To give users a graphical display of the data collected so far, Listing 5 then proceeds to display the data in the terminal's graphics mode using the tview framework, which I also used in last month's Programming Snapshot column [1]. If it's good enough for the Kubernetes command-line tools, it is certainly good enough for the Programming Snapshot column!

Listing 5

ui.go

01 package main
02
03 import (
04   "fmt"
05   "github.com/gdamore/tcell/v2"
06   "github.com/rivo/tview"
07   "golang.org/x/text/language"
08   "golang.org/x/text/message"
09 )
10
11 func ui(rootDir string, bytes int64, dirs []FsEntry, duTotal map[string]duEntry) {
12   root := tview.NewTreeNode(fmt.Sprintf("%s %s", rootDir, commify(bytes))).
13     SetColor(tcell.ColorRed)
14   tree := tview.NewTreeView().
15     SetRoot(root).
16     SetCurrentNode(root)
17
18   for _, dir := range dirs {
19     node := tview.NewTreeNode(fmt.Sprintf("%s %s", dir.path, commify(dir.used))).
20       SetExpanded(false).
21       SetSelectable(false)
22
23     // new dir
24     root.AddChild(node)
25     node.SetSelectable(true)
26     node.SetColor(tcell.ColorGreen)
27     dut, ok := duTotal[dir.path]
28     if !ok {
29       panic(fmt.Sprintf("Can't find %s", dir.path))
30     }
31
32     // add sub-menu
33     h := dut.h
34     h.Iterate(func(fse FsEntry) {
35       line := fmt.Sprintf("%s %s", fse.path, commify(fse.used))
36       n := tview.NewTreeNode(line).
37         SetExpanded(false).
38         SetSelectable(false)
39       node.AddChild(n)
40     })
41   }
42
43   tree.SetSelectedFunc(func(node *tview.TreeNode) {
44     node.SetExpanded(!node.IsExpanded())
45   })
46
47   err := tview.NewApplication().SetRoot(tree, true).EnableMouse(true).Run()
48   if err != nil {
49     panic(err)
50   }
51 }
52
53 func commify(i int64) string {
54   p := message.NewPrinter(language.English)
55   return p.Sprintf("%d", i)
56 }

The topmost entry in the tree shown in Figure 1 is the startup directory passed to the program on the command line, along with the total space it occupies in bytes. Line 12 defines the entry and highlights it in red. The tree itself resides in the tree variable starting in line 14. The code adds the root node to it first. Then, further down, the for loop, starting in line 18, adds the top space-consuming directories that lie below it.

AddChild(), which is provided by the tview GUI, adds each of these entries to the tree as a branch, which is made selectable by SetSelectable(). The user can either press the Enter key or click on the branch with the mouse. What happens on this event is defined by the call to the SetSelectedFunc() function further down in line 43. Its SetExpanded() callback sets the attribute to either true or false, depending on whether the entry has already been expanded. That way the GUI opens closed branches and closes opened ones.

To make the displayed numeric values more readable, the commify() function defined starting in line 53 inserts commas into the integer values, turning 123456789, for example, into the more easily readable 123,456,789. This is done using the NewPrinter() function from the standard language and message libraries in golang.org/x/text/language/.

Line 47 inserts the previously defined tree object into the terminal window controlled by tview and switches the terminal to graphics mode. Run() starts the event loop that intercepts user input and refreshes the display to reflect the incoming events. The user can interrupt the merry dance by pressing Ctrl+C, which switches the terminal back to normal text mode and lets the shell take over again.

Buy this article as PDF

Express-Checkout as PDF
Price $2.95
(incl. VAT)

Buy Linux Magazine

SINGLE ISSUES
 
SUBSCRIPTIONS
 
TABLET & SMARTPHONE APPS
Get it on Google Play

US / Canada

Get it on Google Play

UK / Australia

Related content

  • Ready to Rumble

    A Go program writes a downloaded ISO file to a bootable USB stick. To prevent it from accidentally overwriting the hard disk, Mike Schilli provides it with a user interface and security checks.

  • Making History

    In the history log, the Bash shell records all commands typed by the user. Mike Schilli extracts this data with Go for a statistical analysis of his typing behavior.

  • Perl: Lyrical Logfiles

    Instead of just monitoring incoming requests in your web server's logfile, a sound server makes them audible and lets you listen to the tune of users surfing the site.

  • Motion Sensor

    Inotify lets applications subscribe to change notifications in the filesystem. Mike Schilli uses the cross-platform fsnotify library to instruct a Go program to detect what's happening.

  • Programming Snapshot – Go

    To find files quickly in the deeply nested subdirectories of his home directory, Mike whips up a Go program to index file metadata in an SQLite database.

comments powered by Disqus
Subscribe to our Linux Newsletters
Find Linux and Open Source Jobs
Subscribe to our ADMIN Newsletters

Support Our Work

Linux Magazine content is made possible with support from readers like you. Please consider contributing when you’ve found an article to be beneficial.

Learn More

News