Added "0store manage" command to show the GTK cache viewer.
[zeroinstall/zeroinstall-rsl.git] / zeroinstall / gtkui / cache.py
blobb1c601f2811039e13e1a17f7abe03150c543ee43
1 """Display the contents of the implementation cache."""
2 # Copyright (C) 2008, Thomas Leonard
3 # See the README file for details, or visit http://0install.net.
5 import os, shutil
6 import gtk
8 from zeroinstall.injector.iface_cache import iface_cache
9 from zeroinstall.injector import namespaces, model
10 from zeroinstall.zerostore import BadDigest, manifest
11 from zeroinstall import support
12 from zeroinstall.support import basedir
13 from zeroinstall.gtkui.treetips import TreeTips
14 from zeroinstall.gtkui import help_box, gtkutils
16 _ = lambda x: x
18 __all__ = ['CacheExplorer']
20 ROX_IFACE = 'http://rox.sourceforge.net/2005/interfaces/ROX-Filer'
22 # Model columns
23 ITEM = 0
24 SELF_SIZE = 1
25 PRETTY_SIZE = 2
26 TOOLTIP = 3
27 ITEM_OBJECT = 4
29 def popup_menu(bev, obj):
30 menu = gtk.Menu()
31 for i in obj.menu_items:
32 if i is None:
33 item = gtk.SeparatorMenuItem()
34 else:
35 name, cb = i
36 item = gtk.MenuItem(name)
37 item.connect('activate', lambda item, cb=cb: cb(obj))
38 item.show()
39 menu.append(item)
40 menu.popup(None, None, None, bev.button, bev.time)
42 def size_if_exists(path):
43 "Get the size for a file, or 0 if it doesn't exist."
44 if path and os.path.isfile(path):
45 return os.path.getsize(path)
46 return 0
48 def get_size(path):
49 "Get the size for a directory tree. Get the size from the .manifest if possible."
50 man = os.path.join(path, '.manifest')
51 if os.path.exists(man):
52 size = os.path.getsize(man)
53 for line in file(man):
54 if line[:1] in "XF":
55 size += long(line.split(' ', 4)[3])
56 else:
57 size = 0
58 for root, dirs, files in os.walk(path):
59 for name in files:
60 size += os.path.getsize(os.path.join(root, name))
61 return size
63 def summary(iface):
64 if iface.summary:
65 return iface.get_name() + ' - ' + iface.summary
66 return iface.get_name()
68 def get_selected_paths(tree_view):
69 "GTK 2.0 doesn't have this built-in"
70 selection = tree_view.get_selection()
71 paths = []
72 def add(model, path, iter):
73 paths.append(path)
74 selection.selected_foreach(add)
75 return paths
77 tips = TreeTips()
79 # Responses
80 DELETE = 0
82 class CachedInterface(object):
83 def __init__(self, uri, size):
84 self.uri = uri
85 self.size = size
87 def delete(self):
88 if not self.uri.startswith('/'):
89 cached_iface = basedir.load_first_cache(namespaces.config_site,
90 'interfaces', model.escape(self.uri))
91 if cached_iface:
92 #print "Delete", cached_iface
93 os.unlink(cached_iface)
94 user_overrides = basedir.load_first_config(namespaces.config_site,
95 namespaces.config_prog,
96 'user_overrides', model.escape(self.uri))
97 if user_overrides:
98 #print "Delete", user_overrides
99 os.unlink(user_overrides)
101 def __cmp__(self, other):
102 return self.uri.__cmp__(other.uri)
104 class ValidInterface(CachedInterface):
105 def __init__(self, iface, size):
106 CachedInterface.__init__(self, iface.uri, size)
107 self.iface = iface
108 self.in_cache = []
110 def append_to(self, model, iter):
111 iter2 = model.append(iter,
112 [self.uri, self.size, None, summary(self.iface), self])
113 for cached_impl in self.in_cache:
114 cached_impl.append_to(model, iter2)
116 def get_may_delete(self):
117 for c in self.in_cache:
118 if not isinstance(c, LocalImplementation):
119 return False # Still some impls cached
120 return True
122 may_delete = property(get_may_delete)
124 class InvalidInterface(CachedInterface):
125 may_delete = True
127 def __init__(self, uri, ex, size):
128 CachedInterface.__init__(self, uri, size)
129 self.ex = ex
131 def append_to(self, model, iter):
132 model.append(iter, [self.uri, self.size, None, self.ex, self])
134 class LocalImplementation:
135 may_delete = False
137 def __init__(self, impl):
138 self.impl = impl
140 def append_to(self, model, iter):
141 model.append(iter, [self.impl.id, 0, None, 'This is a local version, not held in the cache.', self])
143 class CachedImplementation:
144 may_delete = True
146 def __init__(self, cache_dir, name):
147 self.impl_path = os.path.join(cache_dir, name)
148 self.size = get_size(self.impl_path)
149 self.name = name
151 def delete(self):
152 #print "Delete", self.impl_path
153 support.ro_rmtree(self.impl_path)
155 def open_rox(self):
156 os.spawnlp(os.P_WAIT, '0launch', '0launch', ROX_IFACE, '-d', self.impl_path)
158 def verify(self):
159 try:
160 manifest.verify(self.impl_path)
161 except BadDigest, ex:
162 box = gtk.MessageDialog(None, 0,
163 gtk.MESSAGE_WARNING, gtk.BUTTONS_OK, str(ex))
164 if ex.detail:
165 swin = gtk.ScrolledWindow()
166 buffer = gtk.TextBuffer()
167 mono = buffer.create_tag('mono', family = 'Monospace')
168 buffer.insert_with_tags(buffer.get_start_iter(), ex.detail, mono)
169 text = gtk.TextView(buffer)
170 text.set_editable(False)
171 text.set_cursor_visible(False)
172 swin.add(text)
173 swin.set_shadow_type(gtk.SHADOW_IN)
174 swin.set_border_width(4)
175 box.vbox.pack_start(swin)
176 swin.show_all()
177 box.set_resizable(True)
178 else:
179 box = gtk.MessageDialog(None, 0,
180 gtk.MESSAGE_INFO, gtk.BUTTONS_OK,
181 'Contents match digest; nothing has been changed.')
182 box.run()
183 box.destroy()
185 menu_items = [('Open in ROX-Filer', open_rox),
186 ('Verify integrity', verify)]
188 class UnusedImplementation(CachedImplementation):
189 def append_to(self, model, iter):
190 model.append(iter, [self.name, self.size, None, self.impl_path, self])
192 class KnownImplementation(CachedImplementation):
193 def __init__(self, cached_iface, cache_dir, impl, impl_size):
194 CachedImplementation.__init__(self, cache_dir, impl.id)
195 self.cached_iface = cached_iface
196 self.impl = impl
197 self.size = impl_size
199 def delete(self):
200 CachedImplementation.delete(self)
201 self.cached_iface.in_cache.remove(self)
203 def append_to(self, model, iter):
204 model.append(iter,
205 ['Version %s : %s' % (self.impl.get_version(), self.impl.id),
206 self.size, None,
207 None,
208 self])
210 def __cmp__(self, other):
211 if hasattr(other, 'impl'):
212 return self.impl.__cmp__(other.impl)
213 return -1
215 class CacheExplorer:
216 """A graphical interface for viewing the cache and deleting old items."""
217 def __init__(self):
218 widgets = gtkutils.Template(os.path.join(os.path.dirname(__file__), 'cache.glade'), 'cache')
219 self.window = window = widgets.get_widget('cache')
220 window.set_default_size(gtk.gdk.screen_width() / 2, gtk.gdk.screen_height() / 2)
222 # Model
223 self.model = gtk.TreeStore(str, int, str, str, object)
224 self.tree_view = widgets.get_widget('treeview')
225 self.tree_view.set_model(self.model)
227 column = gtk.TreeViewColumn('Item', gtk.CellRendererText(), text = ITEM)
228 column.set_resizable(True)
229 self.tree_view.append_column(column)
231 cell = gtk.CellRendererText()
232 cell.set_property('xalign', 1.0)
233 column = gtk.TreeViewColumn('Size', cell, text = PRETTY_SIZE)
234 self.tree_view.append_column(column)
236 def button_press(tree_view, bev):
237 if bev.button != 3:
238 return False
239 pos = tree_view.get_path_at_pos(int(bev.x), int(bev.y))
240 if not pos:
241 return False
242 path, col, x, y = pos
243 obj = self.model[path][ITEM_OBJECT]
244 if obj and hasattr(obj, 'menu_items'):
245 popup_menu(bev, obj)
246 self.tree_view.connect('button-press-event', button_press)
248 # Tree tooltips
249 def motion(tree_view, ev):
250 if ev.window is not tree_view.get_bin_window():
251 return False
252 pos = tree_view.get_path_at_pos(int(ev.x), int(ev.y))
253 if pos:
254 path = pos[0]
255 row = self.model[path]
256 tip = row[TOOLTIP]
257 if tip:
258 if tip != tips.item:
259 tips.prime(tree_view, tip)
260 else:
261 tips.hide()
262 else:
263 tips.hide()
265 self.tree_view.connect('motion-notify-event', motion)
266 self.tree_view.connect('leave-notify-event', lambda tv, ev: tips.hide())
268 # Responses
269 window.set_default_response(gtk.RESPONSE_OK)
271 selection = self.tree_view.get_selection()
272 def selection_changed(selection):
273 any_selected = False
274 for x in get_selected_paths(self.tree_view):
275 obj = self.model[x][ITEM_OBJECT]
276 if obj is None or not obj.may_delete:
277 window.set_response_sensitive(DELETE, False)
278 return
279 any_selected = True
280 window.set_response_sensitive(DELETE, any_selected)
281 selection.set_mode(gtk.SELECTION_MULTIPLE)
282 selection.connect('changed', selection_changed)
283 selection_changed(selection)
285 def response(dialog, resp):
286 if resp == gtk.RESPONSE_OK:
287 window.destroy()
288 elif resp == gtk.RESPONSE_HELP:
289 cache_help.display()
290 elif resp == DELETE:
291 self._delete()
292 window.connect('response', response)
294 def _delete(self):
295 errors = []
297 model = self.model
298 paths = get_selected_paths(self.tree_view)
299 paths.reverse()
300 for path in paths:
301 item = model[path][ITEM_OBJECT]
302 assert item.delete
303 try:
304 item.delete()
305 except OSError, ex:
306 errors.append(str(ex))
307 else:
308 model.remove(model.get_iter(path))
309 self._update_sizes()
311 if errors:
312 gtkutils.show_message_box(self, "Failed to delete:\n%s" % '\n'.join(errors))
314 def show(self):
315 """Display the window and scan the caches to populate it."""
316 self.window.show()
317 self.window.window.set_cursor(gtkutils.get_busy_pointer())
318 gtk.gdk.flush()
319 try:
320 self._populate_model()
321 i = self.model.get_iter_root()
322 while i:
323 self.tree_view.expand_row(self.model.get_path(i), False)
324 i = self.model.iter_next(i)
325 finally:
326 self.window.window.set_cursor(None)
328 def _populate_model(self):
329 # Find cached implementations
331 unowned = {} # Impl ID -> Store
332 duplicates = [] # TODO
334 for s in iface_cache.stores.stores:
335 if os.path.isdir(s.dir):
336 for id in os.listdir(s.dir):
337 if id in unowned:
338 duplicates.append(id)
339 unowned[id] = s
341 ok_interfaces = []
342 error_interfaces = []
344 # Look through cached interfaces for implementation owners
345 all = iface_cache.list_all_interfaces()
346 all.sort()
347 for uri in all:
348 iface_size = 0
349 try:
350 if uri.startswith('/'):
351 cached_iface = uri
352 else:
353 cached_iface = basedir.load_first_cache(namespaces.config_site,
354 'interfaces', model.escape(uri))
355 user_overrides = basedir.load_first_config(namespaces.config_site,
356 namespaces.config_prog,
357 'user_overrides', model.escape(uri))
359 iface_size = size_if_exists(cached_iface) + size_if_exists(user_overrides)
360 iface = iface_cache.get_interface(uri)
361 except Exception, ex:
362 error_interfaces.append((uri, str(ex), iface_size))
363 else:
364 cached_iface = ValidInterface(iface, iface_size)
365 for impl in iface.implementations.values():
366 if impl.id.startswith('/') or impl.id.startswith('.'):
367 cached_iface.in_cache.append(LocalImplementation(impl))
368 if impl.id in unowned:
369 cached_dir = unowned[impl.id].dir
370 impl_path = os.path.join(cached_dir, impl.id)
371 impl_size = get_size(impl_path)
372 cached_iface.in_cache.append(KnownImplementation(cached_iface, cached_dir, impl, impl_size))
373 del unowned[impl.id]
374 cached_iface.in_cache.sort()
375 ok_interfaces.append(cached_iface)
377 if error_interfaces:
378 iter = self.model.append(None, [_("Invalid interfaces (unreadable)"),
379 0, None,
380 _("These interfaces exist in the cache but cannot be "
381 "read. You should probably delete them."),
382 None])
383 for uri, ex, size in error_interfaces:
384 item = InvalidInterface(uri, ex, size)
385 item.append_to(self.model, iter)
387 unowned_sizes = []
388 local_dir = os.path.join(basedir.xdg_cache_home, '0install.net', 'implementations')
389 for id in unowned:
390 if unowned[id].dir == local_dir:
391 impl = UnusedImplementation(local_dir, id)
392 unowned_sizes.append((impl.size, impl))
393 if unowned_sizes:
394 iter = self.model.append(None, [_("Unowned implementations and temporary files"),
395 0, None,
396 _("These probably aren't needed any longer. You can "
397 "delete them."), None])
398 unowned_sizes.sort()
399 unowned_sizes.reverse()
400 for size, item in unowned_sizes:
401 item.append_to(self.model, iter)
403 if ok_interfaces:
404 iter = self.model.append(None,
405 [_("Interfaces"),
406 0, None,
407 _("Interfaces in the cache"),
408 None])
409 for item in ok_interfaces:
410 item.append_to(self.model, iter)
411 self._update_sizes()
413 def _update_sizes(self):
414 """Set PRETTY_SIZE to the total size, including all children."""
415 m = self.model
416 def update(itr):
417 total = m[itr][SELF_SIZE]
418 child = m.iter_children(itr)
419 while child:
420 total += update(child)
421 child = m.iter_next(child)
422 m[itr][PRETTY_SIZE] = support.pretty_size(total)
423 return total
424 itr = m.get_iter_root()
425 while itr:
426 update(itr)
427 itr = m.iter_next(itr)
429 cache_help = help_box.HelpBox("Cache Explorer Help",
430 ('Overview', """
431 When you run a program using Zero Install, it downloads the program's 'interface' file, \
432 which gives information about which versions of the program are available. This interface \
433 file is stored in the cache to save downloading it next time you run the program.
435 When you have chosen which version (implementation) of the program you want to \
436 run, Zero Install downloads that version and stores it in the cache too. Zero Install lets \
437 you have many different versions of each program on your computer at once. This is useful, \
438 since it lets you use an old version if needed, and different programs may need to use \
439 different versions of libraries in some cases.
441 The cache viewer shows you all the interfaces and implementations in your cache. \
442 This is useful to find versions you don't need anymore, so that you can delete them and \
443 free up some disk space."""),
445 ('Invalid interfaces', """
446 The cache viewer gets a list of all interfaces in your cache. However, some may not \
447 be valid; they are shown in the 'Invalid interfaces' section. It should be fine to \
448 delete these. An invalid interface may be caused by a local interface that no longer \
449 exists, by a failed attempt to download an interface (the name ends in '.new'), or \
450 by the interface file format changing since the interface was downloaded."""),
452 ('Unowned implementations and temporary files', """
453 The cache viewer searches through all the interfaces to find out which implementations \
454 they use. If no interface uses an implementation, it is shown in the 'Unowned implementations' \
455 section.
457 Unowned implementations can result from old versions of a program no longer being listed \
458 in the interface file. Temporary files are created when unpacking an implementation after \
459 downloading it. If the archive is corrupted, the unpacked files may be left there. Unless \
460 you are currently unpacking new programs, it should be fine to delete everything in this \
461 section."""),
463 ('Interfaces', """
464 All remaining interfaces are listed in this section. You may wish to delete old versions of \
465 certain programs. Deleting a program which you may later want to run will require it to be downloaded \
466 again. Deleting a version of a program which is currently running may cause it to crash, so be careful!
467 """))