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.
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.
You should put in a stop/cancel button and pause button.
Pretty Neat! Thanks...
If you like this program, you might want to check out recipe 577632, recipe 577633, and recipe 577635.