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