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'
22 def __init__(self
, name
, column_type
, resizable
=False, props
={}, hide
=False, markup
=False):
23 self
.idx
= len(self
.columns
)
24 self
.columns
.append(self
)
26 self
.column_type
= column_type
28 self
.resizable
= resizable
33 def column_types(cls
):
34 return [col
.column_type
for col
in cls
.columns
]
37 def add_all(cls
, tree_view
):
38 [col
.add(tree_view
) for col
in cls
.columns
]
41 cell
= gtk
.CellRendererText()
42 self
.set_props(cell
, self
.props
)
45 def set_props(self
, obj
, props
):
46 for k
,v
in props
.items():
47 obj
.set_property(k
, v
)
51 kwargs
= {'markup': self
.idx
}
53 kwargs
= {'text': self
.idx
}
54 column
= gtk
.TreeViewColumn(self
.name
, self
.get_cell(), **kwargs
)
55 if 'xalign' in self
.props
:
56 self
.set_props(column
, {'alignment': self
.props
['xalign']})
59 def add(self
, tree_view
):
62 column
= self
.get_column()
63 if self
.resizable
: column
.set_resizable(True)
64 tree_view
.append_column(column
)
66 NAME
= Column(_('Name'), str, hide
=True)
67 URI
= Column(_('URI'), str, hide
=True)
68 TOOLTIP
= Column(_('Description'), str, hide
=True)
69 ITEM_VIEW
= Column(_('Item'), str, props
={'ypad': 6, 'yalign': 0}, resizable
=True, markup
=True)
70 SELF_SIZE
= Column(_('Self Size'), int, hide
=True)
71 TOTAL_SIZE
= Column(_('Total Size'), int, hide
=True)
72 PRETTY_SIZE
= Column(_('Size'), str, props
={'xalign':1.0})
73 ITEM_OBJECT
= Column(_('Object'), object, hide
=True)
75 ACTION_REMOVE
= object() # just make a unique value
77 class Section(object):
79 def __init__(self
, name
, tooltip
):
81 self
.tooltip
= tooltip
83 def append_to(self
, model
):
84 return model
.append(None, extract_columns(
90 SECTION_INTERFACES
= Section(
92 _("Interfaces in the cache"))
93 SECTION_UNOWNED_IMPLEMENTATIONS
= Section(
94 _("Unowned implementations and temporary files"),
95 _("These probably aren't needed any longer. You can delete them."))
96 SECTION_INVALID_INTERFACES
= Section(
97 _("Invalid interfaces (unreadable)"),
98 _("These interfaces exist in the cache but cannot be read. You should probably delete them."))
101 def extract_columns(**d
):
102 vals
= list(map(lambda x
:None, Column
.columns
))
103 def setcol(column
, val
):
104 vals
[column
.idx
] = val
106 name
= d
.get('name', None)
107 desc
= d
.get('desc', None)
108 uri
= d
.get('uri', None)
113 setcol(ITEM_VIEW
, '<span font-size="larger" weight="bold">%s</span>\n'
114 '<span color="#666666">%s</span>' % tuple(map(cgi
.escape
, (name
, uri
))))
116 setcol(ITEM_VIEW
, name
or desc
)
118 size
= d
.get('size', 0)
119 setcol(SELF_SIZE
, size
)
120 setcol(TOTAL_SIZE
, 0) # must be set to prevent type error
121 setcol(TOOLTIP
, d
.get('tooltip', None))
122 setcol(ITEM_OBJECT
, d
.get('object', None))
126 def popup_menu(bev
, obj
, model
, path
, cache_explorer
):
128 for i
in obj
.menu_items
:
130 item
= gtk
.SeparatorMenuItem()
133 item
= gtk
.MenuItem(name
)
134 def _cb(item
, cb
=cb
):
135 action_required
= cb(obj
, cache_explorer
)
136 if action_required
is ACTION_REMOVE
:
137 model
.remove(model
.get_iter(path
))
138 item
.connect('activate', _cb
)
141 menu
.popup(None, None, None, bev
.button
, bev
.time
)
143 def warn(message
, parent
=None):
144 "Present a blocking warning message with OK/Cancel buttons, and return True if OK was pressed"
145 dialog
= gtk
.MessageDialog(parent
=parent
, buttons
=gtk
.BUTTONS_OK_CANCEL
, type=gtk
.MESSAGE_WARNING
)
146 dialog
.set_property('text', message
)
148 def _response(dialog
, resp
):
149 if resp
== gtk
.RESPONSE_OK
:
150 response
.append(True)
151 dialog
.connect('response', _response
)
154 return bool(response
)
158 def size_if_exists(path
):
159 "Get the size for a file, or 0 if it doesn't exist."
160 if path
and os
.path
.isfile(path
):
161 return os
.path
.getsize(path
)
165 "Get the size for a directory tree. Get the size from the .manifest if possible."
166 man
= os
.path
.join(path
, '.manifest')
167 if os
.path
.exists(man
):
168 size
= os
.path
.getsize(man
)
169 for line
in file(man
, 'rb'):
171 size
+= int(line
.split(' ', 4)[3])
174 for root
, dirs
, files
in os
.walk(path
):
176 size
+= os
.path
.getsize(os
.path
.join(root
, name
))
181 return iface
.get_name() + ' - ' + iface
.summary
182 return iface
.get_name()
184 def get_selected_paths(tree_view
):
185 "GTK 2.0 doesn't have this built-in"
186 selection
= tree_view
.get_selection()
188 def add(model
, path
, iter):
190 selection
.selected_foreach(add
)
193 def all_children(model
, iter):
194 "make a python generator out of the children of `iter`"
195 iter = model
.iter_children(iter)
198 iter = model
.iter_next(iter)
202 SAFE_MODE
= False # really delete things
203 #SAFE_MODE = True # print deletes, instead of performing them
205 class CachedInterface(object):
206 def __init__(self
, uri
, size
):
211 if not os
.path
.isabs(self
.uri
):
212 cached_iface
= basedir
.load_first_cache(namespaces
.config_site
,
213 'interfaces', model
.escape(self
.uri
))
216 print "Delete", cached_iface
218 os
.unlink(cached_iface
)
219 user_overrides
= basedir
.load_first_config(namespaces
.config_site
,
220 namespaces
.config_prog
,
221 'user_overrides', model
.escape(self
.uri
))
224 print "Delete", cached_iface
226 os
.unlink(user_overrides
)
228 def __cmp__(self
, other
):
229 return self
.uri
.__cmp
__(other
.uri
)
231 class ValidInterface(CachedInterface
):
232 def __init__(self
, iface
, size
):
233 CachedInterface
.__init
__(self
, iface
.uri
, size
)
237 def delete_children(self
):
238 deletable
= self
.deletable_children()
239 undeletable
= list(filter(lambda child
: not child
.may_delete
, self
.in_cache
))
240 # the only undeletable items we expect to encounter are LocalImplementations
241 unexpected_undeletable
= list(filter(lambda child
: not isinstance(child
, LocalImplementation
), undeletable
))
242 assert not unexpected_undeletable
, "unexpected undeletable items!: %r" % (unexpected_undeletable
,)
243 [child
.delete() for child
in deletable
]
246 self
.delete_children()
247 super(ValidInterface
, self
).delete()
249 def append_to(self
, model
, iter):
250 iter2
= model
.append(iter, extract_columns(
251 name
=self
.iface
.get_name(),
253 tooltip
=self
.iface
.summary
,
255 for cached_impl
in self
.in_cache
:
256 cached_impl
.append_to(model
, iter2
)
258 def launch(self
, explorer
):
259 os
.spawnlp(os
.P_NOWAIT
, '0launch', '0launch', '--gui', self
.uri
)
261 def copy_uri(self
, explorer
):
262 clipboard
= gtk
.clipboard_get()
263 clipboard
.set_text(self
.uri
)
265 def deletable_children(self
):
266 return list(filter(lambda child
: child
.may_delete
, self
.in_cache
))
268 def prompt_delete(self
, cache_explorer
):
269 description
= "\"%s\"" % (self
.iface
.get_name(),)
270 num_children
= len(self
.deletable_children())
272 description
+= _(" (and %s %s)") % (num_children
, _("implementation") if num_children
== 1 else _("implementations"))
273 if warn(_("Really delete %s?") % (description
,), parent
=cache_explorer
.window
):
277 menu_items
= [(_('Launch with GUI'), launch
),
278 (_('Copy URI to clipboard'), copy_uri
),
279 (_('Delete'), prompt_delete
)]
281 class RemoteInterface(ValidInterface
):
284 class LocalInterface(ValidInterface
):
287 class InvalidInterface(CachedInterface
):
290 def __init__(self
, uri
, ex
, size
):
291 CachedInterface
.__init
__(self
, uri
, size
)
294 def append_to(self
, model
, iter):
295 model
.append(iter, [self
.uri
, self
.size
, None, self
.ex
, self
])
297 class LocalImplementation
:
300 def __init__(self
, impl
):
303 def append_to(self
, model
, iter):
304 model
.append(iter, extract_columns(
305 name
=self
.impl
.local_path
,
306 tooltip
=_('This is a local version, not held in the cache.'),
310 class CachedImplementation
:
313 def __init__(self
, cache_dir
, digest
):
314 self
.impl_path
= os
.path
.join(cache_dir
, digest
)
315 self
.size
= get_size(self
.impl_path
)
320 print "Delete", self
.impl_path
322 support
.ro_rmtree(self
.impl_path
)
324 def open_rox(self
, explorer
):
325 os
.spawnlp(os
.P_WAIT
, '0launch', '0launch', ROX_IFACE
, '-d', self
.impl_path
)
327 def verify(self
, explorer
):
329 manifest
.verify(self
.impl_path
)
330 except BadDigest
, ex
:
331 box
= gtk
.MessageDialog(None, 0,
332 gtk
.MESSAGE_WARNING
, gtk
.BUTTONS_OK
, str(ex
))
334 swin
= gtk
.ScrolledWindow()
335 buffer = gtk
.TextBuffer()
336 mono
= buffer.create_tag('mono', family
= 'Monospace')
337 buffer.insert_with_tags(buffer.get_start_iter(), ex
.detail
, mono
)
338 text
= gtk
.TextView(buffer)
339 text
.set_editable(False)
340 text
.set_cursor_visible(False)
342 swin
.set_shadow_type(gtk
.SHADOW_IN
)
343 swin
.set_border_width(4)
344 box
.vbox
.pack_start(swin
)
346 box
.set_resizable(True)
348 box
= gtk
.MessageDialog(None, 0,
349 gtk
.MESSAGE_INFO
, gtk
.BUTTONS_OK
,
350 _('Contents match digest; nothing has been changed.'))
354 def prompt_delete(self
, explorer
):
355 if warn(_("Really delete implementation?"), parent
=explorer
.window
):
359 menu_items
= [(_('Open in ROX-Filer'), open_rox
),
360 (_('Verify integrity'), verify
),
361 (_('Delete'), prompt_delete
)]
363 class UnusedImplementation(CachedImplementation
):
364 def append_to(self
, model
, iter):
365 model
.append(iter, extract_columns(
368 tooltip
=self
.impl_path
,
371 class KnownImplementation(CachedImplementation
):
372 def __init__(self
, cached_iface
, cache_dir
, impl
, impl_size
, digest
):
373 CachedImplementation
.__init
__(self
, cache_dir
, digest
)
374 self
.cached_iface
= cached_iface
376 self
.size
= impl_size
380 print "Delete", self
.impl
382 CachedImplementation
.delete(self
)
383 self
.cached_iface
.in_cache
.remove(self
)
385 def append_to(self
, model
, iter):
386 model
.append(iter, extract_columns(
387 name
=_('Version %(implementation_version)s : %(implementation_id)s') % {'implementation_version': self
.impl
.get_version(), 'implementation_id': self
.impl
.id},
389 tooltip
=self
.impl_path
,
392 def __cmp__(self
, other
):
393 if hasattr(other
, 'impl'):
394 return self
.impl
.__cmp
__(other
.impl
)
398 """A graphical interface for viewing the cache and deleting old items."""
400 def __init__(self
, iface_cache
):
401 widgets
= gtkutils
.Template(os
.path
.join(os
.path
.dirname(__file__
), 'cache.ui'), 'cache')
402 self
.window
= window
= widgets
.get_widget('cache')
403 window
.set_default_size(gtk
.gdk
.screen_width() / 2, gtk
.gdk
.screen_height() / 2)
404 self
.iface_cache
= iface_cache
407 self
.raw_model
= gtk
.TreeStore(*Column
.column_types())
408 self
.view_model
= self
.raw_model
.filter_new()
409 self
.model
.set_sort_column_id(URI
.idx
, gtk
.SORT_ASCENDING
)
410 self
.tree_view
= widgets
.get_widget('treeview')
411 self
.tree_view
.set_model(self
.view_model
)
412 Column
.add_all(self
.tree_view
)
414 # Sort / Filter options:
416 def init_combo(combobox
, items
, on_select
):
417 liststore
= gtk
.ListStore(str)
418 combobox
.set_model(liststore
)
419 cell
= gtk
.CellRendererText()
420 combobox
.pack_start(cell
, True)
421 combobox
.add_attribute(cell
, 'text', 0)
423 combobox
.append_text(item
[0])
424 combobox
.set_active(0)
426 selected_item
= combobox
.get_active()
427 on_select(selected_item
)
428 combobox
.connect('changed', lambda *a
: on_select(items
[combobox
.get_active()]))
430 def set_sort_order(sort_order
):
431 print "SORT: %r" % (sort_order
,)
432 name
, column
, order
= sort_order
433 self
.model
.set_sort_column_id(column
.idx
, order
)
434 self
.sort_combo
= widgets
.get_widget('sort_combo')
435 init_combo(self
.sort_combo
, SORT_OPTIONS
, set_sort_order
)
438 print "FILTER: %r" % (f
,)
439 description
, filter_func
= f
440 self
.view_model
= self
.model
.filter_new()
441 self
.view_model
.set_visible_func(filter_func
)
442 self
.tree_view
.set_model(self
.view_model
)
443 self
.set_initial_expansion()
444 self
.filter_combo
= widgets
.get_widget('filter_combo')
445 init_combo(self
.filter_combo
, FILTER_OPTIONS
, set_filter
)
447 def button_press(tree_view
, bev
):
450 pos
= tree_view
.get_path_at_pos(int(bev
.x
), int(bev
.y
))
453 path
, col
, x
, y
= pos
454 obj
= self
.model
[path
][ITEM_OBJECT
.idx
]
455 if obj
and hasattr(obj
, 'menu_items'):
456 popup_menu(bev
, obj
, model
=self
.model
, path
=path
, cache_explorer
=self
)
457 self
.tree_view
.connect('button-press-event', button_press
)
460 window
.set_default_response(gtk
.RESPONSE_CLOSE
)
462 selection
= self
.tree_view
.get_selection()
463 def selection_changed(selection
):
465 for x
in get_selected_paths(self
.tree_view
):
466 obj
= self
.model
[x
][ITEM_OBJECT
.idx
]
467 if obj
is None or not obj
.may_delete
:
468 window
.set_response_sensitive(DELETE
, False)
471 window
.set_response_sensitive(DELETE
, any_selected
)
472 selection
.set_mode(gtk
.SELECTION_MULTIPLE
)
473 selection
.connect('changed', selection_changed
)
474 selection_changed(selection
)
476 def response(dialog
, resp
):
477 if resp
== gtk
.RESPONSE_CLOSE
:
479 elif resp
== gtk
.RESPONSE_HELP
:
483 window
.connect('response', response
)
487 return self
.view_model
.get_model()
493 paths
= get_selected_paths(self
.tree_view
)
496 item
= model
[path
][ITEM_OBJECT
.idx
]
501 errors
.append(str(ex
))
503 model
.remove(model
.get_iter(path
))
507 gtkutils
.show_message_box(self
.window
, _("Failed to delete:\n%s") % '\n'.join(errors
))
510 """Display the window and scan the caches to populate it."""
512 self
.window
.window
.set_cursor(gtkutils
.get_busy_pointer())
514 self
._populate
_model
()
515 self
.set_initial_expansion()
517 def set_initial_expansion(self
):
520 i
= model
.get_iter_root()
522 # expand only "Interfaces"
523 if model
[i
][ITEM_OBJECT
.idx
] is SECTION_INTERFACES
:
524 self
.tree_view
.expand_row(model
.get_path(i
), False)
525 i
= model
.iter_next(i
)
527 self
.window
.window
.set_cursor(None)
529 def _populate_model(self
):
530 # Find cached implementations
532 unowned
= {} # Impl ID -> Store
533 duplicates
= [] # TODO
535 for s
in self
.iface_cache
.stores
.stores
:
536 if os
.path
.isdir(s
.dir):
537 for id in os
.listdir(s
.dir):
539 duplicates
.append(id)
543 error_interfaces
= []
545 # Look through cached interfaces for implementation owners
546 all
= self
.iface_cache
.list_all_interfaces()
551 if os
.path
.isabs(uri
):
553 interface_type
= LocalInterface
555 interface_type
= RemoteInterface
556 cached_iface
= basedir
.load_first_cache(namespaces
.config_site
,
557 'interfaces', model
.escape(uri
))
558 user_overrides
= basedir
.load_first_config(namespaces
.config_site
,
559 namespaces
.config_prog
,
560 'user_overrides', model
.escape(uri
))
562 iface_size
= size_if_exists(cached_iface
) + size_if_exists(user_overrides
)
563 iface
= self
.iface_cache
.get_interface(uri
)
564 except Exception, ex
:
565 error_interfaces
.append((uri
, str(ex
), iface_size
))
567 cached_iface
= interface_type(iface
, iface_size
)
568 for impl
in iface
.implementations
.values():
570 cached_iface
.in_cache
.append(LocalImplementation(impl
))
571 for digest
in impl
.digests
:
572 if digest
in unowned
:
573 cached_dir
= unowned
[digest
].dir
574 impl_path
= os
.path
.join(cached_dir
, digest
)
575 impl_size
= get_size(impl_path
)
576 cached_iface
.in_cache
.append(KnownImplementation(cached_iface
, cached_dir
, impl
, impl_size
, digest
))
578 cached_iface
.in_cache
.sort()
579 ok_interfaces
.append(cached_iface
)
582 iter = SECTION_INVALID_INTERFACES
.append_to(self
.raw_model
)
583 for uri
, ex
, size
in error_interfaces
:
584 item
= InvalidInterface(uri
, ex
, size
)
585 item
.append_to(self
.raw_model
, iter)
588 local_dir
= os
.path
.join(basedir
.xdg_cache_home
, '0install.net', 'implementations')
590 if unowned
[id].dir == local_dir
:
591 impl
= UnusedImplementation(local_dir
, id)
592 unowned_sizes
.append((impl
.size
, impl
))
594 iter = SECTION_UNOWNED_IMPLEMENTATIONS
.append_to(self
.raw_model
)
595 for size
, item
in unowned_sizes
:
596 item
.append_to(self
.raw_model
, iter)
599 iter = SECTION_INTERFACES
.append_to(self
.raw_model
)
600 for item
in ok_interfaces
:
601 item
.append_to(self
.raw_model
, iter)
604 def _update_sizes(self
):
605 """Set TOTAL_SIZE and PRETTY_SIZE to the total size, including all children."""
608 total
= m
[itr
][SELF_SIZE
.idx
]
609 total
+= sum(map(update
, all_children(m
, itr
)))
610 m
[itr
][PRETTY_SIZE
.idx
] = support
.pretty_size(total
) if total
else '-'
611 m
[itr
][TOTAL_SIZE
.idx
] = total
613 itr
= m
.get_iter_root()
616 itr
= m
.iter_next(itr
)
620 ('URI', URI
, gtk
.SORT_ASCENDING
),
621 ('Name', NAME
, gtk
.SORT_ASCENDING
),
622 ('Size', TOTAL_SIZE
, gtk
.SORT_DESCENDING
),
626 def filter_only(filterable_types
, filter_func
):
627 def _filter(model
, iter):
628 obj
= model
.get_value(iter, ITEM_OBJECT
.idx
)
629 if any((isinstance(obj
, t
) for t
in filterable_types
)):
630 result
= filter_func(model
, iter)
636 return lambda *a
: not func(*a
)
638 def is_local_feed(model
, iter):
639 return isinstance(model
[iter][ITEM_OBJECT
.idx
], LocalInterface
)
641 def has_implementations(model
, iter):
642 return model
.iter_has_child(iter)
645 ('All', lambda *a
: True),
646 ('Feeds with implementations', filter_only([ValidInterface
], has_implementations
)),
647 ('Feeds without implementations', filter_only([ValidInterface
], not_(has_implementations
))),
648 ('Local Feeds', filter_only([ValidInterface
], is_local_feed
)),
649 ('Remote Feeds', filter_only([ValidInterface
], not_(is_local_feed
))),
651 FILTER_OPTIONS
= init_filters()
654 cache_help
= help_box
.HelpBox(_("Cache Explorer Help"),
655 (_('Overview'), '\n' +
656 _("""When you run a program using Zero Install, it downloads the program's 'interface' file, \
657 which gives information about which versions of the program are available. This interface \
658 file is stored in the cache to save downloading it next time you run the program.
660 When you have chosen which version (implementation) of the program you want to \
661 run, Zero Install downloads that version and stores it in the cache too. Zero Install lets \
662 you have many different versions of each program on your computer at once. This is useful, \
663 since it lets you use an old version if needed, and different programs may need to use \
664 different versions of libraries in some cases.
666 The cache viewer shows you all the interfaces and implementations in your cache. \
667 This is useful to find versions you don't need anymore, so that you can delete them and \
668 free up some disk space.""")),
670 (_('Invalid interfaces'), '\n' +
671 _("""The cache viewer gets a list of all interfaces in your cache. However, some may not \
672 be valid; they are shown in the 'Invalid interfaces' section. It should be fine to \
673 delete these. An invalid interface may be caused by a local interface that no longer \
674 exists, by a failed attempt to download an interface (the name ends in '.new'), or \
675 by the interface file format changing since the interface was downloaded.""")),
677 (_('Unowned implementations and temporary files'), '\n' +
678 _("""The cache viewer searches through all the interfaces to find out which implementations \
679 they use. If no interface uses an implementation, it is shown in the 'Unowned implementations' \
682 Unowned implementations can result from old versions of a program no longer being listed \
683 in the interface file. Temporary files are created when unpacking an implementation after \
684 downloading it. If the archive is corrupted, the unpacked files may be left there. Unless \
685 you are currently unpacking new programs, it should be fine to delete everything in this \
688 (_('Interfaces'), '\n' +
689 _("""All remaining interfaces are listed in this section. You may wish to delete old versions of \
690 certain programs. Deleting a program which you may later want to run will require it to be downloaded \
691 again. Deleting a version of a program which is currently running may cause it to crash, so be careful!""")))