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.
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
17 __all__
= ['CacheExplorer']
19 ROX_IFACE
= 'http://rox.sourceforge.net/2005/interfaces/ROX-Filer'
28 def popup_menu(bev
, obj
):
30 for i
in obj
.menu_items
:
32 item
= gtk
.SeparatorMenuItem()
35 item
= gtk
.MenuItem(name
)
36 item
.connect('activate', lambda item
, cb
=cb
: cb(obj
))
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
)
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
):
54 size
+= long(line
.split(' ', 4)[3])
57 for root
, dirs
, files
in os
.walk(path
):
59 size
+= os
.path
.getsize(os
.path
.join(root
, name
))
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()
71 def add(model
, path
, iter):
73 selection
.selected_foreach(add
)
81 class CachedInterface(object):
82 def __init__(self
, uri
, size
):
87 if not self
.uri
.startswith('/'):
88 cached_iface
= basedir
.load_first_cache(namespaces
.config_site
,
89 'interfaces', model
.escape(self
.uri
))
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
))
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
)
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
121 may_delete
= property(get_may_delete
)
123 class InvalidInterface(CachedInterface
):
126 def __init__(self
, uri
, ex
, size
):
127 CachedInterface
.__init
__(self
, uri
, size
)
130 def append_to(self
, model
, iter):
131 model
.append(iter, [self
.uri
, self
.size
, None, self
.ex
, self
])
133 class LocalImplementation
:
136 def __init__(self
, 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
:
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
)
151 #print "Delete", self.impl_path
152 support
.ro_rmtree(self
.impl_path
)
155 os
.spawnlp(os
.P_WAIT
, '0launch', '0launch', ROX_IFACE
, '-d', self
.impl_path
)
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
))
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)
172 swin
.set_shadow_type(gtk
.SHADOW_IN
)
173 swin
.set_border_width(4)
174 box
.vbox
.pack_start(swin
)
176 box
.set_resizable(True)
178 box
= gtk
.MessageDialog(None, 0,
179 gtk
.MESSAGE_INFO
, gtk
.BUTTONS_OK
,
180 'Contents match digest; nothing has been changed.')
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
196 self
.size
= impl_size
199 CachedImplementation
.delete(self
)
200 self
.cached_iface
.in_cache
.remove(self
)
202 def append_to(self
, model
, iter):
204 ['Version %s : %s' % (self
.impl
.get_version(), self
.impl
.id),
209 def __cmp__(self
, other
):
210 if hasattr(other
, 'impl'):
211 return self
.impl
.__cmp
__(other
.impl
)
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
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
):
239 pos
= tree_view
.get_path_at_pos(int(bev
.x
), int(bev
.y
))
242 path
, col
, x
, y
= pos
243 obj
= self
.model
[path
][ITEM_OBJECT
]
244 if obj
and hasattr(obj
, 'menu_items'):
246 self
.tree_view
.connect('button-press-event', button_press
)
249 def motion(tree_view
, ev
):
250 if ev
.window
is not tree_view
.get_bin_window():
252 pos
= tree_view
.get_path_at_pos(int(ev
.x
), int(ev
.y
))
255 row
= self
.model
[path
]
259 tips
.prime(tree_view
, tip
)
265 self
.tree_view
.connect('motion-notify-event', motion
)
266 self
.tree_view
.connect('leave-notify-event', lambda tv
, ev
: tips
.hide())
269 window
.set_default_response(gtk
.RESPONSE_CLOSE
)
271 selection
= self
.tree_view
.get_selection()
272 def selection_changed(selection
):
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)
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
:
288 elif resp
== gtk
.RESPONSE_HELP
:
292 window
.connect('response', response
)
298 paths
= get_selected_paths(self
.tree_view
)
301 item
= model
[path
][ITEM_OBJECT
]
306 errors
.append(str(ex
))
308 model
.remove(model
.get_iter(path
))
312 gtkutils
.show_message_box(self
, "Failed to delete:\n%s" % '\n'.join(errors
))
315 """Display the window and scan the caches to populate it."""
317 self
.window
.window
.set_cursor(gtkutils
.get_busy_pointer())
320 self
._populate
_model
()
321 i
= self
.model
.get_iter_root()
323 self
.tree_view
.expand_row(self
.model
.get_path(i
), False)
324 i
= self
.model
.iter_next(i
)
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):
338 duplicates
.append(id)
342 error_interfaces
= []
344 # Look through cached interfaces for implementation owners
345 all
= self
.iface_cache
.list_all_interfaces()
350 if uri
.startswith('/'):
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
))
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
))
374 cached_iface
.in_cache
.sort()
375 ok_interfaces
.append(cached_iface
)
378 iter = self
.model
.append(None, [_("Invalid interfaces (unreadable)"),
380 _("These interfaces exist in the cache but cannot be "
381 "read. You should probably delete them."),
383 for uri
, ex
, size
in error_interfaces
:
384 item
= InvalidInterface(uri
, ex
, size
)
385 item
.append_to(self
.model
, iter)
388 local_dir
= os
.path
.join(basedir
.xdg_cache_home
, '0install.net', 'implementations')
390 if unowned
[id].dir == local_dir
:
391 impl
= UnusedImplementation(local_dir
, id)
392 unowned_sizes
.append((impl
.size
, impl
))
394 iter = self
.model
.append(None, [_("Unowned implementations and temporary files"),
396 _("These probably aren't needed any longer. You can "
397 "delete them."), None])
399 unowned_sizes
.reverse()
400 for size
, item
in unowned_sizes
:
401 item
.append_to(self
.model
, iter)
404 iter = self
.model
.append(None,
407 _("Interfaces in the cache"),
409 for item
in ok_interfaces
:
410 item
.append_to(self
.model
, iter)
413 def _update_sizes(self
):
414 """Set PRETTY_SIZE to the total size, including all children."""
417 total
= m
[itr
][SELF_SIZE
]
418 child
= m
.iter_children(itr
)
420 total
+= update(child
)
421 child
= m
.iter_next(child
)
422 m
[itr
][PRETTY_SIZE
] = support
.pretty_size(total
)
424 itr
= m
.get_iter_root()
427 itr
= m
.iter_next(itr
)
429 cache_help
= help_box
.HelpBox("Cache Explorer Help",
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' \
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 \
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!