Welcome, guest | Sign In | My Account | Store | Cart

Have you ever wanted to find out how much room a particular directory was taking up on your hard drive? A roommate of mine in college was having trouble keeping track of where his hard drive space is going, so the following program provided a solution that allows a brief overview of a directory's size along with all of its children. A tree view is created using tkinter and is populated with the directory's name, cumulative size, immediate total file size, and full path. The search button is disabled during a search, and the directory listing with its children is automatically updated.

Python, 232 lines
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
#! /usr/bin/env python
from tkinter import NoDefaultRoot, Tk, ttk, filedialog
from _tkinter import getbusywaitinterval
from tkinter.constants import *
from math import sin, pi
import base64, zlib, os

################################################################################

ICON = b'eJxjYGAEQgEBBiApwZDBzMAgxsDAoAHEQCEGBQaIOAwkQDE2UOSkiUM\
Gp/rlyd740Ugzf8/uXROxAaA4VvVAqcfYAFCcoHqge4hR/+btWwgCqoez8aj//fs\
XWiAARfCrhyCg+XA2HvV/YACoHs4mRj0ywKWe1PD//p+B4QMOmqGeMAYAAY/2nw=='

################################################################################

class GUISizeTree(ttk.Frame):

    @classmethod
    def main(cls):
        # Create the application's root.
        NoDefaultRoot()
        root = Tk()
        # Restrict sizing and add title.
        root.minsize(350, 175)
        root.title('Directory Size')
        # Create the application's icon.
        with open('tree.ico', 'wb') as file:
            file.write(zlib.decompress(base64.b64decode(ICON)))
        root.iconbitmap('tree.ico')
        os.remove('tree.ico')
        # Configure the SizeTree object.
        view = cls(root)
        view.grid(row=0, column=0, sticky=NSEW)
        # Setup the window for resizing.
        root.grid_rowconfigure(0, weight=1)
        root.grid_columnconfigure(0, weight=1)
        # Enter the GUI main event loop.
        root.mainloop()

    def __init__(self, master=None, **kw):
        super().__init__(master, **kw)
        # Configure the progressbar.
        self.__progress = ttk.Progressbar(self, orient=HORIZONTAL)
        self.__progress.grid(row=0, column=0, columnspan=4, sticky=EW)
        # Configure the tree.
        self.__tree = ttk.Treeview(self, selectmode=BROWSE,
                                   columns=('d_size', 'f_size', 'path'))
        self.__tree.heading('#0', text=' Name', anchor=W)
        self.__tree.heading('d_size', text=' Total Size', anchor=W)
        self.__tree.heading('f_size', text=' File Size', anchor=W)
        self.__tree.heading('path', text=' Path', anchor=W)
        self.__tree.column('#0', minwidth=80, width=160)
        self.__tree.column('d_size', minwidth=80, width=160)
        self.__tree.column('f_size', minwidth=80, width=160)
        self.__tree.column('path', minwidth=80, width=160)
        self.__tree.grid(row=1, column=0, columnspan=3, sticky=NSEW)
        # Configure the scrollbar.
        self.__scroll = ttk.Scrollbar(self, orient=VERTICAL,
                                      command=self.__tree.yview)
        self.__tree.configure(yscrollcommand=self.__scroll.set)
        self.__scroll.grid(row=1, column=3, sticky=NS)
        # Configure the path button.
        self.__label = ttk.Button(self, text='Path:', command=self.choose)
        self.__label.bind('<Return>', self.choose)
        self.__label.grid(row=2, column=0)
        # Configure the directory dialog.
        head, tail = os.getcwd(), True
        while tail:
            head, tail = os.path.split(head)
        self.__dialog = filedialog.Directory(self, initialdir=head)
        # Configure the path entry box.
        self.__path = ttk.Entry(self, cursor='xterm')
        self.__path.bind('<Control-Key-a>', self.select_all)
        self.__path.bind('<Control-Key-/>', lambda event: 'break')
        self.__path.bind('<Return>', self.search)
        self.__path.grid(row=2, column=1, sticky=EW)
        self.__path.focus_set()
        # Configure the execution button.
        self.__run = ttk.Button(self, text='Search', command=self.search)
        self.__run.bind('<Return>', self.search)
        self.__run.grid(row=2, column=2)
        # Configure the sizegrip.
        self.__grip = ttk.Sizegrip(self)
        self.__grip.grid(row=2, column=3, sticky=SE)
        # Configure the grid.
        self.grid_rowconfigure(1, weight=1)
        self.grid_columnconfigure(1, weight=1)
        # Configure root item in tree.
        self.__root = None

    def choose(self, event=None):
        # Get a directory path via a dialog.
        path = self.__dialog.show()
        if path:
            # Fill entry box with user path.
            self.__path.delete(0, END)
            self.__path.insert(0, os.path.abspath(path))

    def select_all(self, event):
        # Select the contents of the widget.
        event.widget.selection_range(0, END)
        return 'break'

    def search(self, event=None):
        if self.__run['state'].string == NORMAL:
            # Show background work progress.
            self.__run['state'] = DISABLED
            path = os.path.abspath(self.__path.get())
            if os.path.isdir(path):
                self.__progress.configure(mode='indeterminate', maximum=100)
                self.__progress.start()
                # Search while updating display.
                if self.__root is not None:
                    self.__tree.delete(self.__root)
                tree = SizeTree(self.update, path)
                nodes = tree.total_nodes + 1
                # Build user directory treeview.
                self.__progress.stop()
                self.__progress.configure(mode='determinate', maximum=nodes)
                self.__root = self.__tree.insert('', END, text=tree.name)
                self.build_tree(self.__root, tree)
                # Indicate completion of search.
                self.__run['state'] = NORMAL
            else:
                self.shake()

    def shake(self):
        # Check frame rate.
        assert getbusywaitinterval() == 20, 'Values are hard-coded for 50 FPS.'
        # Get application root.
        root = self
        while not isinstance(root, Tk):
            root = root.master
        # Schedule beginning of animation.
        self.after_idle(self.__shake, root, 0)

    def __shake(self, root, frame):
        frame += 1
        # Get the window's location and update X value.
        x, y = map(int, root.geometry().split('+')[1:])
        x += int(sin(pi * frame / 2.5) * sin(pi * frame / 50) * 5)
        root.geometry('+{}+{}'.format(x, y))
        # Schedule next frame or restore search button.
        if frame < 50:
            self.after(20, self.__shake, root, frame)
        else:
            self.__run['state'] = NORMAL

    def build_tree(self, node, tree):
        # Make changes to the treeview and progress bar.
        text = 'Unknown!' if tree.dir_error else convert(tree.total_size)
        self.__tree.set(node, 'd_size', text)
        text = 'Unknown!' if tree.file_error else convert(tree.file_size)
        self.__tree.set(node, 'f_size', text)
        self.__tree.set(node, 'path', tree.path)
        self.__progress.step()
        # Update the display and extract any child node.
        self.update()
        for child in tree.children:
            subnode = self.__tree.insert(node, END, text=child.name)
            self.build_tree(subnode, child)

################################################################################

class SizeTree:

    "Create a tree structure outlining a directory's size."

    def __init__(self, callback, path):
        callback()
        self.path = path
        head, tail = os.path.split(path)
        self.name = tail or head
        self.children = []
        self.file_size = 0
        self.total_size = 0
        self.total_nodes = 0
        self.file_error = False
        self.dir_error = False
        try:
            dir_list = os.listdir(path)
        except OSError:
            self.dir_error = True
        else:
            for name in dir_list:
                path_name = os.path.join(path, name)
                if os.path.isdir(path_name):
                    size_tree = SizeTree(callback, path_name)
                    self.children.append(size_tree)
                    self.total_size += size_tree.total_size
                    self.total_nodes += size_tree.total_nodes + 1
                elif os.path.isfile(path_name):
                    try:
                        self.file_size += os.path.getsize(path_name)
                    except OSError:
                        self.file_error = True
            self.total_size += self.file_size

################################################################################

def convert(number):
    "Convert bytes into human-readable representation."
    if not number:
        return '0 Bytes'
    assert 0 < number < 1 << 110, 'number out of range'
    ordered = reversed(tuple(format_bytes(partition_number(number, 1 << 10))))
    cleaned = ', '.join(item for item in ordered if item[0] != '0')
    return cleaned

def partition_number(number, base):
    "Continually divide number by base until zero."
    div, mod = divmod(number, base)
    yield mod
    while div:
        div, mod = divmod(div, base)
        yield mod

def format_bytes(parts):
    "Format partitioned bytes into human-readable strings."
    for power, number in enumerate(parts):
        yield '{} {}'.format(number, format_suffix(power, number))

def format_suffix(power, number):
    "Compute the suffix for a certain power of bytes."
    return (PREFIX[power] + 'byte').capitalize() + ('s' if number != 1 else '')

PREFIX = ' kilo mega giga tera peta exa zetta yotta bronto geop'.split(' ')

################################################################################

if __name__ == '__main__':
    GUISizeTree.main()

If you want a command-line version of this program, recipe 577566 was the original inspiration for this utility.

3 comments

Sunjay Varma 13 years ago  # | flag

You should put in a stop/cancel button and pause button.

Steven Oldner 12 years, 7 months ago  # | flag

Pretty Neat! Thanks...

Stephen Chappell (author) 12 years, 7 months ago  # | flag

If you like this program, you might want to check out recipe 577632, recipe 577633, and recipe 577635.