1 """Display the contents of the implementation cache."""
2 # Copyright (C) 2008, Thomas Leonard
3 # See the README file for details, or visit http://0install.net.
8 from zeroinstall
.injector
.iface_cache
import iface_cache
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
18 __all__
= ['CacheExplorer']
20 ROX_IFACE
= 'http://rox.sourceforge.net/2005/interfaces/ROX-Filer'
29 def popup_menu(bev
, obj
):
31 for i
in obj
.menu_items
:
33 item
= gtk
.SeparatorMenuItem()
36 item
= gtk
.MenuItem(name
)
37 item
.connect('activate', lambda item
, cb
=cb
: cb(obj
))
40 menu
.popup(None, None, None, bev
.button
, bev
.time
)
42 def size_if_exists(path
):
43 "Get the size for a file, or 0 if it doesn't exist."
44 if path
and os
.path
.isfile(path
):
45 return os
.path
.getsize(path
)
49 "Get the size for a directory tree. Get the size from the .manifest if possible."
50 man
= os
.path
.join(path
, '.manifest')
51 if os
.path
.exists(man
):
52 size
= os
.path
.getsize(man
)
53 for line
in file(man
):
55 size
+= long(line
.split(' ', 4)[3])
58 for root
, dirs
, files
in os
.walk(path
):
60 size
+= os
.path
.getsize(os
.path
.join(root
, name
))
65 return iface
.get_name() + ' - ' + iface
.summary
66 return iface
.get_name()
68 def get_selected_paths(tree_view
):
69 "GTK 2.0 doesn't have this built-in"
70 selection
= tree_view
.get_selection()
72 def add(model
, path
, iter):
74 selection
.selected_foreach(add
)
82 class CachedInterface(object):
83 def __init__(self
, uri
, size
):
88 if not self
.uri
.startswith('/'):
89 cached_iface
= basedir
.load_first_cache(namespaces
.config_site
,
90 'interfaces', model
.escape(self
.uri
))
92 #print "Delete", cached_iface
93 os
.unlink(cached_iface
)
94 user_overrides
= basedir
.load_first_config(namespaces
.config_site
,
95 namespaces
.config_prog
,
96 'user_overrides', model
.escape(self
.uri
))
98 #print "Delete", user_overrides
99 os
.unlink(user_overrides
)
101 def __cmp__(self
, other
):
102 return self
.uri
.__cmp
__(other
.uri
)
104 class ValidInterface(CachedInterface
):
105 def __init__(self
, iface
, size
):
106 CachedInterface
.__init
__(self
, iface
.uri
, size
)
110 def append_to(self
, model
, iter):
111 iter2
= model
.append(iter,
112 [self
.uri
, self
.size
, None, summary(self
.iface
), self
])
113 for cached_impl
in self
.in_cache
:
114 cached_impl
.append_to(model
, iter2
)
116 def get_may_delete(self
):
117 for c
in self
.in_cache
:
118 if not isinstance(c
, LocalImplementation
):
119 return False # Still some impls cached
122 may_delete
= property(get_may_delete
)
124 class InvalidInterface(CachedInterface
):
127 def __init__(self
, uri
, ex
, size
):
128 CachedInterface
.__init
__(self
, uri
, size
)
131 def append_to(self
, model
, iter):
132 model
.append(iter, [self
.uri
, self
.size
, None, self
.ex
, self
])
134 class LocalImplementation
:
137 def __init__(self
, impl
):
140 def append_to(self
, model
, iter):
141 model
.append(iter, [self
.impl
.id, 0, None, 'This is a local version, not held in the cache.', self
])
143 class CachedImplementation
:
146 def __init__(self
, cache_dir
, name
):
147 self
.impl_path
= os
.path
.join(cache_dir
, name
)
148 self
.size
= get_size(self
.impl_path
)
152 #print "Delete", self.impl_path
153 support
.ro_rmtree(self
.impl_path
)
156 os
.spawnlp(os
.P_WAIT
, '0launch', '0launch', ROX_IFACE
, '-d', self
.impl_path
)
160 manifest
.verify(self
.impl_path
)
161 except BadDigest
, ex
:
162 box
= gtk
.MessageDialog(None, 0,
163 gtk
.MESSAGE_WARNING
, gtk
.BUTTONS_OK
, str(ex
))
165 swin
= gtk
.ScrolledWindow()
166 buffer = gtk
.TextBuffer()
167 mono
= buffer.create_tag('mono', family
= 'Monospace')
168 buffer.insert_with_tags(buffer.get_start_iter(), ex
.detail
, mono
)
169 text
= gtk
.TextView(buffer)
170 text
.set_editable(False)
171 text
.set_cursor_visible(False)
173 swin
.set_shadow_type(gtk
.SHADOW_IN
)
174 swin
.set_border_width(4)
175 box
.vbox
.pack_start(swin
)
177 box
.set_resizable(True)
179 box
= gtk
.MessageDialog(None, 0,
180 gtk
.MESSAGE_INFO
, gtk
.BUTTONS_OK
,
181 'Contents match digest; nothing has been changed.')
185 menu_items
= [('Open in ROX-Filer', open_rox
),
186 ('Verify integrity', verify
)]
188 class UnusedImplementation(CachedImplementation
):
189 def append_to(self
, model
, iter):
190 model
.append(iter, [self
.name
, self
.size
, None, self
.impl_path
, self
])
192 class KnownImplementation(CachedImplementation
):
193 def __init__(self
, cached_iface
, cache_dir
, impl
, impl_size
):
194 CachedImplementation
.__init
__(self
, cache_dir
, impl
.id)
195 self
.cached_iface
= cached_iface
197 self
.size
= impl_size
200 CachedImplementation
.delete(self
)
201 self
.cached_iface
.in_cache
.remove(self
)
203 def append_to(self
, model
, iter):
205 ['Version %s : %s' % (self
.impl
.get_version(), self
.impl
.id),
210 def __cmp__(self
, other
):
211 if hasattr(other
, 'impl'):
212 return self
.impl
.__cmp
__(other
.impl
)
216 """A graphical interface for viewing the cache and deleting old items."""
218 widgets
= gtkutils
.Template(os
.path
.join(os
.path
.dirname(__file__
), 'cache.glade'), 'cache')
219 self
.window
= window
= widgets
.get_widget('cache')
220 window
.set_default_size(gtk
.gdk
.screen_width() / 2, gtk
.gdk
.screen_height() / 2)
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_OK
)
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_OK
:
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 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
= 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
= 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!