AutoPolicy can take a handler argument to use a different Handler.
[zeroinstall.git] / zeroinstall / 0launch-gui / cache.py
bloba672db166a6d158ae41b8aa06b1c170580de270a
1 import os, shutil
2 import gtk, gobject
4 import help_box
5 from dialog import Dialog, alert
6 from zeroinstall.injector.iface_cache import iface_cache
7 from zeroinstall.injector import basedir, namespaces, model
8 from zeroinstall.zerostore import BadDigest, manifest
9 from zeroinstall import support
10 from treetips import TreeTips
12 ROX_IFACE = 'http://rox.sourceforge.net/2005/interfaces/ROX-Filer'
14 # Model columns
15 ITEM = 0
16 SELF_SIZE = 1
17 PRETTY_SIZE = 2
18 TOOLTIP = 3
19 ITEM_OBJECT = 4
21 def popup_menu(bev, obj):
22 menu = gtk.Menu()
23 for i in obj.menu_items:
24 if i is None:
25 item = gtk.SeparatorMenuItem()
26 else:
27 name, cb = i
28 item = gtk.MenuItem(name)
29 item.connect('activate', lambda item, cb=cb: cb(obj))
30 item.show()
31 menu.append(item)
32 menu.popup(None, None, None, bev.button, bev.time)
34 def size_if_exists(path):
35 "Get the size for a file, or 0 if it doesn't exist."
36 if path and os.path.isfile(path):
37 return os.path.getsize(path)
38 return 0
40 def get_size(path):
41 "Get the size for a directory tree. Get the size from the .manifest if possible."
42 man = os.path.join(path, '.manifest')
43 if os.path.exists(man):
44 size = os.path.getsize(man)
45 for line in file(man):
46 if line[:1] in "XF":
47 size += long(line.split(' ', 4)[3])
48 else:
49 size = 0
50 for root, dirs, files in os.walk(path):
51 for name in files:
52 size += os.path.getsize(os.path.join(root, name))
53 return size
55 def summary(iface):
56 if iface.summary:
57 return iface.get_name() + ' - ' + iface.summary
58 return iface.get_name()
60 def get_selected_paths(tree_view):
61 "GTK 2.0 doesn't have this built-in"
62 selection = tree_view.get_selection()
63 paths = []
64 def add(model, path, iter):
65 paths.append(path)
66 selection.selected_foreach(add)
67 return paths
69 tips = TreeTips()
71 # Responses
72 DELETE = 0
74 class CachedInterface(object):
75 def __init__(self, uri, size):
76 self.uri = uri
77 self.size = size
79 def delete(self):
80 if not self.uri.startswith('/'):
81 cached_iface = basedir.load_first_cache(namespaces.config_site,
82 'interfaces', model.escape(self.uri))
83 if cached_iface:
84 #print "Delete", cached_iface
85 os.unlink(cached_iface)
86 user_overrides = basedir.load_first_config(namespaces.config_site,
87 namespaces.config_prog,
88 'user_overrides', model.escape(self.uri))
89 if user_overrides:
90 #print "Delete", user_overrides
91 os.unlink(user_overrides)
93 def __cmp__(self, other):
94 return self.uri.__cmp__(other.uri)
96 class ValidInterface(CachedInterface):
97 def __init__(self, iface, size):
98 CachedInterface.__init__(self, iface.uri, size)
99 self.iface = iface
100 self.in_cache = []
102 def append_to(self, model, iter):
103 iter2 = model.append(iter,
104 [self.uri, self.size, None, summary(self.iface), self])
105 for cached_impl in self.in_cache:
106 cached_impl.append_to(model, iter2)
108 def get_may_delete(self):
109 for c in self.in_cache:
110 if not isinstance(c, LocalImplementation):
111 return False # Still some impls cached
112 return True
114 may_delete = property(get_may_delete)
116 class InvalidInterface(CachedInterface):
117 may_delete = True
119 def __init__(self, uri, ex, size):
120 CachedInterface.__init__(self, uri, size)
121 self.ex = ex
123 def append_to(self, model, iter):
124 model.append(iter, [self.uri, self.size, None, self.ex, self])
126 class LocalImplementation:
127 may_delete = False
129 def __init__(self, impl):
130 self.impl = impl
132 def append_to(self, model, iter):
133 model.append(iter, [self.impl.id, 0, None, 'This is a local version, not held in the cache.', self])
135 class CachedImplementation:
136 may_delete = True
138 def __init__(self, cache_dir, name):
139 self.impl_path = os.path.join(cache_dir, name)
140 self.size = get_size(self.impl_path)
141 self.name = name
143 def delete(self):
144 #print "Delete", self.impl_path
145 shutil.rmtree(self.impl_path)
147 def open_rox(self):
148 os.spawnlp(os.P_WAIT, '0launch', '0launch', ROX_IFACE, '-d', self.impl_path)
150 def verify(self):
151 try:
152 manifest.verify(self.impl_path)
153 except BadDigest, ex:
154 box = gtk.MessageDialog(None, 0,
155 gtk.MESSAGE_WARNING, gtk.BUTTONS_OK, str(ex))
156 if ex.detail:
157 swin = gtk.ScrolledWindow()
158 buffer = gtk.TextBuffer()
159 mono = buffer.create_tag('mono', family = 'Monospace')
160 buffer.insert_with_tags(buffer.get_start_iter(), ex.detail, mono)
161 text = gtk.TextView(buffer)
162 text.set_editable(False)
163 text.set_cursor_visible(False)
164 swin.add(text)
165 swin.set_shadow_type(gtk.SHADOW_IN)
166 swin.set_border_width(4)
167 box.vbox.pack_start(swin)
168 swin.show_all()
169 box.set_resizable(True)
170 else:
171 box = gtk.MessageDialog(None, 0,
172 gtk.MESSAGE_INFO, gtk.BUTTONS_OK,
173 'Contents match digest; nothing has been changed.')
174 box.run()
175 box.destroy()
177 menu_items = [('Open in ROX-Filer', open_rox),
178 ('Verify integrity', verify)]
180 class UnusedImplementation(CachedImplementation):
181 def append_to(self, model, iter):
182 model.append(iter, [self.name, self.size, None, self.impl_path, self])
184 class KnownImplementation(CachedImplementation):
185 def __init__(self, cached_iface, cache_dir, impl, impl_size):
186 CachedImplementation.__init__(self, cache_dir, impl.id)
187 self.cached_iface = cached_iface
188 self.impl = impl
189 self.size = impl_size
191 def delete(self):
192 CachedImplementation.delete(self)
193 self.cached_iface.in_cache.remove(self)
195 def append_to(self, model, iter):
196 model.append(iter,
197 ['Version %s : %s' % (self.impl.get_version(), self.impl.id),
198 self.size, None,
199 None,
200 self])
202 def __cmp__(self, other):
203 if hasattr(other, 'impl'):
204 return self.impl.__cmp__(other.impl)
205 return -1
207 class CacheExplorer(Dialog):
208 def __init__(self):
209 Dialog.__init__(self)
210 self.set_title('Zero Install Cache')
211 self.set_default_size(gtk.gdk.screen_width() / 2, gtk.gdk.screen_height() / 2)
213 # Model
214 self.model = gtk.TreeStore(str, int, str, str, object)
215 self.tree_view = gtk.TreeView(self.model)
217 # Tree view
218 swin = gtk.ScrolledWindow()
219 swin.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_ALWAYS)
220 swin.set_shadow_type(gtk.SHADOW_IN)
221 swin.add(self.tree_view)
222 self.vbox.pack_start(swin, True, True, 0)
223 self.tree_view.set_rules_hint(True)
224 swin.show_all()
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
269 self.add_button(gtk.STOCK_HELP, gtk.RESPONSE_HELP)
270 self.add_button(gtk.STOCK_CLOSE, gtk.RESPONSE_OK)
271 self.add_button(gtk.STOCK_DELETE, DELETE)
272 self.set_default_response(gtk.RESPONSE_OK)
274 selection = self.tree_view.get_selection()
275 def selection_changed(selection):
276 any_selected = False
277 for x in get_selected_paths(self.tree_view):
278 obj = self.model[x][ITEM_OBJECT]
279 if obj is None or not obj.may_delete:
280 self.set_response_sensitive(DELETE, False)
281 return
282 any_selected = True
283 self.set_response_sensitive(DELETE, any_selected)
284 selection.set_mode(gtk.SELECTION_MULTIPLE)
285 selection.connect('changed', selection_changed)
286 selection_changed(selection)
288 def response(dialog, resp):
289 if resp == gtk.RESPONSE_OK:
290 self.destroy()
291 elif resp == gtk.RESPONSE_HELP:
292 cache_help.display()
293 elif resp == DELETE:
294 self.delete()
295 self.connect('response', response)
297 def delete(self):
298 errors = []
300 model = self.model
301 paths = get_selected_paths(self.tree_view)
302 paths.reverse()
303 for path in paths:
304 item = model[path][ITEM_OBJECT]
305 assert item.delete
306 try:
307 item.delete()
308 except OSError, ex:
309 errors.append(str(ex))
310 else:
311 model.remove(model.get_iter(path))
312 self.update_sizes()
314 if errors:
315 alert(self, "Failed to delete:\n%s" % '\n'.join(errors))
317 def populate_model(self):
318 # Find cached implementations
320 unowned = {} # Impl ID -> Store
321 duplicates = [] # TODO
323 for s in iface_cache.stores.stores:
324 if os.path.isdir(s.dir):
325 for id in os.listdir(s.dir):
326 if id in unowned:
327 duplicates.append(id)
328 unowned[id] = s
330 ok_interfaces = []
331 error_interfaces = []
333 # Look through cached interfaces for implementation owners
334 all = iface_cache.list_all_interfaces()
335 all.sort()
336 for uri in all:
337 iface_size = 0
338 try:
339 if uri.startswith('/'):
340 cached_iface = uri
341 else:
342 cached_iface = basedir.load_first_cache(namespaces.config_site,
343 'interfaces', model.escape(uri))
344 user_overrides = basedir.load_first_config(namespaces.config_site,
345 namespaces.config_prog,
346 'user_overrides', model.escape(uri))
348 iface_size = size_if_exists(cached_iface) + size_if_exists(user_overrides)
349 iface = iface_cache.get_interface(uri)
350 except Exception, ex:
351 error_interfaces.append((uri, str(ex), iface_size))
352 else:
353 cached_iface = ValidInterface(iface, iface_size)
354 for impl in iface.implementations.values():
355 if impl.id.startswith('/') or impl.id.startswith('.'):
356 cached_iface.in_cache.append(LocalImplementation(impl))
357 if impl.id in unowned:
358 cached_dir = unowned[impl.id].dir
359 impl_path = os.path.join(cached_dir, impl.id)
360 impl_size = get_size(impl_path)
361 cached_iface.in_cache.append(KnownImplementation(cached_iface, cached_dir, impl, impl_size))
362 del unowned[impl.id]
363 cached_iface.in_cache.sort()
364 ok_interfaces.append(cached_iface)
366 if error_interfaces:
367 iter = self.model.append(None, [_("Invalid interfaces (unreadable)"),
368 0, None,
369 _("These interfaces exist in the cache but cannot be "
370 "read. You should probably delete them."),
371 None])
372 for uri, ex, size in error_interfaces:
373 item = InvalidInterface(uri, ex, size)
374 item.append_to(self.model, iter)
376 unowned_sizes = []
377 local_dir = os.path.join(basedir.xdg_cache_home, '0install.net', 'implementations')
378 for id in unowned:
379 if unowned[id].dir == local_dir:
380 impl = UnusedImplementation(local_dir, id)
381 unowned_sizes.append((impl.size, impl))
382 if unowned_sizes:
383 iter = self.model.append(None, [_("Unowned implementations and temporary files"),
384 0, None,
385 _("These probably aren't needed any longer. You can "
386 "delete them."), None])
387 unowned_sizes.sort()
388 unowned_sizes.reverse()
389 for size, item in unowned_sizes:
390 item.append_to(self.model, iter)
392 if ok_interfaces:
393 iter = self.model.append(None,
394 [_("Interfaces"),
395 0, None,
396 _("Interfaces in the cache"),
397 None])
398 for item in ok_interfaces:
399 item.append_to(self.model, iter)
400 self.update_sizes()
402 def update_sizes(self):
403 """Set PRETTY_SIZE to the total size, including all children."""
404 m = self.model
405 def update(itr):
406 total = m[itr][SELF_SIZE]
407 child = m.iter_children(itr)
408 while child:
409 total += update(child)
410 child = m.iter_next(child)
411 m[itr][PRETTY_SIZE] = support.pretty_size(total)
412 return total
413 itr = m.get_iter_root()
414 while itr:
415 update(itr)
416 itr = m.iter_next(itr)
418 cache_help = help_box.HelpBox("Cache Explorer Help",
419 ('Overview', """
420 When you run a program using Zero Install, it downloads the program's 'interface' file, \
421 which gives information about which versions of the program are available. This interface \
422 file is stored in the cache to save downloading it next time you run the program.
424 When you have chosen which version (implementation) of the program you want to \
425 run, Zero Install downloads that version and stores it in the cache too. Zero Install lets \
426 you have many different versions of each program on your computer at once. This is useful, \
427 since it lets you use an old version if needed, and different programs may need to use \
428 different versions of libraries in some cases.
430 The cache viewer shows you all the interfaces and implementations in your cache. \
431 This is useful to find versions you don't need anymore, so that you can delete them and \
432 free up some disk space."""),
434 ('Invalid interfaces', """
435 The cache viewer gets a list of all interfaces in your cache. However, some may not \
436 be valid; they are shown in the 'Invalid interfaces' section. It should be fine to \
437 delete these. An invalid interface may be caused by a local interface that no longer \
438 exists, by a failed attempt to download an interface (the name ends in '.new'), or \
439 by the interface file format changing since the interface was downloaded."""),
441 ('Unowned implementations and temporary files', """
442 The cache viewer searches through all the interfaces to find out which implementations \
443 they use. If no interface uses an implementation, it is shown in the 'Unowned implementations' \
444 section.
446 Unowned implementations can result from old versions of a program no longer being listed \
447 in the interface file. Temporary files are created when unpacking an implementation after \
448 downloading it. If the archive is corrupted, the unpacked files may be left there. Unless \
449 you are currently unpacking new programs, it should be fine to delete everything in this \
450 section."""),
452 ('Interfaces', """
453 All remaining interfaces are listed in this section. You may wish to delete old versions of \
454 certain programs. Deleting a program which you may later want to run will require it to be downloaded \
455 again. Deleting a version of a program which is currently running may cause it to crash, so be careful!
456 """))