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.
5 from zeroinstall
import _
9 from zeroinstall
.injector
import namespaces
, model
10 from zeroinstall
.zerostore
import BadDigest
, manifest
11 from zeroinstall
import support
12 from zeroinstall
.support
import basedir
13 from zeroinstall
.gtkui
import help_box
, gtkutils
15 __all__
= ['CacheExplorer']
17 ROX_IFACE
= 'http://rox.sourceforge.net/2005/interfaces/ROX-Filer'
26 def popup_menu(bev
, obj
):
28 for i
in obj
.menu_items
:
30 item
= gtk
.SeparatorMenuItem()
33 item
= gtk
.MenuItem(name
)
34 item
.connect('activate', lambda item
, cb
=cb
: cb(obj
))
37 menu
.popup(None, None, None, bev
.button
, bev
.time
)
39 def size_if_exists(path
):
40 "Get the size for a file, or 0 if it doesn't exist."
41 if path
and os
.path
.isfile(path
):
42 return os
.path
.getsize(path
)
46 "Get the size for a directory tree. Get the size from the .manifest if possible."
47 man
= os
.path
.join(path
, '.manifest')
48 if os
.path
.exists(man
):
49 size
= os
.path
.getsize(man
)
50 for line
in file(man
, 'rb'):
52 size
+= long(line
.split(' ', 4)[3])
55 for root
, dirs
, files
in os
.walk(path
):
57 size
+= os
.path
.getsize(os
.path
.join(root
, name
))
62 return iface
.get_name() + ' - ' + iface
.summary
63 return iface
.get_name()
65 def get_selected_paths(tree_view
):
66 "GTK 2.0 doesn't have this built-in"
67 selection
= tree_view
.get_selection()
69 def add(model
, path
, iter):
71 selection
.selected_foreach(add
)
77 class CachedInterface(object):
78 def __init__(self
, uri
, size
):
83 if not os
.path
.isabs(self
.uri
):
84 cached_iface
= basedir
.load_first_cache(namespaces
.config_site
,
85 'interfaces', model
.escape(self
.uri
))
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
))
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
):
101 CachedInterface
.__init
__(self
, iface
.uri
, size
)
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 def get_may_delete(self
):
112 for c
in self
.in_cache
:
113 if not isinstance(c
, LocalImplementation
):
114 return False # Still some impls cached
117 may_delete
= property(get_may_delete
)
119 class InvalidInterface(CachedInterface
):
122 def __init__(self
, uri
, ex
, size
):
123 CachedInterface
.__init
__(self
, uri
, size
)
126 def append_to(self
, model
, iter):
127 model
.append(iter, [self
.uri
, self
.size
, None, self
.ex
, self
])
129 class LocalImplementation
:
132 def __init__(self
, impl
):
135 def append_to(self
, model
, iter):
136 model
.append(iter, [self
.impl
.id, 0, None, _('This is a local version, not held in the cache.'), self
])
138 class CachedImplementation
:
141 def __init__(self
, cache_dir
, name
):
142 self
.impl_path
= os
.path
.join(cache_dir
, name
)
143 self
.size
= get_size(self
.impl_path
)
147 #print "Delete", self.impl_path
148 support
.ro_rmtree(self
.impl_path
)
151 os
.spawnlp(os
.P_WAIT
, '0launch', '0launch', ROX_IFACE
, '-d', self
.impl_path
)
155 manifest
.verify(self
.impl_path
)
156 except BadDigest
, ex
:
157 box
= gtk
.MessageDialog(None, 0,
158 gtk
.MESSAGE_WARNING
, gtk
.BUTTONS_OK
, str(ex
))
160 swin
= gtk
.ScrolledWindow()
161 buffer = gtk
.TextBuffer()
162 mono
= buffer.create_tag('mono', family
= 'Monospace')
163 buffer.insert_with_tags(buffer.get_start_iter(), ex
.detail
, mono
)
164 text
= gtk
.TextView(buffer)
165 text
.set_editable(False)
166 text
.set_cursor_visible(False)
168 swin
.set_shadow_type(gtk
.SHADOW_IN
)
169 swin
.set_border_width(4)
170 box
.vbox
.pack_start(swin
)
172 box
.set_resizable(True)
174 box
= gtk
.MessageDialog(None, 0,
175 gtk
.MESSAGE_INFO
, gtk
.BUTTONS_OK
,
176 _('Contents match digest; nothing has been changed.'))
180 menu_items
= [(_('Open in ROX-Filer'), open_rox
),
181 (_('Verify integrity'), verify
)]
183 class UnusedImplementation(CachedImplementation
):
184 def append_to(self
, model
, iter):
185 model
.append(iter, [self
.name
, self
.size
, None, self
.impl_path
, self
])
187 class KnownImplementation(CachedImplementation
):
188 def __init__(self
, cached_iface
, cache_dir
, impl
, impl_size
):
189 CachedImplementation
.__init
__(self
, cache_dir
, impl
.id)
190 self
.cached_iface
= cached_iface
192 self
.size
= impl_size
195 CachedImplementation
.delete(self
)
196 self
.cached_iface
.in_cache
.remove(self
)
198 def append_to(self
, model
, iter):
200 [_('Version %(implementation_version)s : %(implementation_id)s') % {'implementation_version': self
.impl
.get_version(), 'implementation_id': self
.impl
.id},
205 def __cmp__(self
, other
):
206 if hasattr(other
, 'impl'):
207 return self
.impl
.__cmp
__(other
.impl
)
211 """A graphical interface for viewing the cache and deleting old items."""
212 def __init__(self
, iface_cache
):
213 widgets
= gtkutils
.Template(os
.path
.join(os
.path
.dirname(__file__
), 'cache.ui'), 'cache')
214 self
.window
= window
= widgets
.get_widget('cache')
215 window
.set_default_size(gtk
.gdk
.screen_width() / 2, gtk
.gdk
.screen_height() / 2)
216 self
.iface_cache
= iface_cache
219 self
.model
= gtk
.TreeStore(str, int, str, str, object)
220 self
.tree_view
= widgets
.get_widget('treeview')
221 self
.tree_view
.set_model(self
.model
)
223 column
= gtk
.TreeViewColumn(_('Item'), gtk
.CellRendererText(), text
= ITEM
)
224 column
.set_resizable(True)
225 self
.tree_view
.append_column(column
)
227 cell
= gtk
.CellRendererText()
228 cell
.set_property('xalign', 1.0)
229 column
= gtk
.TreeViewColumn(_('Size'), cell
, text
= PRETTY_SIZE
)
230 self
.tree_view
.append_column(column
)
232 def button_press(tree_view
, bev
):
235 pos
= tree_view
.get_path_at_pos(int(bev
.x
), int(bev
.y
))
238 path
, col
, x
, y
= pos
239 obj
= self
.model
[path
][ITEM_OBJECT
]
240 if obj
and hasattr(obj
, 'menu_items'):
242 self
.tree_view
.connect('button-press-event', button_press
)
245 self
.tree_view
.set_property('has-tooltip', True)
246 def query_tooltip(widget
, x
, y
, keyboard_mode
, tooltip
):
247 x
, y
= self
.tree_view
.convert_widget_to_bin_window_coords(x
, y
)
248 pos
= self
.tree_view
.get_path_at_pos(x
, y
)
251 row
= self
.model
[path
]
254 self
.tree_view
.set_tooltip_cell(tooltip
, pos
[0], None, None)
255 tooltip
.set_text(tip
)
258 self
.tree_view
.connect('query-tooltip', query_tooltip
)
261 window
.set_default_response(gtk
.RESPONSE_CLOSE
)
263 selection
= self
.tree_view
.get_selection()
264 def selection_changed(selection
):
266 for x
in get_selected_paths(self
.tree_view
):
267 obj
= self
.model
[x
][ITEM_OBJECT
]
268 if obj
is None or not obj
.may_delete
:
269 window
.set_response_sensitive(DELETE
, False)
272 window
.set_response_sensitive(DELETE
, any_selected
)
273 selection
.set_mode(gtk
.SELECTION_MULTIPLE
)
274 selection
.connect('changed', selection_changed
)
275 selection_changed(selection
)
277 def response(dialog
, resp
):
278 if resp
== gtk
.RESPONSE_CLOSE
:
280 elif resp
== gtk
.RESPONSE_HELP
:
284 window
.connect('response', response
)
290 paths
= get_selected_paths(self
.tree_view
)
293 item
= model
[path
][ITEM_OBJECT
]
298 errors
.append(str(ex
))
300 model
.remove(model
.get_iter(path
))
304 gtkutils
.show_message_box(self
.window
, _("Failed to delete:\n%s") % '\n'.join(errors
))
307 """Display the window and scan the caches to populate it."""
309 self
.window
.window
.set_cursor(gtkutils
.get_busy_pointer())
312 self
._populate
_model
()
313 i
= self
.model
.get_iter_root()
315 self
.tree_view
.expand_row(self
.model
.get_path(i
), False)
316 i
= self
.model
.iter_next(i
)
318 self
.window
.window
.set_cursor(None)
320 def _populate_model(self
):
321 # Find cached implementations
323 unowned
= {} # Impl ID -> Store
324 duplicates
= [] # TODO
326 for s
in self
.iface_cache
.stores
.stores
:
327 if os
.path
.isdir(s
.dir):
328 for id in os
.listdir(s
.dir):
330 duplicates
.append(id)
334 error_interfaces
= []
336 # Look through cached interfaces for implementation owners
337 all
= self
.iface_cache
.list_all_interfaces()
342 if os
.path
.isabs(uri
):
345 cached_iface
= basedir
.load_first_cache(namespaces
.config_site
,
346 'interfaces', model
.escape(uri
))
347 user_overrides
= basedir
.load_first_config(namespaces
.config_site
,
348 namespaces
.config_prog
,
349 'user_overrides', model
.escape(uri
))
351 iface_size
= size_if_exists(cached_iface
) + size_if_exists(user_overrides
)
352 iface
= self
.iface_cache
.get_interface(uri
)
353 except Exception, ex
:
354 error_interfaces
.append((uri
, str(ex
), iface_size
))
356 cached_iface
= ValidInterface(iface
, iface_size
)
357 for impl
in iface
.implementations
.values():
359 cached_iface
.in_cache
.append(LocalImplementation(impl
))
360 if impl
.id in unowned
:
361 cached_dir
= unowned
[impl
.id].dir
362 impl_path
= os
.path
.join(cached_dir
, impl
.id)
363 impl_size
= get_size(impl_path
)
364 cached_iface
.in_cache
.append(KnownImplementation(cached_iface
, cached_dir
, impl
, impl_size
))
366 cached_iface
.in_cache
.sort()
367 ok_interfaces
.append(cached_iface
)
370 iter = self
.model
.append(None, [_("Invalid interfaces (unreadable)"),
372 _("These interfaces exist in the cache but cannot be "
373 "read. You should probably delete them."),
375 for uri
, ex
, size
in error_interfaces
:
376 item
= InvalidInterface(uri
, ex
, size
)
377 item
.append_to(self
.model
, iter)
380 local_dir
= os
.path
.join(basedir
.xdg_cache_home
, '0install.net', 'implementations')
382 if unowned
[id].dir == local_dir
:
383 impl
= UnusedImplementation(local_dir
, id)
384 unowned_sizes
.append((impl
.size
, impl
))
386 iter = self
.model
.append(None, [_("Unowned implementations and temporary files"),
388 _("These probably aren't needed any longer. You can "
389 "delete them."), None])
391 unowned_sizes
.reverse()
392 for size
, item
in unowned_sizes
:
393 item
.append_to(self
.model
, iter)
396 iter = self
.model
.append(None,
399 _("Interfaces in the cache"),
401 for item
in ok_interfaces
:
402 item
.append_to(self
.model
, iter)
405 def _update_sizes(self
):
406 """Set PRETTY_SIZE to the total size, including all children."""
409 total
= m
[itr
][SELF_SIZE
]
410 child
= m
.iter_children(itr
)
412 total
+= update(child
)
413 child
= m
.iter_next(child
)
414 m
[itr
][PRETTY_SIZE
] = support
.pretty_size(total
)
416 itr
= m
.get_iter_root()
419 itr
= m
.iter_next(itr
)
421 cache_help
= help_box
.HelpBox(_("Cache Explorer Help"),
422 (_('Overview'), '\n' +
423 _("""When you run a program using Zero Install, it downloads the program's 'interface' file, \
424 which gives information about which versions of the program are available. This interface \
425 file is stored in the cache to save downloading it next time you run the program.
427 When you have chosen which version (implementation) of the program you want to \
428 run, Zero Install downloads that version and stores it in the cache too. Zero Install lets \
429 you have many different versions of each program on your computer at once. This is useful, \
430 since it lets you use an old version if needed, and different programs may need to use \
431 different versions of libraries in some cases.
433 The cache viewer shows you all the interfaces and implementations in your cache. \
434 This is useful to find versions you don't need anymore, so that you can delete them and \
435 free up some disk space.""")),
437 (_('Invalid interfaces'), '\n' +
438 _("""The cache viewer gets a list of all interfaces in your cache. However, some may not \
439 be valid; they are shown in the 'Invalid interfaces' section. It should be fine to \
440 delete these. An invalid interface may be caused by a local interface that no longer \
441 exists, by a failed attempt to download an interface (the name ends in '.new'), or \
442 by the interface file format changing since the interface was downloaded.""")),
444 (_('Unowned implementations and temporary files'), '\n' +
445 _("""The cache viewer searches through all the interfaces to find out which implementations \
446 they use. If no interface uses an implementation, it is shown in the 'Unowned implementations' \
449 Unowned implementations can result from old versions of a program no longer being listed \
450 in the interface file. Temporary files are created when unpacking an implementation after \
451 downloading it. If the archive is corrupted, the unpacked files may be left there. Unless \
452 you are currently unpacking new programs, it should be fine to delete everything in this \
455 (_('Interfaces'), '\n' +
456 _("""All remaining interfaces are listed in this section. You may wish to delete old versions of \
457 certain programs. Deleting a program which you may later want to run will require it to be downloaded \
458 again. Deleting a version of a program which is currently running may cause it to crash, so be careful!""")))