Update year to 2009 in various places
[zeroinstall/zeroinstall-rsl.git] / zeroinstall / gtkui / cache.py
blob020c6ecd182eefae2bdc1958c3e82708d3e997f7
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 import os
6 import gtk
8 from zeroinstall.injector import namespaces, model
9 from zeroinstall.zerostore import BadDigest, manifest
10 from zeroinstall import support
11 from zeroinstall.support import basedir
12 from zeroinstall.gtkui.treetips import TreeTips
13 from zeroinstall.gtkui import help_box, gtkutils
15 _ = lambda x: x
17 __all__ = ['CacheExplorer']
19 ROX_IFACE = 'http://rox.sourceforge.net/2005/interfaces/ROX-Filer'
21 # Model columns
22 ITEM = 0
23 SELF_SIZE = 1
24 PRETTY_SIZE = 2
25 TOOLTIP = 3
26 ITEM_OBJECT = 4
28 def popup_menu(bev, obj):
29 menu = gtk.Menu()
30 for i in obj.menu_items:
31 if i is None:
32 item = gtk.SeparatorMenuItem()
33 else:
34 name, cb = i
35 item = gtk.MenuItem(name)
36 item.connect('activate', lambda item, cb=cb: cb(obj))
37 item.show()
38 menu.append(item)
39 menu.popup(None, None, None, bev.button, bev.time)
41 def size_if_exists(path):
42 "Get the size for a file, or 0 if it doesn't exist."
43 if path and os.path.isfile(path):
44 return os.path.getsize(path)
45 return 0
47 def get_size(path):
48 "Get the size for a directory tree. Get the size from the .manifest if possible."
49 man = os.path.join(path, '.manifest')
50 if os.path.exists(man):
51 size = os.path.getsize(man)
52 for line in file(man):
53 if line[:1] in "XF":
54 size += long(line.split(' ', 4)[3])
55 else:
56 size = 0
57 for root, dirs, files in os.walk(path):
58 for name in files:
59 size += os.path.getsize(os.path.join(root, name))
60 return size
62 def summary(iface):
63 if iface.summary:
64 return iface.get_name() + ' - ' + iface.summary
65 return iface.get_name()
67 def get_selected_paths(tree_view):
68 "GTK 2.0 doesn't have this built-in"
69 selection = tree_view.get_selection()
70 paths = []
71 def add(model, path, iter):
72 paths.append(path)
73 selection.selected_foreach(add)
74 return paths
76 tips = TreeTips()
78 # Responses
79 DELETE = 0
81 class CachedInterface(object):
82 def __init__(self, uri, size):
83 self.uri = uri
84 self.size = size
86 def delete(self):
87 if not self.uri.startswith('/'):
88 cached_iface = basedir.load_first_cache(namespaces.config_site,
89 'interfaces', model.escape(self.uri))
90 if cached_iface:
91 #print "Delete", cached_iface
92 os.unlink(cached_iface)
93 user_overrides = basedir.load_first_config(namespaces.config_site,
94 namespaces.config_prog,
95 'user_overrides', model.escape(self.uri))
96 if user_overrides:
97 #print "Delete", user_overrides
98 os.unlink(user_overrides)
100 def __cmp__(self, other):
101 return self.uri.__cmp__(other.uri)
103 class ValidInterface(CachedInterface):
104 def __init__(self, iface, size):
105 CachedInterface.__init__(self, iface.uri, size)
106 self.iface = iface
107 self.in_cache = []
109 def append_to(self, model, iter):
110 iter2 = model.append(iter,
111 [self.uri, self.size, None, summary(self.iface), self])
112 for cached_impl in self.in_cache:
113 cached_impl.append_to(model, iter2)
115 def get_may_delete(self):
116 for c in self.in_cache:
117 if not isinstance(c, LocalImplementation):
118 return False # Still some impls cached
119 return True
121 may_delete = property(get_may_delete)
123 class InvalidInterface(CachedInterface):
124 may_delete = True
126 def __init__(self, uri, ex, size):
127 CachedInterface.__init__(self, uri, size)
128 self.ex = ex
130 def append_to(self, model, iter):
131 model.append(iter, [self.uri, self.size, None, self.ex, self])
133 class LocalImplementation:
134 may_delete = False
136 def __init__(self, impl):
137 self.impl = impl
139 def append_to(self, model, iter):
140 model.append(iter, [self.impl.id, 0, None, 'This is a local version, not held in the cache.', self])
142 class CachedImplementation:
143 may_delete = True
145 def __init__(self, cache_dir, name):
146 self.impl_path = os.path.join(cache_dir, name)
147 self.size = get_size(self.impl_path)
148 self.name = name
150 def delete(self):
151 #print "Delete", self.impl_path
152 support.ro_rmtree(self.impl_path)
154 def open_rox(self):
155 os.spawnlp(os.P_WAIT, '0launch', '0launch', ROX_IFACE, '-d', self.impl_path)
157 def verify(self):
158 try:
159 manifest.verify(self.impl_path)
160 except BadDigest, ex:
161 box = gtk.MessageDialog(None, 0,
162 gtk.MESSAGE_WARNING, gtk.BUTTONS_OK, str(ex))
163 if ex.detail:
164 swin = gtk.ScrolledWindow()
165 buffer = gtk.TextBuffer()
166 mono = buffer.create_tag('mono', family = 'Monospace')
167 buffer.insert_with_tags(buffer.get_start_iter(), ex.detail, mono)
168 text = gtk.TextView(buffer)
169 text.set_editable(False)
170 text.set_cursor_visible(False)
171 swin.add(text)
172 swin.set_shadow_type(gtk.SHADOW_IN)
173 swin.set_border_width(4)
174 box.vbox.pack_start(swin)
175 swin.show_all()
176 box.set_resizable(True)
177 else:
178 box = gtk.MessageDialog(None, 0,
179 gtk.MESSAGE_INFO, gtk.BUTTONS_OK,
180 'Contents match digest; nothing has been changed.')
181 box.run()
182 box.destroy()
184 menu_items = [('Open in ROX-Filer', open_rox),
185 ('Verify integrity', verify)]
187 class UnusedImplementation(CachedImplementation):
188 def append_to(self, model, iter):
189 model.append(iter, [self.name, self.size, None, self.impl_path, self])
191 class KnownImplementation(CachedImplementation):
192 def __init__(self, cached_iface, cache_dir, impl, impl_size):
193 CachedImplementation.__init__(self, cache_dir, impl.id)
194 self.cached_iface = cached_iface
195 self.impl = impl
196 self.size = impl_size
198 def delete(self):
199 CachedImplementation.delete(self)
200 self.cached_iface.in_cache.remove(self)
202 def append_to(self, model, iter):
203 model.append(iter,
204 ['Version %s : %s' % (self.impl.get_version(), self.impl.id),
205 self.size, None,
206 None,
207 self])
209 def __cmp__(self, other):
210 if hasattr(other, 'impl'):
211 return self.impl.__cmp__(other.impl)
212 return -1
214 class CacheExplorer:
215 """A graphical interface for viewing the cache and deleting old items."""
216 def __init__(self, iface_cache):
217 widgets = gtkutils.Template(os.path.join(os.path.dirname(__file__), 'cache.glade'), 'cache')
218 self.window = window = widgets.get_widget('cache')
219 window.set_default_size(gtk.gdk.screen_width() / 2, gtk.gdk.screen_height() / 2)
220 self.iface_cache = iface_cache
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_CLOSE)
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_CLOSE:
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 self.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 = self.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 = self.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 """))