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
.treetips
import TreeTips
14 from zeroinstall
.gtkui
import help_box
, gtkutils
16 __all__
= ['CacheExplorer']
18 ROX_IFACE
= 'http://rox.sourceforge.net/2005/interfaces/ROX-Filer'
27 def popup_menu(bev
, obj
):
29 for i
in obj
.menu_items
:
31 item
= gtk
.SeparatorMenuItem()
34 item
= gtk
.MenuItem(name
)
35 item
.connect('activate', lambda item
, cb
=cb
: cb(obj
))
38 menu
.popup(None, None, None, bev
.button
, bev
.time
)
40 def size_if_exists(path
):
41 "Get the size for a file, or 0 if it doesn't exist."
42 if path
and os
.path
.isfile(path
):
43 return os
.path
.getsize(path
)
47 "Get the size for a directory tree. Get the size from the .manifest if possible."
48 man
= os
.path
.join(path
, '.manifest')
49 if os
.path
.exists(man
):
50 size
= os
.path
.getsize(man
)
51 for line
in file(man
):
53 size
+= long(line
.split(' ', 4)[3])
56 for root
, dirs
, files
in os
.walk(path
):
58 size
+= os
.path
.getsize(os
.path
.join(root
, name
))
63 return iface
.get_name() + ' - ' + iface
.summary
64 return iface
.get_name()
66 def get_selected_paths(tree_view
):
67 "GTK 2.0 doesn't have this built-in"
68 selection
= tree_view
.get_selection()
70 def add(model
, path
, iter):
72 selection
.selected_foreach(add
)
80 class CachedInterface(object):
81 def __init__(self
, uri
, size
):
86 if not self
.uri
.startswith('/'):
87 cached_iface
= basedir
.load_first_cache(namespaces
.config_site
,
88 'interfaces', model
.escape(self
.uri
))
90 #print "Delete", cached_iface
91 os
.unlink(cached_iface
)
92 user_overrides
= basedir
.load_first_config(namespaces
.config_site
,
93 namespaces
.config_prog
,
94 'user_overrides', model
.escape(self
.uri
))
96 #print "Delete", user_overrides
97 os
.unlink(user_overrides
)
99 def __cmp__(self
, other
):
100 return self
.uri
.__cmp
__(other
.uri
)
102 class ValidInterface(CachedInterface
):
103 def __init__(self
, iface
, size
):
104 CachedInterface
.__init
__(self
, iface
.uri
, size
)
108 def append_to(self
, model
, iter):
109 iter2
= model
.append(iter,
110 [self
.uri
, self
.size
, None, summary(self
.iface
), self
])
111 for cached_impl
in self
.in_cache
:
112 cached_impl
.append_to(model
, iter2
)
114 def get_may_delete(self
):
115 for c
in self
.in_cache
:
116 if not isinstance(c
, LocalImplementation
):
117 return False # Still some impls cached
120 may_delete
= property(get_may_delete
)
122 class InvalidInterface(CachedInterface
):
125 def __init__(self
, uri
, ex
, size
):
126 CachedInterface
.__init
__(self
, uri
, size
)
129 def append_to(self
, model
, iter):
130 model
.append(iter, [self
.uri
, self
.size
, None, self
.ex
, self
])
132 class LocalImplementation
:
135 def __init__(self
, impl
):
138 def append_to(self
, model
, iter):
139 model
.append(iter, [self
.impl
.id, 0, None, _('This is a local version, not held in the cache.'), self
])
141 class CachedImplementation
:
144 def __init__(self
, cache_dir
, name
):
145 self
.impl_path
= os
.path
.join(cache_dir
, name
)
146 self
.size
= get_size(self
.impl_path
)
150 #print "Delete", self.impl_path
151 support
.ro_rmtree(self
.impl_path
)
154 os
.spawnlp(os
.P_WAIT
, '0launch', '0launch', ROX_IFACE
, '-d', self
.impl_path
)
158 manifest
.verify(self
.impl_path
)
159 except BadDigest
, ex
:
160 box
= gtk
.MessageDialog(None, 0,
161 gtk
.MESSAGE_WARNING
, gtk
.BUTTONS_OK
, str(ex
))
163 swin
= gtk
.ScrolledWindow()
164 buffer = gtk
.TextBuffer()
165 mono
= buffer.create_tag('mono', family
= 'Monospace')
166 buffer.insert_with_tags(buffer.get_start_iter(), ex
.detail
, mono
)
167 text
= gtk
.TextView(buffer)
168 text
.set_editable(False)
169 text
.set_cursor_visible(False)
171 swin
.set_shadow_type(gtk
.SHADOW_IN
)
172 swin
.set_border_width(4)
173 box
.vbox
.pack_start(swin
)
175 box
.set_resizable(True)
177 box
= gtk
.MessageDialog(None, 0,
178 gtk
.MESSAGE_INFO
, gtk
.BUTTONS_OK
,
179 _('Contents match digest; nothing has been changed.'))
183 menu_items
= [(_('Open in ROX-Filer'), open_rox
),
184 (_('Verify integrity'), verify
)]
186 class UnusedImplementation(CachedImplementation
):
187 def append_to(self
, model
, iter):
188 model
.append(iter, [self
.name
, self
.size
, None, self
.impl_path
, self
])
190 class KnownImplementation(CachedImplementation
):
191 def __init__(self
, cached_iface
, cache_dir
, impl
, impl_size
):
192 CachedImplementation
.__init
__(self
, cache_dir
, impl
.id)
193 self
.cached_iface
= cached_iface
195 self
.size
= impl_size
198 CachedImplementation
.delete(self
)
199 self
.cached_iface
.in_cache
.remove(self
)
201 def append_to(self
, model
, iter):
203 [_('Version %(implementation_version)s : %(implementation_id)s') % {'implementation_version': self
.impl
.get_version(), 'implementation_id': self
.impl
.id},
208 def __cmp__(self
, other
):
209 if hasattr(other
, 'impl'):
210 return self
.impl
.__cmp
__(other
.impl
)
214 """A graphical interface for viewing the cache and deleting old items."""
215 def __init__(self
, iface_cache
):
216 widgets
= gtkutils
.Template(os
.path
.join(os
.path
.dirname(__file__
), 'cache.ui'), 'cache')
217 self
.window
= window
= widgets
.get_widget('cache')
218 window
.set_default_size(gtk
.gdk
.screen_width() / 2, gtk
.gdk
.screen_height() / 2)
219 self
.iface_cache
= iface_cache
222 self
.model
= gtk
.TreeStore(str, int, str, str, object)
223 self
.tree_view
= widgets
.get_widget('treeview')
224 self
.tree_view
.set_model(self
.model
)
226 column
= gtk
.TreeViewColumn(_('Item'), gtk
.CellRendererText(), text
= ITEM
)
227 column
.set_resizable(True)
228 self
.tree_view
.append_column(column
)
230 cell
= gtk
.CellRendererText()
231 cell
.set_property('xalign', 1.0)
232 column
= gtk
.TreeViewColumn(_('Size'), cell
, text
= PRETTY_SIZE
)
233 self
.tree_view
.append_column(column
)
235 def button_press(tree_view
, bev
):
238 pos
= tree_view
.get_path_at_pos(int(bev
.x
), int(bev
.y
))
241 path
, col
, x
, y
= pos
242 obj
= self
.model
[path
][ITEM_OBJECT
]
243 if obj
and hasattr(obj
, 'menu_items'):
245 self
.tree_view
.connect('button-press-event', button_press
)
248 def motion(tree_view
, ev
):
249 if ev
.window
is not tree_view
.get_bin_window():
251 pos
= tree_view
.get_path_at_pos(int(ev
.x
), int(ev
.y
))
254 row
= self
.model
[path
]
258 tips
.prime(tree_view
, tip
)
264 self
.tree_view
.connect('motion-notify-event', motion
)
265 self
.tree_view
.connect('leave-notify-event', lambda tv
, ev
: tips
.hide())
268 window
.set_default_response(gtk
.RESPONSE_CLOSE
)
270 selection
= self
.tree_view
.get_selection()
271 def selection_changed(selection
):
273 for x
in get_selected_paths(self
.tree_view
):
274 obj
= self
.model
[x
][ITEM_OBJECT
]
275 if obj
is None or not obj
.may_delete
:
276 window
.set_response_sensitive(DELETE
, False)
279 window
.set_response_sensitive(DELETE
, any_selected
)
280 selection
.set_mode(gtk
.SELECTION_MULTIPLE
)
281 selection
.connect('changed', selection_changed
)
282 selection_changed(selection
)
284 def response(dialog
, resp
):
285 if resp
== gtk
.RESPONSE_CLOSE
:
287 elif resp
== gtk
.RESPONSE_HELP
:
291 window
.connect('response', response
)
297 paths
= get_selected_paths(self
.tree_view
)
300 item
= model
[path
][ITEM_OBJECT
]
305 errors
.append(str(ex
))
307 model
.remove(model
.get_iter(path
))
311 gtkutils
.show_message_box(self
.window
, _("Failed to delete:\n%s") % '\n'.join(errors
))
314 """Display the window and scan the caches to populate it."""
316 self
.window
.window
.set_cursor(gtkutils
.get_busy_pointer())
319 self
._populate
_model
()
320 i
= self
.model
.get_iter_root()
322 self
.tree_view
.expand_row(self
.model
.get_path(i
), False)
323 i
= self
.model
.iter_next(i
)
325 self
.window
.window
.set_cursor(None)
327 def _populate_model(self
):
328 # Find cached implementations
330 unowned
= {} # Impl ID -> Store
331 duplicates
= [] # TODO
333 for s
in self
.iface_cache
.stores
.stores
:
334 if os
.path
.isdir(s
.dir):
335 for id in os
.listdir(s
.dir):
337 duplicates
.append(id)
341 error_interfaces
= []
343 # Look through cached interfaces for implementation owners
344 all
= self
.iface_cache
.list_all_interfaces()
349 if uri
.startswith('/'):
352 cached_iface
= basedir
.load_first_cache(namespaces
.config_site
,
353 'interfaces', model
.escape(uri
))
354 user_overrides
= basedir
.load_first_config(namespaces
.config_site
,
355 namespaces
.config_prog
,
356 'user_overrides', model
.escape(uri
))
358 iface_size
= size_if_exists(cached_iface
) + size_if_exists(user_overrides
)
359 iface
= self
.iface_cache
.get_interface(uri
)
360 except Exception, ex
:
361 error_interfaces
.append((uri
, str(ex
), iface_size
))
363 cached_iface
= ValidInterface(iface
, iface_size
)
364 for impl
in iface
.implementations
.values():
365 if impl
.id.startswith('/') or impl
.id.startswith('.'):
366 cached_iface
.in_cache
.append(LocalImplementation(impl
))
367 if impl
.id in unowned
:
368 cached_dir
= unowned
[impl
.id].dir
369 impl_path
= os
.path
.join(cached_dir
, impl
.id)
370 impl_size
= get_size(impl_path
)
371 cached_iface
.in_cache
.append(KnownImplementation(cached_iface
, cached_dir
, impl
, impl_size
))
373 cached_iface
.in_cache
.sort()
374 ok_interfaces
.append(cached_iface
)
377 iter = self
.model
.append(None, [_("Invalid interfaces (unreadable)"),
379 _("These interfaces exist in the cache but cannot be "
380 "read. You should probably delete them."),
382 for uri
, ex
, size
in error_interfaces
:
383 item
= InvalidInterface(uri
, ex
, size
)
384 item
.append_to(self
.model
, iter)
387 local_dir
= os
.path
.join(basedir
.xdg_cache_home
, '0install.net', 'implementations')
389 if unowned
[id].dir == local_dir
:
390 impl
= UnusedImplementation(local_dir
, id)
391 unowned_sizes
.append((impl
.size
, impl
))
393 iter = self
.model
.append(None, [_("Unowned implementations and temporary files"),
395 _("These probably aren't needed any longer. You can "
396 "delete them."), None])
398 unowned_sizes
.reverse()
399 for size
, item
in unowned_sizes
:
400 item
.append_to(self
.model
, iter)
403 iter = self
.model
.append(None,
406 _("Interfaces in the cache"),
408 for item
in ok_interfaces
:
409 item
.append_to(self
.model
, iter)
412 def _update_sizes(self
):
413 """Set PRETTY_SIZE to the total size, including all children."""
416 total
= m
[itr
][SELF_SIZE
]
417 child
= m
.iter_children(itr
)
419 total
+= update(child
)
420 child
= m
.iter_next(child
)
421 m
[itr
][PRETTY_SIZE
] = support
.pretty_size(total
)
423 itr
= m
.get_iter_root()
426 itr
= m
.iter_next(itr
)
428 cache_help
= help_box
.HelpBox(_("Cache Explorer Help"),
429 (_('Overview'), '\n' +
430 _("""When you run a program using Zero Install, it downloads the program's 'interface' file, \
431 which gives information about which versions of the program are available. This interface \
432 file is stored in the cache to save downloading it next time you run the program.
434 When you have chosen which version (implementation) of the program you want to \
435 run, Zero Install downloads that version and stores it in the cache too. Zero Install lets \
436 you have many different versions of each program on your computer at once. This is useful, \
437 since it lets you use an old version if needed, and different programs may need to use \
438 different versions of libraries in some cases.
440 The cache viewer shows you all the interfaces and implementations in your cache. \
441 This is useful to find versions you don't need anymore, so that you can delete them and \
442 free up some disk space.""")),
444 (_('Invalid interfaces'), '\n' +
445 _("""The cache viewer gets a list of all interfaces in your cache. However, some may not \
446 be valid; they are shown in the 'Invalid interfaces' section. It should be fine to \
447 delete these. An invalid interface may be caused by a local interface that no longer \
448 exists, by a failed attempt to download an interface (the name ends in '.new'), or \
449 by the interface file format changing since the interface was downloaded.""")),
451 (_('Unowned implementations and temporary files'), '\n' +
452 _("""The cache viewer searches through all the interfaces to find out which implementations \
453 they use. If no interface uses an implementation, it is shown in the 'Unowned implementations' \
456 Unowned implementations can result from old versions of a program no longer being listed \
457 in the interface file. Temporary files are created when unpacking an implementation after \
458 downloading it. If the archive is corrupted, the unpacked files may be left there. Unless \
459 you are currently unpacking new programs, it should be fine to delete everything in this \
462 (_('Interfaces'), '\n' +
463 _("""All remaining interfaces are listed in this section. You may wish to delete old versions of \
464 certain programs. Deleting a program which you may later want to run will require it to be downloaded \
465 again. Deleting a version of a program which is currently running may cause it to crash, so be careful!""")))