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