diff --git a/src/gui/bottier.py b/src/gui/bottier.py index 7469f4f..6cfc623 100644 --- a/src/gui/bottier.py +++ b/src/gui/bottier.py @@ -8,8 +8,8 @@ import Tkinter as Tk import logging from StringIO import StringIO -from PIL import Image -from PIL import ImageTk +import Image +import ImageTk # local imports diff --git a/src/misc/DirectoryPruner1.py b/src/misc/DirectoryPruner1.py new file mode 100644 index 0000000..367fa86 --- /dev/null +++ b/src/misc/DirectoryPruner1.py @@ -0,0 +1,993 @@ +#! /usr/bin/env python + +"""Module providing GUI capability to prune any directory. + +The code presented in this module is for the purposes of: (1) ascertaining +the space taken up by a directory, its files, its sub-directories, and its +sub-files; (2) allowing for the removal of the sub-files, sub-directories, +files, and directory found in the first purpose; (3) giving the user a GUI +to accomplish said purposes in a convenient way that is easily accessible.""" + +################################################################################ + +__author__ = 'Stephen "Zero" Chappell ' +__date__ = '15 February 2011' +__version__ = '$Revision: 298 $' + +################################################################################ + +# Import several GUI libraries. +import tkinter +import tkinter.ttk +import tkinter.Filedialog +import tkinter.Messagebox + +# Import other needed modules. +import zlib +import base64 +import os +import math + +################################################################################ + +ICON = b'eJxjYGAEQgEBBiApwZDBzMAgxsDAoAHEQCEGBQaIOAwkQDE2UOSkiUM\ +Gp/rlyd740Ugzf8/uXROxAaA4VvVAqcfYAFCcoHqge4hR/+btWwgCqoez8aj//fs\ +XWiAARfCrhyCg+XA2HvV/YACoHs4mRj0ywKWe1PD//p+B4QMOmqGeMAYAAY/2nw==' + +################################################################################ + +class GUISizeTree(tkinter.ttk.Frame): + + "Widget for examining size of directory with optional deletion." + + WARN = True # Should warnings be made for permanent operations? + MENU = True # Should the (destructive) context menu be enabled? + + # Give names to columns. + CLMS = 'total_size', 'file_size', 'path' + TREE = '#0' + + ######################################################################## + + # Allow direct execution of GUISizeTree widget. + + @classmethod + def main(cls): + "Create an application containing a single GUISizeTree widget." + tkinter.NoDefaultRoot() + root = cls.create_application_root() + cls.attach_window_icon(root, ICON) + view = cls.setup_class_instance(root) + root.mainloop() + + @staticmethod + def create_application_root(): + "Create and configure the main application window." + root = tkinter.Tk() + root.minsize(430, 215) + root.title('Directory Pruner') + root.option_add('*tearOff', tkinter.FALSE) + return root + + @staticmethod + def attach_window_icon(root, icon): + "Generate and use the icon in the window's corner." + with open('tree.ico', 'wb') as file: + file.write(zlib.decompress(base64.b64decode(ICON))) + root.iconbitmap('tree.ico') + os.remove('tree.ico') + + @classmethod + def setup_class_instance(cls, root): + "Build GUISizeTree instance that expects resizing." + instance = cls(root) + instance.grid(row=0, column=0, sticky=tkinter.NSEW) + root.grid_rowconfigure(0, weight=1) + root.grid_columnconfigure(0, weight=1) + return instance + + ######################################################################## + + # Initialize the GUISizeTree object. + + def __init__(self, master=None, **kw): + "Initialize the GUISizeTree instance and configure for operation." + super().__init__(master, **kw) + # Initialize and configure this frame widget. + self.capture_root() + self.create_widgets() + self.create_supports() + self.create_bindings() + self.configure_grid() + self.configure_tree() + self.configure_menu() + # Set focus to path entry. + self.__path.focus_set() + + def capture_root(self): + "Capture the root (Tk instance) of this application." + widget = self.master + while not isinstance(widget, tkinter.Tk): + widget = widget.master + self.__tk = widget + + def create_widgets(self): + "Create all the widgets that will be placed in this frame." + self.__label = tkinter.ttk.Button(self, text='Path:', + command=self.choose) + self.__path = tkinter.ttk.Entry(self, cursor='xterm') + self.__run = tkinter.ttk.Button(self, text='Search', + command=self.search) + self.__cancel = tkinter.ttk.Button(self, text='Cancel', + command=self.stop_search) + self.__progress = tkinter.ttk.Progressbar(self, + orient=tkinter.HORIZONTAL) + self.__tree = tkinter.ttk.Treeview(self, columns=self.CLMS, + selectmode=tkinter.BROWSE) + self.__scroll_1 = tkinter.ttk.Scrollbar(self, orient=tkinter.VERTICAL, + command=self.__tree.yview) + self.__scroll_2 = tkinter.ttk.Scrollbar(self, orient=tkinter.HORIZONTAL, + command=self.__tree.xview) + self.__grip = tkinter.ttk.Sizegrip(self) + + def create_supports(self): + "Create all GUI elements not placed directly in this frame." + self.__menu = tkinter.Menu(self) + self.create_directory_browser() + self.create_error_message() + self.create_warning_message() + + def create_directory_browser(self): + "Find root of file system and create directory browser." + head, tail = os.getcwd(), True + while tail: + head, tail = os.path.split(head) + self.__dialog = tkinter.filedialog.Directory(self, initialdir=head) + + def create_error_message(self): + "Create error message when trying to search bad path." + options = {'title': 'Path Error', + 'icon': tkinter.messagebox.ERROR, + 'type': tkinter.messagebox.OK, + 'message': 'Directory does not exist.'} + self.__error = tkinter.messagebox.Message(self, **options) + + def create_warning_message(self): + "Create warning message for permanent operations." + options = {'title': 'Important Warning', + 'icon': tkinter.messagebox.QUESTION, + 'type': tkinter.messagebox.YESNO, + 'message': '''\ +You cannot undo these operations. +Are you sure you want to do this?'''} + self.__warn = tkinter.messagebox.Message(self, **options) + + def create_bindings(self): + "Bind the widgets to any events they will need to handle." + self.__label.bind('', self.choose) + self.__path.bind('', self.select_all) + self.__path.bind('', lambda event: 'break') + self.__path.bind('', self.search) + self.__run.bind('', self.search) + self.__cancel.bind('', self.stop_search) + self.bind_right_click(self.__tree, self.open_menu) + + @staticmethod + def select_all(event): + "Select all of the contents in this Entry widget." + event.widget.selection_range(0, tkinter.END) + return 'break' + + def bind_right_click(self, widget, action): + "Bind action to widget while considering Apple computers." + if self.__tk.tk.call('tk', 'windowingsystem') == 'aqua': + widget.bind('<2>', action) + widget.bind('', action) + else: + widget.bind('<3>', action) + + def configure_grid(self): + "Place all widgets on the grid in their respective locations." + self.__label.grid(row=0, column=0) + self.__path.grid(row=0, column=1, sticky=tkinter.EW) + self.__run.grid(row=0, column=2, columnspan=2) + self.__run.grid_remove() + self.__cancel.grid(row=0, column=2, columnspan=2) + self.__cancel.grid_remove() + self.__run.grid() + self.__progress.grid(row=1, column=0, columnspan=4, sticky=tkinter.EW) + self.__tree.grid(row=2, column=0, columnspan=3, sticky=tkinter.NSEW) + self.__scroll_1.grid(row=2, column=3, sticky=tkinter.NS) + self.__scroll_2.grid(row=3, column=0, columnspan=3, sticky=tkinter.EW) + self.__grip.grid(row=3, column=3, sticky=tkinter.SE) + # Configure the grid to automatically resize internal widgets. + self.grid_rowconfigure(2, weight=1) + self.grid_columnconfigure(1, weight=1) + + def configure_tree(self): + "Configure the Treeview widget." + # Setup the headings. + self.__tree.heading(self.TREE, text=' Name', anchor=tkinter.W, + command=self.sort_name) + self.__tree.heading(self.CLMS[0], text=' Total Size', anchor=tkinter.W, + command=self.sort_total_size) + self.__tree.heading(self.CLMS[1], text=' File Size', anchor=tkinter.W, + command=self.sort_file_size) + self.__tree.heading(self.CLMS[2], text=' Path', anchor=tkinter.W, + command=self.sort_path) + # Setup the columns. + self.__tree.column(self.TREE, minwidth=100, width=200) + self.__tree.column(self.CLMS[0], minwidth=100, width=200) + self.__tree.column(self.CLMS[1], minwidth=100, width=200) + self.__tree.column(self.CLMS[2], minwidth=100, width=200) + # Connect the Scrollbars. + self.__tree.configure(yscrollcommand=self.__scroll_1.set) + self.__tree.configure(xscrollcommand=self.__scroll_2.set) + + def configure_menu(self): + "Configure the (context) Menu widget." + # Shortcut for narrowing the search. + self.__menu.add_command(label='Search Directory', + command=self.search_dir) + self.__menu.add_separator() + # Operations committed on directory. + self.__menu.add_command(label='Remove Directory', command=self.rm_dir) + self.__menu.add_command(label='Remove Files', command=self.rm_files) + self.__menu.add_separator() + # Operations that recurse on sub-directories. + self.__menu.add_command(label='Remove Sub-directories', + command=self.rm_subdirs) + self.__menu.add_command(label='Remove Sub-files', + command=self.rm_subfiles) + # Only add "Open Directory" command on Windows. + if hasattr(os, 'startfile'): + self.__menu.add_separator() + self.__menu.add_command(label='Open Directory', + command=self.open_dir) + + ######################################################################## + + # This property is used to control access to operations. + + def __get_operations_enabled(self): + "Return if run button is in normal state." + return self.__run['state'].string == tkinter.NORMAL + + def __set_operations_enabled(self, value): + "Enable or disable run button's state according to value." + self.__run['state'] = tkinter.NORMAL if value else tkinter.DISABLED + + operations_enabled = property(__get_operations_enabled, + __set_operations_enabled, + doc="Flag controlling certain operations") + + ######################################################################## + + # Handle path browsing and searching actions. + + def choose(self, event=None): + "Show directory browser and set path as needed." + path = self.__dialog.show() + if path: + # Entry is cleared before absolute path is added. + self.__path.delete(0, tkinter.END) + self.__path.insert(0, os.path.abspath(path)) + + def search(self, event=None): + "Search the path and display the size of the directory." + if self.operations_enabled: + self.operations_enabled = False + # Get absolute path and check existence. + path = os.path.abspath(self.__path.get()) + if os.path.isdir(path): + # Enable operations after finishing search. + self.__search(path) + self.operations_enabled = True + else: + self.shake() + + def __search(self, path): + "Execute the search procedure and display in Treeview." + self.__run.grid_remove() + self.__cancel.grid() + children = self.start_search() + try: + tree = SizeTree(self.update_search, path) + except StopIteration: + self.handle_stop_search(children) + else: + self.finish_search(children, tree) + self.__cancel.grid_remove() + self.__run.grid() + + ######################################################################## + + # Execute various phases of a search. + + def start_search(self): + "Edit the GUI in preparation for executing a search." + self.__stop_search = False + children = Apply(TreeviewNode(self.__tree).children) + children.detach() + self.__progress.configure(mode='indeterminate', maximum=100) + self.__progress.start() + return children + + def update_search(self): + "Check if search has been stopped and update the GUI." + self.validate_search() + self.update() + + def validate_search(self): + "Check that the current search action is valid." + if self.__stop_search: + self.__stop_search = False + raise StopIteration('Search has been canceled!') + + def stop_search(self, event=None): + "Cancel a search by setting its stop flag." + self.__stop_search = True + + def handle_stop_search(self, children): + "Reset the Treeview and Progressbar on premature termination." + children.reattach() + self.__progress.stop() + self.__progress['mode'] = 'determinate' + + def finish_search(self, children, tree): + "Delete old children, update Progressbar, and update Treeview." + children.delete() + self.__progress.stop() + self.__progress.configure(mode='determinate', + maximum=tree.total_nodes+1) + node = TreeviewNode(self.__tree).append(tree.name) + try: + self.build_tree(node, tree) + except StopIteration: + pass + + ######################################################################## + + # Handle Treeview column sorting events initiated by user. + + def sort_name(self): + "Sort children of selected node by name." + TreeviewNode.current(self.__tree).sort_name() + + def sort_total_size(self): + "Sort children of selected node by total size." + TreeviewNode.current(self.__tree).sort_total_size() + + def sort_file_size(self): + "Sort children of selected node by file size." + TreeviewNode.current(self.__tree).sort_file_size() + + def sort_path(self): + "Sort children of selected node by path." + TreeviewNode.current(self.__tree).sort_path() + + ######################################################################## + + # Handle right-click events on the Treeview widget. + + def open_menu(self, event): + "Select Treeview row and show context menu if allowed." + item = event.widget.identify_row(event.y) + if item: + event.widget.selection_set(item) + if self.menu_allowed: + self.__menu.post(event.x_root, event.y_root) + + @property + def menu_allowed(self): + "Check if menu is enabled along with operations." + return self.MENU and self.operations_enabled + + def search_dir(self): + "Search the path of the currently selected row." + path = TreeviewNode.current(self.__tree).path + self.__path.delete(0, tkinter.END) + self.__path.insert(0, path) + self.search() + + def rm_dir(self): + "Remove the currently selected directory." + if self.commit_permanent_operation: + self.do_remove_directory() + + def rm_files(self): + "Remove the files in the currently selected directory." + if self.commit_permanent_operation: + self.do_remove_files() + + def rm_subdirs(self): + "Remove the sub-directories of the currently selected directory." + if self.commit_permanent_operation: + self.do_remove_subdirectories() + + def rm_subfiles(self): + "Remove the sub-files of the currently selected directory." + if self.commit_permanent_operation: + self.do_remove_subfiles() + + @property + def commit_permanent_operation(self): + "Check if warning should be issued before committing operation." + return not self.WARN or self.__warn.show() == tkinter.messagebox.YES + + def open_dir(self): + "Open up the current directory (only available on Windows)." + os.startfile(TreeviewNode.current(self.__tree).path) + + ######################################################################## + + # Execute actions requested by context menu. + + def do_remove_directory(self): + "Remove a directory and all of its sub-directories." + self.begin_rm() + # Get the current Treeview node and delete it. + node = TreeviewNode.current(self.__tree) + directory_size, path = node.total_size, node.path + position, parent = node.position, node.delete(True) + # Delete the entire directory at path. + self.__rm_dir(self.update, path, True, True) + if os.path.isdir(path): + # Add the directory back to the Treeview. + tree = SizeTree(self.update, path) + self.begin_rm_update(tree.total_nodes + 1) + # Rebuild the Treeview under the parent. + node = parent.insert(position, tree.name) + self.build_tree(node, tree) + # New directory size. + total_size = tree.total_size + else: + self.begin_rm_update() + # New directory size. + total_size = 0 + # If the size has changed, update parent nodes. + if directory_size != total_size: + diff = total_size - directory_size + self.update_parents(parent, diff) + self.end_rm() + + def do_remove_files(self): + "Remove all of the files in the selected directory." + # Delete files in the directory and get its new size. + node = TreeviewNode.current(self.__tree) + total_size = self.__rm_files(node.path) + # Update current and parent nodes if the size changed. + if node.file_size != total_size: + diff = total_size - node.file_size + node.file_size = total_size + node.total_size += diff + self.update_parents(node.parent, diff) + + def do_remove_subdirectories(self): + "Remove all subdirectories in the directory." + self.begin_rm() + # Remove all the children nodes in Viewtree. + node = TreeviewNode.current(self.__tree) + for child in node.children: + child.delete() + # Delete all of the subdirectories and their files. + self.__rm_dir(self.update, node.path, True) + # Find out what subdirectories could not be deteled. + tree = SizeTree(self.update, node.path) + self.begin_rm_update(tree.total_nodes) + if tree.total_nodes: + # Rebuild the Viewtree as needed. + self.build_tree(node, tree, False) + # Fix node and prepare to update parents. + diff = node.total_size - tree.total_size + node.total_size = tree.total_size + else: + # Fix node and prepare to update parents. + diff = node.file_size - node.total_size + node.total_size = node.file_size + # Update parents with new size. + self.update_parents(node.parent, diff) + self.end_rm() + + def do_remove_subfiles(self): + "Remove all subfiles while keeping subdirectories in place." + self.begin_rm() + # Delete all subfiles from current directory. + node = TreeviewNode.current(self.__tree) + self.__rm_dir(self.update, node.path) + # Build a new SizeTree to find the result. + tree = SizeTree(self.update, node.path) + self.begin_rm_update(tree.total_nodes) + # Record the difference and patch the Viewtree. + diff = tree.total_size - node.total_size + self.patch_tree(node, tree) + # Fix all parent nodes with the correct size. + self.update_parents(node.parent, diff) + self.end_rm() + + ######################################################################## + + # Help update Progressbar in removal process. + + def begin_rm(self): + "Start a long-running removal operation." + self.operations_enabled = False + self.__progress.configure(mode='indeterminate', maximum=100) + self.__progress.start() + + def begin_rm_update(self, nodes=0): + "Move to determinate mode of updating the Viewtree." + self.__progress.stop() + self.__progress.configure(mode='determinate', maximum=nodes) + + def end_rm(self): + "Finish removal process by enabling operations." + self.operations_enabled = True + + ######################################################################## + + # Help in removing directories and files. + + @staticmethod + def __rm_dir(callback, path, rm_dir=False, rm_root=False): + "Remove directory at path, respecting the flags." + for root, dirs, files in os.walk(path, False): + # Ignore path if rm_root is false. + if rm_root or root != path: + callback() + for name in files: + file_name = os.path.join(root, name) + # Remove file while catching errors. + try: + os.remove(file_name) + except OSError: + pass + # Ignore directory if rm_dir is false. + if rm_dir: + try: + os.rmdir(root) + except OSError: + pass + + @staticmethod + def __rm_files(path): + "Remove files in path and get remaining space." + total_size = 0 + # Find all files in directory of path. + for name in os.listdir(path): + path_name = os.path.join(path, name) + if os.path.isfile(path_name): + # Try to remove any file that may have been found. + try: + os.remove(path_name) + except OSError: + try: + # If there was an error, try to get the filesize. + total_size += os.path.getsize(path_name) + except OSError: + pass + # Return best guess of space still occupied. + return total_size + + ######################################################################## + + # Update the Viewtree nodes after creating a SizeTree object. + + def build_tree(self, node, tree, update_node=True): + "Build the Treeview while updating the Progressbar." + self.validate_search() + if update_node: + self.sync_nodes(node, tree) + self.add_children(node, tree) + + def sync_nodes(self, node, tree): + "Update attributes on node and refresh GUI." + # Copy the information on the node. + node.total_size = tree.total_size + node.file_size = tree.file_size + node.path = tree.path + # Update the Progressbar and GUI. + self.__progress.step() + self.update() + + def patch_tree(self, node, tree): + "Patch differences between node and tree." + node.total_size = tree.total_size + node.file_size = tree.file_size + self.patch_children(node, tree) + self.add_children(node, tree) + + def add_children(self, node, tree): + "Build and traverse all child nodes." + for child in tree.children: + subnode = node.append(child.name) + self.build_tree(subnode, child) + + def patch_children(self, node, tree): + "Patch Viewtree based on children of SizeTree." + for subnode in node.children: + child = tree.pop_child(subnode.name) + if child is None: + # Directory is gone. + subnode.delete() + else: + # Dig down further in tree. + self.__progress.step() + self.update() + self.patch_tree(subnode, child) + + @staticmethod + def update_parents(node, diff): + "Add in difference to node and parents." + while not node.root: + node.total_size += diff + node = node.parent + + ######################################################################## + + # Show an error when searching paths that do not exist. + + def shake(self, force=False): + "Prepare to shake the application's root window." + if force: + tkinter._tkinter.setbusywaitinterval(20) + elif tkinter._tkinter.getbusywaitinterval() != 20: + # Show error message if not running at 50 FPS. + self.__error.show() + self.operations_enabled = True + return + # Shake window at 50 FPS. + self.after_idle(self.__shake) + + def __shake(self, frame=0): + "Animate each step of shaking the root window." + frame += 1 + # Get the window's location and update the X position. + x, y = map(int, self.__tk.geometry().split('+')[1:]) + x += round(math.sin(math.pi * frame / 2.5) * \ + math.sin(math.pi * frame / 50) * 5) + self.__tk.geometry('+{}+{}'.format(x, y)) + if frame < 50: + # Schedule next step in the animation. + self.after(20, self.__shake, frame) + else: + # Enable operations after one second. + self.operations_enabled = True + +################################################################################ + +class TreeviewNode: + + "Interface to allow easier interaction with Treeview instance." + + @classmethod + def current(cls, tree): + "Take a tree view and return its currently selected node." + node = tree.selection() + return cls(tree, node[0] if node else node) + + ######################################################################## + + # Standard Treeview Operations + + __slots__ = '__tree', '__node' + + def __init__(self, tree, node=''): + "Initialize the TreeviewNode object (root if node not given)." + self.__tree = tree + self.__node = node + + def __str__(self): + "Return a string representation of this node." + return '''\ +NODE: {!r} + Name: {} + Total Size: {} + File Size: {} + Path {}\ +'''.format(self.__node, self.name, self.total_size, self.file_size, self.path) + + def insert(self, position, text): + "Insert a new node with text at position in current node." + node = self.__tree.insert(self.__node, position, text=text) + return TreeviewNode(self.__tree, node) + + def append(self, text): + "Add a new node with text to the end of this node." + return self.insert(tkinter.END, text) + + def move(self, parent, index): + "Insert this node under parent at index." + self.__tree.move(self.__node, parent, index) + + def reattach(self, parent='', index=tkinter.END): + "Attach node to parent at index (defaults to end of root)." + self.move(parent, index) + + def detach(self): + "Unlink this node from its parent but do not delete." + self.__tree.detach(self.__node) + + def delete(self, get_parent=False): + "Delete this node (optionally, return parent)." + if self.__tree.exists(self.__node): + parent = self.parent if get_parent else None + self.__tree.delete(self.__node) + return parent + assert not get_parent, 'Cannot return parent!' + + ######################################################################## + + # Standard Treeview Properties + + @property + def root(self): + "Return if this is the root node." + return self.__node == '' + + @property + def parent(self): + "Return the parent of this node." + return TreeviewNode(self.__tree, self.__tree.parent(self.__node)) + + @property + def level(self): + "Return number of levels this node is under root." + count, node = 0, self + while not node.root: + node = node.parent + count += 1 + return count + + @property + def position(self): + "Return the position of this node in its parent." + return self.__tree.index(self.__node) + + @property + def expanded(self): + "Return whether or not the node is current open." + value = self.__tree.item(self.__node, 'open') + return bool(value) and value.string == 'true' + + @property + def children(self): + "Yield back each child of this node." + for child in self.__tree.get_children(self.__node): + yield TreeviewNode(self.__tree, child) + + ######################################################################## + + # Custom Treeview Properties + # (specific for application) + + @property + def name(self): + "Return the name of this node (tree column)." + return self.__tree.item(self.__node, 'text') + + def __get_total_size(self): + return parse(self.__tree.set(self.__node, GUISizeTree.CLMS[0])) + + def __set_total_size(self, value): + self.__tree.set(self.__node, GUISizeTree.CLMS[0], convert(value)) + + total_size = property(__get_total_size, __set_total_size, + doc="Total size of this node (first column)") + + def __get_file_size(self): + return parse(self.__tree.set(self.__node, GUISizeTree.CLMS[1])) + + def __set_file_size(self, value): + self.__tree.set(self.__node, GUISizeTree.CLMS[1], convert(value)) + + file_size = property(__get_file_size, __set_file_size, + doc="File size of this node (second column)") + + def __get_path(self): + return self.__tree.set(self.__node, GUISizeTree.CLMS[2]) + + def __set_path(self, value): + self.__tree.set(self.__node, GUISizeTree.CLMS[2], value) + + path = property(__get_path, __set_path, + doc="Path of this node (third column)") + + ######################################################################## + + # Custom Treeview Sort Order + # (specific for application) + + def sort_name(self): + "If the node is open, sort its children by name." + self.__sort(lambda child: child.name) + + def sort_total_size(self): + "If the node is open, sort its children by total size." + self.__sort(lambda child: child.total_size) + + def sort_file_size(self): + "If the node is open, sort its children by file size." + self.__sort(lambda child: child.file_size) + + def sort_path(self): + "If the node is open, sort its children by path." + self.__sort(lambda child: child.path) + + def __sort(self, key): + "Sort an expanded node's children by the given key." + if self.expanded: + nodes = list(self.children) + order = sorted(nodes, key=key) + if order == nodes: + order = reversed(order) + for child in order: + self.__tree.move(child.__node, self.__node, tkinter.END) + +################################################################################ + +class SizeTree: + + "Create a tree structure outlining a directory's size." + + __slots__ = 'name path children file_size total_size total_nodes'.split() + + def __init__(self, callback, path): + "Initialize the SizeTree object and search the path while updating." + callback() # Allow the GUI to be updated. + head, tail = os.path.split(path) + # Create attributes for this instance. + self.name = tail or head + self.path = path + self.children = [] + self.file_size = 0 + self.total_size = 0 + self.total_nodes = 0 + # Try searching this directory. + try: + dir_list = os.listdir(path) + except OSError: + pass + else: + # Examine each object in this directory. + for name in dir_list: + path_name = os.path.join(path, name) + if os.path.isdir(path_name): + # Create child nodes for subdirectories. + 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 getting the size of files. + try: + self.file_size += os.path.getsize(path_name) + except OSError: + pass + # Add in the total file size to the total size. + self.total_size += self.file_size + + def pop_child(self, name): + "Return a named child or None if not found." + for index, child in enumerate(self.children): + if child.name == name: + return self.children.pop(index) + + ######################################################################## + + def __str__(self): + "Return a representation of the tree formed by this object." + lines = [self.path] + self.__walk(lines, self.children, '') + return '\n'.join(lines) + + @classmethod + def __walk(cls, lines, children, prefix): + "Generate lines based on children and keep track of prefix." + dir_prefix, walk_prefix = prefix + '+---', prefix + '| ' + for pos, neg, child in cls.__enumerate(children): + if neg == -1: + dir_prefix, walk_prefix = prefix + '\\---', prefix + ' ' + lines.append(dir_prefix + child.name) + cls.__walk(lines, child.children, walk_prefix) + + @staticmethod + def __enumerate(sequence): + "Generate positive and negative indices for sequence." + length = len(sequence) + for count, value in enumerate(sequence): + yield count, count - length, value + +################################################################################ + +class Apply(tuple): + + "Create a container that can run a method from its contents." + + def __getattr__(self, name): + "Get a virtual method to map and apply to the contents." + return self.__Method(self, name) + + ######################################################################## + + class __Method: + + "Provide a virtual method that can be called on the array." + + def __init__(self, array, name): + "Initialize the method with array and method name." + self.__array = array + self.__name = name + + def __call__(self, *args, **kwargs): + "Execute method on contents with provided arguments." + name, error, buffer = self.__name, False, [] + for item in self.__array: + attr = getattr(item, name) + try: + data = attr(*args, **kwargs) + except Exception as problem: + error = problem + else: + if not error: + buffer.append(data) + if error: + raise error + return tuple(buffer) + +################################################################################ + +# Provide a way of converting byte sizes into strings. + +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(' ') + +################################################################################ + +# Allow conversion of byte size strings back into numbers. + +def parse(string): + "Convert human-readable string back into bytes." + total = 0 + for part in string.split(', '): + number, unit = part.split(' ') + s = number != '1' and 's' or '' + for power, prefix in enumerate(PREFIX): + if unit == (prefix + 'byte' + s).capitalize(): + break + else: + raise ValueError('{!r} not found!'.format(unit)) + total += int(number) * 1 << 10 * power + return total + +################################################################################ + +# Execute the main method if ran directly. + +if __name__ == '__main__': + GUISizeTree.main() diff --git a/src/misc/main.py b/src/misc/main.py deleted file mode 100644 index 0125816..0000000 --- a/src/misc/main.py +++ /dev/null @@ -1,120 +0,0 @@ -#!/usr/bin/env python - -# Copyright (c) 2009 Andreas Balogh -# See LICENSE for details. - -""" new module template """ - -# system imports - -import logging -import sys -import os -import getopt - -# local imports - -# constants - -# globals - -LOG = logging.getLogger() - -logging.basicConfig(level=logging.DEBUG, - format="%(asctime)s %(levelname).3s %(process)d:%(thread)d %(message)s", - datefmt="%H:%M:%S") - -# definitions - -class Usage(Exception): - pass - -class Error(Exception): - pass - -def main(argv = [__name__]): - try: - # check for parameters - LOG.debug("starting '%s %s'", argv[0], " ".join(argv[1:])) - script_name = os.path.basename(argv[0]) - try: - opts, args = getopt.getopt(argv[1:], "hfgp", \ - ["help", "force", "gui", "preview"]) - except getopt.error, err: - raise Usage(err) - LOG.debug("opts: %s, args: %s", opts, args) - o_overwrite = False - o_gui = False - o_preview = False - for o, a in opts: - if o in ("-h", "--help"): - usage(script_name) - return 0 - elif o in ("-f", "--force"): - o_overwrite = True - elif o in ("-p", "--preview"): - o_preview = True - elif o in ("-g", "--gui"): - o_gui = True - if len(args) == 2: - src_dir = args[0] - dest_dir = args[1] - elif len(args) == 1 : - src_dir = args[0] - dest_dir = args[0] - elif len(args) == 0 : - src_dir = None - dest_dir = None - o_gui = True - else: - raise Usage("more than two arguments provided") - # call method with appropriate arguments - if src_dir and not os.path.exists(src_dir): - raise Error("Source directory not found [%s], aborting" % (src_dir, )) - if dest_dir and not os.path.exists(dest_dir): - LOG.warn("Destination directory not found [%s]", dest_dir) - if not o_preview: - LOG.info("Creating destination directory [%s]", dest_dir) - os.makedirs(dest_dir) - if o_gui: - gui(src_dir, dest_dir, o_overwrite) - else: - cli(src_dir, dest_dir, o_preview, o_overwrite) - LOG.debug("Done.") - return 0 - except Error, err: - LOG.error(err) - return 1 - except Usage, err: - LOG.error(err) - LOG.info("for usage use -h or --help") - return 2 - - -def gui(src_dir, dest_dir, o_overwrite): - """ graphical user interface """ - print src_dir, dest_dir, o_overwrite - - -def cli(src_dir, dest_dir, o_preview, o_overwrite): - """ command line interface """ - print src_dir, dest_dir, o_preview, o_overwrite - - -def usage(script_name): - print - print "usage: %s [options] [src_dir [dest_dir]]" % (script_name,) - print """ - src_dir source directory to search for MOD/MOI - dest_dir destination directory for MPG files -options: - -h, --help show this help message and exit - -f, --force override files with same name in destination directory - -g, --gui force interactive mode - -p, --preview preview only, don't copy, don't create non-existent directories -""" - - -if __name__ == "__main__": - sys.exit(main(sys.argv)) - \ No newline at end of file