Can now remove known versions of programs.
[zeroinstall.git] / zeroinstall / 0launch-gui / cache.py
blobb1b1e609a736b0b47f25f765fd359cbba5f3496f
1 import os, shutil
2 import gtk, gobject
4 import gui
5 import help_box
6 from dialog import Dialog
7 from zeroinstall.injector.iface_cache import iface_cache
8 from zeroinstall.injector import basedir, namespaces, model
9 from treetips import TreeTips
11 ROX_IFACE = 'http://rox.sourceforge.net/2005/interfaces/ROX-Filer'
13 # Model columns
14 ITEM = 0
15 SELF_SIZE = 1
16 PRETTY_SIZE = 2
17 TOOLTIP = 3
18 ITEM_OBJECT = 4
20 def popup_menu(bev, obj):
21 menu = gtk.Menu()
22 for i in obj.menu_items:
23 if i is None:
24 item = gtk.SeparatorMenuItem()
25 else:
26 name, cb = i
27 item = gtk.MenuItem(name)
28 item.connect('activate', lambda item, cb=cb: cb(obj))
29 item.show()
30 menu.append(item)
31 menu.popup(None, None, None, bev.button, bev.time)
33 def pretty_size(size):
34 if size == 0: return ''
35 return gui.pretty_size(size)
37 def size_if_exists(path):
38 "Get the size for a file, or 0 if it doesn't exist."
39 if path and os.path.isfile(path):
40 return os.path.getsize(path)
41 return 0
43 def get_size(path):
44 "Get the size for a directory tree. Get the size from the .manifest if possible."
45 man = os.path.join(path, '.manifest')
46 if os.path.exists(man):
47 size = os.path.getsize(man)
48 for line in file(man):
49 if line[:1] in "XF":
50 size += long(line.split(' ', 4)[3])
51 else:
52 size = 0
53 for root, dirs, files in os.walk(path):
54 for name in files:
55 size += getsize(join(root, name))
56 return size
58 def summary(iface):
59 if iface.summary:
60 return iface.get_name() + ' - ' + iface.summary
61 return iface.get_name()
63 def get_selected_paths(tree_view):
64 "GTK 2.0 doesn't have this built-in"
65 selection = tree_view.get_selection()
66 paths = []
67 def add(model, path, iter):
68 paths.append(path)
69 selection.selected_foreach(add)
70 return paths
72 tips = TreeTips()
74 # Responses
75 DELETE = 0
77 class CachedInterface:
78 def __init__(self, uri, size):
79 self.uri = uri
80 self.size = size
82 def delete(self):
83 if not self.uri.startswith('/'):
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, in_cache):
101 CachedInterface.__init__(self, iface.uri, size)
102 self.iface = iface
103 self.in_cache = 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 may_delete = property(lambda self: not self.in_cache)
113 class InvalidInterface(CachedInterface):
114 may_delete = True
116 def __init__(self, uri, ex, size):
117 CachedInterface.__init__(self, uri, size)
118 self.ex = ex
120 def append_to(self, model, iter):
121 model.append(iter, [self.uri, self.size, None, self.ex, self])
123 class CachedImplementation:
124 may_delete = True
126 def __init__(self, cache_dir, name):
127 self.impl_path = os.path.join(cache_dir, name)
128 self.size = get_size(self.impl_path)
129 self.name = name
131 def delete(self):
132 #print "Delete", self.impl_path
133 shutil.rmtree(self.impl_path)
135 def open_rox(self):
136 os.spawnlp(os.P_WAIT, '0launch', '0launch', ROX_IFACE, '-d', self.impl_path)
138 menu_items = [('Open in ROX-Filer', open_rox)]
140 class UnusedImplementation(CachedImplementation):
141 def append_to(self, model, iter):
142 model.append(iter, [self.name, self.size, None, self.impl_path, self])
144 class KnownImplementation(CachedImplementation):
145 def __init__(self, cache_dir, impl, impl_size):
146 CachedImplementation.__init__(self, cache_dir, impl.id)
147 self.impl = impl
148 self.size = impl_size
150 def append_to(self, model, iter):
151 model.append(iter,
152 ['Version %s : %s' % (self.impl.get_version(), self.impl.id),
153 self.size, None,
154 None,
155 self])
157 def __cmp__(self, other):
158 if hasattr(other, 'impl'):
159 return self.impl.__cmp__(other.impl)
160 return -1
162 class CacheExplorer(Dialog):
163 def __init__(self):
164 Dialog.__init__(self)
165 self.set_title('Zero Install Cache')
166 self.set_default_size(gtk.gdk.screen_width() / 2, gtk.gdk.screen_height() / 2)
168 # Model
169 self.model = gtk.TreeStore(str, int, str, str, object)
170 self.tree_view = gtk.TreeView(self.model)
172 # Tree view
173 swin = gtk.ScrolledWindow()
174 swin.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_ALWAYS)
175 swin.set_shadow_type(gtk.SHADOW_IN)
176 swin.add(self.tree_view)
177 self.vbox.pack_start(swin, True, True, 0)
178 self.tree_view.set_rules_hint(True)
179 swin.show_all()
181 column = gtk.TreeViewColumn('Item', gtk.CellRendererText(), text = ITEM)
182 column.set_resizable(True)
183 self.tree_view.append_column(column)
185 cell = gtk.CellRendererText()
186 cell.set_property('xalign', 1.0)
187 column = gtk.TreeViewColumn('Size', cell, text = PRETTY_SIZE)
188 self.tree_view.append_column(column)
190 def button_press(tree_view, bev):
191 if bev.button != 3:
192 return False
193 pos = tree_view.get_path_at_pos(int(bev.x), int(bev.y))
194 if not pos:
195 return False
196 path, col, x, y = pos
197 obj = self.model[path][ITEM_OBJECT]
198 if obj and hasattr(obj, 'menu_items'):
199 popup_menu(bev, obj)
200 self.tree_view.connect('button-press-event', button_press)
202 # Tree tooltips
203 def motion(tree_view, ev):
204 if ev.window is not tree_view.get_bin_window():
205 return False
206 pos = tree_view.get_path_at_pos(int(ev.x), int(ev.y))
207 if pos:
208 path = pos[0]
209 row = self.model[path]
210 tip = row[TOOLTIP]
211 if tip:
212 if tip != tips.item:
213 tips.prime(tree_view, tip)
214 else:
215 tips.hide()
216 else:
217 tips.hide()
219 self.tree_view.connect('motion-notify-event', motion)
220 self.tree_view.connect('leave-notify-event', lambda tv, ev: tips.hide())
222 # Responses
224 self.add_button(gtk.STOCK_HELP, gtk.RESPONSE_HELP)
225 self.add_button(gtk.STOCK_CLOSE, gtk.RESPONSE_OK)
226 self.add_button(gtk.STOCK_DELETE, DELETE)
227 self.set_default_response(gtk.RESPONSE_OK)
229 selection = self.tree_view.get_selection()
230 def selection_changed(selection):
231 any_selected = False
232 for x in get_selected_paths(self.tree_view):
233 obj = self.model[x][ITEM_OBJECT]
234 if obj is None or not obj.may_delete:
235 self.set_response_sensitive(DELETE, False)
236 return
237 any_selected = True
238 self.set_response_sensitive(DELETE, any_selected)
239 selection.set_mode(gtk.SELECTION_MULTIPLE)
240 selection.connect('changed', selection_changed)
241 selection_changed(selection)
243 def response(dialog, resp):
244 if resp == gtk.RESPONSE_OK:
245 self.destroy()
246 elif resp == gtk.RESPONSE_HELP:
247 cache_help.display()
248 elif resp == DELETE:
249 self.delete()
250 self.connect('response', response)
252 def delete(self):
253 model = self.model
254 paths = get_selected_paths(self.tree_view)
255 paths.reverse()
256 for path in paths:
257 item = model[path][ITEM_OBJECT]
258 assert item.delete
259 item.delete()
260 model.remove(model.get_iter(path))
261 self.update_sizes()
263 def populate_model(self):
264 # Find cached implementations
266 unowned = {} # Impl ID -> Store
267 duplicates = []
269 for s in iface_cache.stores.stores:
270 for id in os.listdir(s.dir):
271 if id in unowned:
272 duplicates.append(id)
273 unowned[id] = s
275 ok_interfaces = []
276 error_interfaces = []
277 unused_interfaces = []
279 # Look through cached interfaces for implementation owners
280 all = iface_cache.list_all_interfaces()
281 all.sort()
282 for uri in all:
283 iface_size = 0
284 try:
285 if uri.startswith('/'):
286 cached_iface = uri
287 else:
288 cached_iface = basedir.load_first_cache(namespaces.config_site,
289 'interfaces', model.escape(uri))
290 user_overrides = basedir.load_first_config(namespaces.config_site,
291 namespaces.config_prog,
292 'user_overrides', model.escape(uri))
294 iface_size = size_if_exists(cached_iface) + size_if_exists(user_overrides)
295 iface = iface_cache.get_interface(uri)
296 except Exception, ex:
297 error_interfaces.append((uri, str(ex), iface_size))
298 else:
299 in_cache = []
300 for impl in iface.implementations.values():
301 if impl.id in unowned:
302 cached_dir = unowned[impl.id].dir
303 impl_path = os.path.join(cached_dir, impl.id)
304 impl_size = get_size(impl_path)
305 in_cache.append(KnownImplementation(cached_dir, impl, impl_size))
306 del unowned[impl.id]
307 in_cache.sort()
308 item = ValidInterface(iface, iface_size, in_cache)
309 if in_cache:
310 ok_interfaces.append(item)
311 else:
312 unused_interfaces.append(item)
314 if error_interfaces:
315 iter = self.model.append(None, [_("Invalid interfaces (unreadable)"),
316 0, None,
317 _("These interfaces exist in the cache but cannot be "
318 "read. You should probably delete them."),
319 None])
320 for uri, ex, size in error_interfaces:
321 item = InvalidInterface(uri, ex, size)
322 item.append_to(self.model, iter)
324 unowned_sizes = []
325 local_dir = os.path.join(basedir.xdg_cache_home, '0install.net', 'implementations')
326 for id in unowned:
327 if unowned[id].dir == local_dir:
328 impl = UnusedImplementation(local_dir, id)
329 unowned_sizes.append((impl.size, impl))
330 if unowned_sizes:
331 iter = self.model.append(None, [_("Unowned implementations and temporary files"),
332 0, None,
333 _("These probably aren't needed any longer. You can "
334 "delete them."), None])
335 unowned_sizes.sort()
336 unowned_sizes.reverse()
337 for size, item in unowned_sizes:
338 item.append_to(self.model, iter)
340 if unused_interfaces:
341 iter = self.model.append(None, [_("Unused interfaces (no versions cached)"),
342 0, None,
343 _("These interfaces are cached, but no actual versions "
344 "are present. They might be useful, and they don't "
345 "take up much space."),
346 None])
347 unused_interfaces.sort()
348 for item in unused_interfaces:
349 item.append_to(self.model, iter)
351 if ok_interfaces:
352 iter = self.model.append(None,
353 [_("Used interfaces"),
354 0, None,
355 _("At least one implementation of each of "
356 "these interfaces is in the cache."),
357 None])
358 for item in ok_interfaces:
359 item.append_to(self.model, iter)
360 self.update_sizes()
362 def update_sizes(self):
363 """Set PRETTY_SIZE to the total size, including all children."""
364 m = self.model
365 def update(itr):
366 total = m[itr][SELF_SIZE]
367 child = m.iter_children(itr)
368 while child:
369 total += update(child)
370 child = m.iter_next(child)
371 m[itr][PRETTY_SIZE] = pretty_size(total)
372 return total
373 itr = m.get_iter_root()
374 while itr:
375 update(itr)
376 itr = m.iter_next(itr)
378 cache_help = help_box.HelpBox("Cache Explorer Help",
379 ('Overview', """
380 When you run a program using Zero Install, it downloads the program's 'interface' file, \
381 which gives information about which versions of the program are available. This interface \
382 file is stored in the cache to save downloading it next time you run the program.
384 When you have chosen which version (implementation) of the program you want to \
385 run, Zero Install downloads that version and stores it in the cache too. Zero Install lets \
386 you have many different versions of each program on your computer at once. This is useful, \
387 since it lets you use an old version if needed, and different programs may need to use \
388 different versions of libraries in some cases.
390 The cache viewer shows you all the interfaces and implementations in your cache. \
391 This is useful to find versions you don't need anymore, so that you can delete them and \
392 free up some disk space.
394 Note: the cache viewer isn't finished; it doesn't currently let you delete things!"""),
396 ('Invalid interfaces', """
397 The cache viewer gets a list of all interfaces in your cache. However, some may not \
398 be valid; they are shown in the 'Invalid interfaces' section. It should be fine to \
399 delete these. An invalid interface may be caused by a local interface that no longer \
400 exists, by a failed attempt to download an interface (the name ends in '.new'), or \
401 by the interface file format changing since the interface was downloaded."""),
403 ('Unowned implementations and temporary files', """
404 The cache viewer searches through all the interfaces to find out which implementations \
405 they use. If no interface uses an implementation, it is shown in the 'Unowned implementations' \
406 section.
408 Unowned implementations can result from old versions of a program no longer being listed \
409 in the interface file. Temporary files are created when unpacking an implementation after \
410 downloading it. If the archive is corrupted, the unpacked files may be left there. Unless \
411 you are currently unpacking new programs, it should be fine to delete everything in this \
412 section."""),
414 ('Unused interfaces', """
415 An unused interface is one which was downloaded, but you don't have any implementations in \
416 the cache. Since interface files are small, there is little point in deleting them. They may \
417 even be useful in some cases (for example, the injector sometimes checks multiple interfaces \
418 to find a usable version; if you delete one of them then it will have to fetch it again, because \
419 it will forget that it doesn't contain anything useful)."""),
421 ('Used interfaces', """
422 All remaining interfaces are listed in this section. You may wish to delete old versions of \
423 certain programs. Deleting a program which you may later want to run will require it to be downloaded \
424 again. Deleting a version of a program which is currently running may cause it to crash, so be careful!
425 """))