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