Moved treetips into zeroinstall.gtkui.
[zeroinstall/solver.git] / zeroinstall / 0launch-gui / cache.py
blob8bfa057195fc7d12136f5c13ef05a47862d85173
1 # Copyright (C) 2008, Thomas Leonard
2 # See the README file for details, or visit http://0install.net.
4 import os, shutil
5 import gtk, gobject
7 import help_box
8 from dialog import Dialog, alert
9 from zeroinstall.injector.iface_cache import iface_cache
10 from zeroinstall.injector import namespaces, model
11 from zeroinstall.zerostore import BadDigest, manifest
12 from zeroinstall import support
13 from zeroinstall.support import basedir
14 from zeroinstall.gtkui.treetips import TreeTips
16 ROX_IFACE = 'http://rox.sourceforge.net/2005/interfaces/ROX-Filer'
18 # Model columns
19 ITEM = 0
20 SELF_SIZE = 1
21 PRETTY_SIZE = 2
22 TOOLTIP = 3
23 ITEM_OBJECT = 4
25 def popup_menu(bev, obj):
26 menu = gtk.Menu()
27 for i in obj.menu_items:
28 if i is None:
29 item = gtk.SeparatorMenuItem()
30 else:
31 name, cb = i
32 item = gtk.MenuItem(name)
33 item.connect('activate', lambda item, cb=cb: cb(obj))
34 item.show()
35 menu.append(item)
36 menu.popup(None, None, None, bev.button, bev.time)
38 def size_if_exists(path):
39 "Get the size for a file, or 0 if it doesn't exist."
40 if path and os.path.isfile(path):
41 return os.path.getsize(path)
42 return 0
44 def get_size(path):
45 "Get the size for a directory tree. Get the size from the .manifest if possible."
46 man = os.path.join(path, '.manifest')
47 if os.path.exists(man):
48 size = os.path.getsize(man)
49 for line in file(man):
50 if line[:1] in "XF":
51 size += long(line.split(' ', 4)[3])
52 else:
53 size = 0
54 for root, dirs, files in os.walk(path):
55 for name in files:
56 size += os.path.getsize(os.path.join(root, name))
57 return size
59 def summary(iface):
60 if iface.summary:
61 return iface.get_name() + ' - ' + iface.summary
62 return iface.get_name()
64 def get_selected_paths(tree_view):
65 "GTK 2.0 doesn't have this built-in"
66 selection = tree_view.get_selection()
67 paths = []
68 def add(model, path, iter):
69 paths.append(path)
70 selection.selected_foreach(add)
71 return paths
73 tips = TreeTips()
75 # Responses
76 DELETE = 0
78 class CachedInterface(object):
79 def __init__(self, uri, size):
80 self.uri = uri
81 self.size = size
83 def delete(self):
84 if not self.uri.startswith('/'):
85 cached_iface = basedir.load_first_cache(namespaces.config_site,
86 'interfaces', model.escape(self.uri))
87 if cached_iface:
88 #print "Delete", cached_iface
89 os.unlink(cached_iface)
90 user_overrides = basedir.load_first_config(namespaces.config_site,
91 namespaces.config_prog,
92 'user_overrides', model.escape(self.uri))
93 if user_overrides:
94 #print "Delete", user_overrides
95 os.unlink(user_overrides)
97 def __cmp__(self, other):
98 return self.uri.__cmp__(other.uri)
100 class ValidInterface(CachedInterface):
101 def __init__(self, iface, size):
102 CachedInterface.__init__(self, iface.uri, size)
103 self.iface = iface
104 self.in_cache = []
106 def append_to(self, model, iter):
107 iter2 = model.append(iter,
108 [self.uri, self.size, None, summary(self.iface), self])
109 for cached_impl in self.in_cache:
110 cached_impl.append_to(model, iter2)
112 def get_may_delete(self):
113 for c in self.in_cache:
114 if not isinstance(c, LocalImplementation):
115 return False # Still some impls cached
116 return True
118 may_delete = property(get_may_delete)
120 class InvalidInterface(CachedInterface):
121 may_delete = True
123 def __init__(self, uri, ex, size):
124 CachedInterface.__init__(self, uri, size)
125 self.ex = ex
127 def append_to(self, model, iter):
128 model.append(iter, [self.uri, self.size, None, self.ex, self])
130 class LocalImplementation:
131 may_delete = False
133 def __init__(self, impl):
134 self.impl = impl
136 def append_to(self, model, iter):
137 model.append(iter, [self.impl.id, 0, None, 'This is a local version, not held in the cache.', self])
139 class CachedImplementation:
140 may_delete = True
142 def __init__(self, cache_dir, name):
143 self.impl_path = os.path.join(cache_dir, name)
144 self.size = get_size(self.impl_path)
145 self.name = name
147 def delete(self):
148 #print "Delete", self.impl_path
149 support.ro_rmtree(self.impl_path)
151 def open_rox(self):
152 os.spawnlp(os.P_WAIT, '0launch', '0launch', ROX_IFACE, '-d', self.impl_path)
154 def verify(self):
155 try:
156 manifest.verify(self.impl_path)
157 except BadDigest, ex:
158 box = gtk.MessageDialog(None, 0,
159 gtk.MESSAGE_WARNING, gtk.BUTTONS_OK, str(ex))
160 if ex.detail:
161 swin = gtk.ScrolledWindow()
162 buffer = gtk.TextBuffer()
163 mono = buffer.create_tag('mono', family = 'Monospace')
164 buffer.insert_with_tags(buffer.get_start_iter(), ex.detail, mono)
165 text = gtk.TextView(buffer)
166 text.set_editable(False)
167 text.set_cursor_visible(False)
168 swin.add(text)
169 swin.set_shadow_type(gtk.SHADOW_IN)
170 swin.set_border_width(4)
171 box.vbox.pack_start(swin)
172 swin.show_all()
173 box.set_resizable(True)
174 else:
175 box = gtk.MessageDialog(None, 0,
176 gtk.MESSAGE_INFO, gtk.BUTTONS_OK,
177 'Contents match digest; nothing has been changed.')
178 box.run()
179 box.destroy()
181 menu_items = [('Open in ROX-Filer', open_rox),
182 ('Verify integrity', verify)]
184 class UnusedImplementation(CachedImplementation):
185 def append_to(self, model, iter):
186 model.append(iter, [self.name, self.size, None, self.impl_path, self])
188 class KnownImplementation(CachedImplementation):
189 def __init__(self, cached_iface, cache_dir, impl, impl_size):
190 CachedImplementation.__init__(self, cache_dir, impl.id)
191 self.cached_iface = cached_iface
192 self.impl = impl
193 self.size = impl_size
195 def delete(self):
196 CachedImplementation.delete(self)
197 self.cached_iface.in_cache.remove(self)
199 def append_to(self, model, iter):
200 model.append(iter,
201 ['Version %s : %s' % (self.impl.get_version(), self.impl.id),
202 self.size, None,
203 None,
204 self])
206 def __cmp__(self, other):
207 if hasattr(other, 'impl'):
208 return self.impl.__cmp__(other.impl)
209 return -1
211 class CacheExplorer(Dialog):
212 def __init__(self):
213 Dialog.__init__(self)
214 self.set_title('Zero Install Cache')
215 self.set_default_size(gtk.gdk.screen_width() / 2, gtk.gdk.screen_height() / 2)
217 # Model
218 self.model = gtk.TreeStore(str, int, str, str, object)
219 self.tree_view = gtk.TreeView(self.model)
221 # Tree view
222 swin = gtk.ScrolledWindow()
223 swin.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_ALWAYS)
224 swin.set_shadow_type(gtk.SHADOW_IN)
225 swin.add(self.tree_view)
226 self.vbox.pack_start(swin, True, True, 0)
227 self.tree_view.set_rules_hint(True)
228 swin.show_all()
230 column = gtk.TreeViewColumn('Item', gtk.CellRendererText(), text = ITEM)
231 column.set_resizable(True)
232 self.tree_view.append_column(column)
234 cell = gtk.CellRendererText()
235 cell.set_property('xalign', 1.0)
236 column = gtk.TreeViewColumn('Size', cell, text = PRETTY_SIZE)
237 self.tree_view.append_column(column)
239 def button_press(tree_view, bev):
240 if bev.button != 3:
241 return False
242 pos = tree_view.get_path_at_pos(int(bev.x), int(bev.y))
243 if not pos:
244 return False
245 path, col, x, y = pos
246 obj = self.model[path][ITEM_OBJECT]
247 if obj and hasattr(obj, 'menu_items'):
248 popup_menu(bev, obj)
249 self.tree_view.connect('button-press-event', button_press)
251 # Tree tooltips
252 def motion(tree_view, ev):
253 if ev.window is not tree_view.get_bin_window():
254 return False
255 pos = tree_view.get_path_at_pos(int(ev.x), int(ev.y))
256 if pos:
257 path = pos[0]
258 row = self.model[path]
259 tip = row[TOOLTIP]
260 if tip:
261 if tip != tips.item:
262 tips.prime(tree_view, tip)
263 else:
264 tips.hide()
265 else:
266 tips.hide()
268 self.tree_view.connect('motion-notify-event', motion)
269 self.tree_view.connect('leave-notify-event', lambda tv, ev: tips.hide())
271 # Responses
273 self.add_button(gtk.STOCK_HELP, gtk.RESPONSE_HELP)
274 self.add_button(gtk.STOCK_CLOSE, gtk.RESPONSE_OK)
275 self.add_button(gtk.STOCK_DELETE, DELETE)
276 self.set_default_response(gtk.RESPONSE_OK)
278 selection = self.tree_view.get_selection()
279 def selection_changed(selection):
280 any_selected = False
281 for x in get_selected_paths(self.tree_view):
282 obj = self.model[x][ITEM_OBJECT]
283 if obj is None or not obj.may_delete:
284 self.set_response_sensitive(DELETE, False)
285 return
286 any_selected = True
287 self.set_response_sensitive(DELETE, any_selected)
288 selection.set_mode(gtk.SELECTION_MULTIPLE)
289 selection.connect('changed', selection_changed)
290 selection_changed(selection)
292 def response(dialog, resp):
293 if resp == gtk.RESPONSE_OK:
294 self.destroy()
295 elif resp == gtk.RESPONSE_HELP:
296 cache_help.display()
297 elif resp == DELETE:
298 self.delete()
299 self.connect('response', response)
301 def delete(self):
302 errors = []
304 model = self.model
305 paths = get_selected_paths(self.tree_view)
306 paths.reverse()
307 for path in paths:
308 item = model[path][ITEM_OBJECT]
309 assert item.delete
310 try:
311 item.delete()
312 except OSError, ex:
313 errors.append(str(ex))
314 else:
315 model.remove(model.get_iter(path))
316 self.update_sizes()
318 if errors:
319 alert(self, "Failed to delete:\n%s" % '\n'.join(errors))
321 def populate_model(self):
322 # Find cached implementations
324 unowned = {} # Impl ID -> Store
325 duplicates = [] # TODO
327 for s in iface_cache.stores.stores:
328 if os.path.isdir(s.dir):
329 for id in os.listdir(s.dir):
330 if id in unowned:
331 duplicates.append(id)
332 unowned[id] = s
334 ok_interfaces = []
335 error_interfaces = []
337 # Look through cached interfaces for implementation owners
338 all = iface_cache.list_all_interfaces()
339 all.sort()
340 for uri in all:
341 iface_size = 0
342 try:
343 if uri.startswith('/'):
344 cached_iface = uri
345 else:
346 cached_iface = basedir.load_first_cache(namespaces.config_site,
347 'interfaces', model.escape(uri))
348 user_overrides = basedir.load_first_config(namespaces.config_site,
349 namespaces.config_prog,
350 'user_overrides', model.escape(uri))
352 iface_size = size_if_exists(cached_iface) + size_if_exists(user_overrides)
353 iface = iface_cache.get_interface(uri)
354 except Exception, ex:
355 error_interfaces.append((uri, str(ex), iface_size))
356 else:
357 cached_iface = ValidInterface(iface, iface_size)
358 for impl in iface.implementations.values():
359 if impl.id.startswith('/') or impl.id.startswith('.'):
360 cached_iface.in_cache.append(LocalImplementation(impl))
361 if impl.id in unowned:
362 cached_dir = unowned[impl.id].dir
363 impl_path = os.path.join(cached_dir, impl.id)
364 impl_size = get_size(impl_path)
365 cached_iface.in_cache.append(KnownImplementation(cached_iface, cached_dir, impl, impl_size))
366 del unowned[impl.id]
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', """
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', """
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', """
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', """
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!
460 """))