import os, shutil import gtk, gobject import help_box from dialog import Dialog, alert from zeroinstall.injector.iface_cache import iface_cache from zeroinstall.injector import basedir, namespaces, model from zeroinstall.zerostore import BadDigest, manifest from zeroinstall import support from treetips import TreeTips ROX_IFACE = 'http://rox.sourceforge.net/2005/interfaces/ROX-Filer' # Model columns ITEM = 0 SELF_SIZE = 1 PRETTY_SIZE = 2 TOOLTIP = 3 ITEM_OBJECT = 4 def popup_menu(bev, obj): menu = gtk.Menu() for i in obj.menu_items: if i is None: item = gtk.SeparatorMenuItem() else: name, cb = i item = gtk.MenuItem(name) item.connect('activate', lambda item, cb=cb: cb(obj)) item.show() menu.append(item) menu.popup(None, None, None, bev.button, bev.time) def size_if_exists(path): "Get the size for a file, or 0 if it doesn't exist." if path and os.path.isfile(path): return os.path.getsize(path) return 0 def get_size(path): "Get the size for a directory tree. Get the size from the .manifest if possible." man = os.path.join(path, '.manifest') if os.path.exists(man): size = os.path.getsize(man) for line in file(man): if line[:1] in "XF": size += long(line.split(' ', 4)[3]) else: size = 0 for root, dirs, files in os.walk(path): for name in files: size += os.path.getsize(os.path.join(root, name)) return size def summary(iface): if iface.summary: return iface.get_name() + ' - ' + iface.summary return iface.get_name() def get_selected_paths(tree_view): "GTK 2.0 doesn't have this built-in" selection = tree_view.get_selection() paths = [] def add(model, path, iter): paths.append(path) selection.selected_foreach(add) return paths tips = TreeTips() # Responses DELETE = 0 class CachedInterface(object): def __init__(self, uri, size): self.uri = uri self.size = size def delete(self): if not self.uri.startswith('/'): cached_iface = basedir.load_first_cache(namespaces.config_site, 'interfaces', model.escape(self.uri)) if cached_iface: #print "Delete", cached_iface os.unlink(cached_iface) user_overrides = basedir.load_first_config(namespaces.config_site, namespaces.config_prog, 'user_overrides', model.escape(self.uri)) if user_overrides: #print "Delete", user_overrides os.unlink(user_overrides) def __cmp__(self, other): return self.uri.__cmp__(other.uri) class ValidInterface(CachedInterface): def __init__(self, iface, size): CachedInterface.__init__(self, iface.uri, size) self.iface = iface self.in_cache = [] def append_to(self, model, iter): iter2 = model.append(iter, [self.uri, self.size, None, summary(self.iface), self]) for cached_impl in self.in_cache: cached_impl.append_to(model, iter2) def get_may_delete(self): for c in self.in_cache: if not isinstance(c, LocalImplementation): return False # Still some impls cached return True may_delete = property(get_may_delete) class InvalidInterface(CachedInterface): may_delete = True def __init__(self, uri, ex, size): CachedInterface.__init__(self, uri, size) self.ex = ex def append_to(self, model, iter): model.append(iter, [self.uri, self.size, None, self.ex, self]) class LocalImplementation: may_delete = False def __init__(self, impl): self.impl = impl def append_to(self, model, iter): model.append(iter, [self.impl.id, 0, None, 'This is a local version, not held in the cache.', self]) class CachedImplementation: may_delete = True def __init__(self, cache_dir, name): self.impl_path = os.path.join(cache_dir, name) self.size = get_size(self.impl_path) self.name = name def delete(self): #print "Delete", self.impl_path shutil.rmtree(self.impl_path) def open_rox(self): os.spawnlp(os.P_WAIT, '0launch', '0launch', ROX_IFACE, '-d', self.impl_path) def verify(self): try: manifest.verify(self.impl_path) except BadDigest, ex: box = gtk.MessageDialog(None, 0, gtk.MESSAGE_WARNING, gtk.BUTTONS_OK, str(ex)) if ex.detail: swin = gtk.ScrolledWindow() buffer = gtk.TextBuffer() mono = buffer.create_tag('mono', family = 'Monospace') buffer.insert_with_tags(buffer.get_start_iter(), ex.detail, mono) text = gtk.TextView(buffer) text.set_editable(False) text.set_cursor_visible(False) swin.add(text) swin.set_shadow_type(gtk.SHADOW_IN) swin.set_border_width(4) box.vbox.pack_start(swin) swin.show_all() box.set_resizable(True) else: box = gtk.MessageDialog(None, 0, gtk.MESSAGE_INFO, gtk.BUTTONS_OK, 'Contents match digest; nothing has been changed.') box.run() box.destroy() menu_items = [('Open in ROX-Filer', open_rox), ('Verify integrity', verify)] class UnusedImplementation(CachedImplementation): def append_to(self, model, iter): model.append(iter, [self.name, self.size, None, self.impl_path, self]) class KnownImplementation(CachedImplementation): def __init__(self, cached_iface, cache_dir, impl, impl_size): CachedImplementation.__init__(self, cache_dir, impl.id) self.cached_iface = cached_iface self.impl = impl self.size = impl_size def delete(self): CachedImplementation.delete(self) self.cached_iface.in_cache.remove(self) def append_to(self, model, iter): model.append(iter, ['Version %s : %s' % (self.impl.get_version(), self.impl.id), self.size, None, None, self]) def __cmp__(self, other): if hasattr(other, 'impl'): return self.impl.__cmp__(other.impl) return -1 class CacheExplorer(Dialog): def __init__(self): Dialog.__init__(self) self.set_title('Zero Install Cache') self.set_default_size(gtk.gdk.screen_width() / 2, gtk.gdk.screen_height() / 2) # Model self.model = gtk.TreeStore(str, int, str, str, object) self.tree_view = gtk.TreeView(self.model) # Tree view swin = gtk.ScrolledWindow() swin.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_ALWAYS) swin.set_shadow_type(gtk.SHADOW_IN) swin.add(self.tree_view) self.vbox.pack_start(swin, True, True, 0) self.tree_view.set_rules_hint(True) swin.show_all() column = gtk.TreeViewColumn('Item', gtk.CellRendererText(), text = ITEM) column.set_resizable(True) self.tree_view.append_column(column) cell = gtk.CellRendererText() cell.set_property('xalign', 1.0) column = gtk.TreeViewColumn('Size', cell, text = PRETTY_SIZE) self.tree_view.append_column(column) def button_press(tree_view, bev): if bev.button != 3: return False pos = tree_view.get_path_at_pos(int(bev.x), int(bev.y)) if not pos: return False path, col, x, y = pos obj = self.model[path][ITEM_OBJECT] if obj and hasattr(obj, 'menu_items'): popup_menu(bev, obj) self.tree_view.connect('button-press-event', button_press) # Tree tooltips def motion(tree_view, ev): if ev.window is not tree_view.get_bin_window(): return False pos = tree_view.get_path_at_pos(int(ev.x), int(ev.y)) if pos: path = pos[0] row = self.model[path] tip = row[TOOLTIP] if tip: if tip != tips.item: tips.prime(tree_view, tip) else: tips.hide() else: tips.hide() self.tree_view.connect('motion-notify-event', motion) self.tree_view.connect('leave-notify-event', lambda tv, ev: tips.hide()) # Responses self.add_button(gtk.STOCK_HELP, gtk.RESPONSE_HELP) self.add_button(gtk.STOCK_CLOSE, gtk.RESPONSE_OK) self.add_button(gtk.STOCK_DELETE, DELETE) self.set_default_response(gtk.RESPONSE_OK) selection = self.tree_view.get_selection() def selection_changed(selection): any_selected = False for x in get_selected_paths(self.tree_view): obj = self.model[x][ITEM_OBJECT] if obj is None or not obj.may_delete: self.set_response_sensitive(DELETE, False) return any_selected = True self.set_response_sensitive(DELETE, any_selected) selection.set_mode(gtk.SELECTION_MULTIPLE) selection.connect('changed', selection_changed) selection_changed(selection) def response(dialog, resp): if resp == gtk.RESPONSE_OK: self.destroy() elif resp == gtk.RESPONSE_HELP: cache_help.display() elif resp == DELETE: self.delete() self.connect('response', response) def delete(self): errors = [] model = self.model paths = get_selected_paths(self.tree_view) paths.reverse() for path in paths: item = model[path][ITEM_OBJECT] assert item.delete try: item.delete() except OSError, ex: errors.append(str(ex)) else: model.remove(model.get_iter(path)) self.update_sizes() if errors: alert(self, "Failed to delete:\n%s" % '\n'.join(errors)) def populate_model(self): # Find cached implementations unowned = {} # Impl ID -> Store duplicates = [] # TODO for s in iface_cache.stores.stores: if os.path.isdir(s.dir): for id in os.listdir(s.dir): if id in unowned: duplicates.append(id) unowned[id] = s ok_interfaces = [] error_interfaces = [] # Look through cached interfaces for implementation owners all = iface_cache.list_all_interfaces() all.sort() for uri in all: iface_size = 0 try: if uri.startswith('/'): cached_iface = uri else: cached_iface = basedir.load_first_cache(namespaces.config_site, 'interfaces', model.escape(uri)) user_overrides = basedir.load_first_config(namespaces.config_site, namespaces.config_prog, 'user_overrides', model.escape(uri)) iface_size = size_if_exists(cached_iface) + size_if_exists(user_overrides) iface = iface_cache.get_interface(uri) except Exception, ex: error_interfaces.append((uri, str(ex), iface_size)) else: cached_iface = ValidInterface(iface, iface_size) for impl in iface.implementations.values(): if impl.id.startswith('/') or impl.id.startswith('.'): cached_iface.in_cache.append(LocalImplementation(impl)) if impl.id in unowned: cached_dir = unowned[impl.id].dir impl_path = os.path.join(cached_dir, impl.id) impl_size = get_size(impl_path) cached_iface.in_cache.append(KnownImplementation(cached_iface, cached_dir, impl, impl_size)) del unowned[impl.id] cached_iface.in_cache.sort() ok_interfaces.append(cached_iface) if error_interfaces: iter = self.model.append(None, [_("Invalid interfaces (unreadable)"), 0, None, _("These interfaces exist in the cache but cannot be " "read. You should probably delete them."), None]) for uri, ex, size in error_interfaces: item = InvalidInterface(uri, ex, size) item.append_to(self.model, iter) unowned_sizes = [] local_dir = os.path.join(basedir.xdg_cache_home, '0install.net', 'implementations') for id in unowned: if unowned[id].dir == local_dir: impl = UnusedImplementation(local_dir, id) unowned_sizes.append((impl.size, impl)) if unowned_sizes: iter = self.model.append(None, [_("Unowned implementations and temporary files"), 0, None, _("These probably aren't needed any longer. You can " "delete them."), None]) unowned_sizes.sort() unowned_sizes.reverse() for size, item in unowned_sizes: item.append_to(self.model, iter) if ok_interfaces: iter = self.model.append(None, [_("Interfaces"), 0, None, _("Interfaces in the cache"), None]) for item in ok_interfaces: item.append_to(self.model, iter) self.update_sizes() def update_sizes(self): """Set PRETTY_SIZE to the total size, including all children.""" m = self.model def update(itr): total = m[itr][SELF_SIZE] child = m.iter_children(itr) while child: total += update(child) child = m.iter_next(child) m[itr][PRETTY_SIZE] = support.pretty_size(total) return total itr = m.get_iter_root() while itr: update(itr) itr = m.iter_next(itr) cache_help = help_box.HelpBox("Cache Explorer Help", ('Overview', """ When you run a program using Zero Install, it downloads the program's 'interface' file, \ which gives information about which versions of the program are available. This interface \ file is stored in the cache to save downloading it next time you run the program. When you have chosen which version (implementation) of the program you want to \ run, Zero Install downloads that version and stores it in the cache too. Zero Install lets \ you have many different versions of each program on your computer at once. This is useful, \ since it lets you use an old version if needed, and different programs may need to use \ different versions of libraries in some cases. The cache viewer shows you all the interfaces and implementations in your cache. \ This is useful to find versions you don't need anymore, so that you can delete them and \ free up some disk space."""), ('Invalid interfaces', """ The cache viewer gets a list of all interfaces in your cache. However, some may not \ be valid; they are shown in the 'Invalid interfaces' section. It should be fine to \ delete these. An invalid interface may be caused by a local interface that no longer \ exists, by a failed attempt to download an interface (the name ends in '.new'), or \ by the interface file format changing since the interface was downloaded."""), ('Unowned implementations and temporary files', """ The cache viewer searches through all the interfaces to find out which implementations \ they use. If no interface uses an implementation, it is shown in the 'Unowned implementations' \ section. Unowned implementations can result from old versions of a program no longer being listed \ in the interface file. Temporary files are created when unpacking an implementation after \ downloading it. If the archive is corrupted, the unpacked files may be left there. Unless \ you are currently unpacking new programs, it should be fine to delete everything in this \ section."""), ('Interfaces', """ All remaining interfaces are listed in this section. You may wish to delete old versions of \ certain programs. Deleting a program which you may later want to run will require it to be downloaded \ again. Deleting a version of a program which is currently running may cause it to crash, so be careful! """))