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.
« Previous 1 2 3 4 Next »
Buy this article as PDF
(incl. VAT)
Buy Linux Magazine
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.
News
-
SUSE Renames Several Products for Better Name Recognition
SUSE has been a very powerful player in the European market, but it knows it must branch out to gain serious traction. Will a name change do the trick?
-
ESET Discovers New Linux Malware
WolfsBane is an all-in-one malware that has hit the Linux operating system and includes a dropper, a launcher, and a backdoor.
-
New Linux Kernel Patch Allows Forcing a CPU Mitigation
Even when CPU mitigations can consume precious CPU cycles, it might not be a bad idea to allow users to enable them, even if your machine isn't vulnerable.
-
Red Hat Enterprise Linux 9.5 Released
Notify your friends, loved ones, and colleagues that the latest version of RHEL is available with plenty of enhancements.
-
Linux Sees Massive Performance Increase from a Single Line of Code
With one line of code, Intel was able to increase the performance of the Linux kernel by 4,000 percent.
-
Fedora KDE Approved as an Official Spin
If you prefer the Plasma desktop environment and the Fedora distribution, you're in luck because there's now an official spin that is listed on the same level as the Fedora Workstation edition.
-
New Steam Client Ups the Ante for Linux
The latest release from Steam has some pretty cool tricks up its sleeve.
-
Gnome OS Transitioning Toward a General-Purpose Distro
If you're looking for the perfectly vanilla take on the Gnome desktop, Gnome OS might be for you.
-
Fedora 41 Released with New Features
If you're a Fedora fan or just looking for a Linux distribution to help you migrate from Windows, Fedora 41 might be just the ticket.
-
AlmaLinux OS Kitten 10 Gives Power Users a Sneak Preview
If you're looking to kick the tires of AlmaLinux's upstream version, the developers have a purrfect solution.