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
, cgi
.escape(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
)
156 def size_if_exists(path
):
157 "Get the size for a file, or 0 if it doesn't exist."
158 if path
and os
.path
.isfile(path
):
159 return os
.path
.getsize(path
)
163 "Get the size for a directory tree. Get the size from the .manifest if possible."
164 man
= os
.path
.join(path
, '.manifest')
165 if os
.path
.exists(man
):
166 size
= os
.path
.getsize(man
)
167 for line
in file(man
, 'rb'):
169 size
+= int(line
.split(' ', 4)[3])
172 for root
, dirs
, files
in os
.walk(path
):
174 size
+= os
.path
.getsize(os
.path
.join(root
, name
))
179 return iface
.get_name() + ' - ' + iface
.summary
180 return iface
.get_name()
182 def get_selected_paths(tree_view
):
183 "GTK 2.0 doesn't have this built-in"
184 selection
= tree_view
.get_selection()
186 def add(model
, path
, iter):
188 selection
.selected_foreach(add
)
191 def all_children(model
, iter):
192 "make a python generator out of the children of `iter`"
193 iter = model
.iter_children(iter)
196 iter = model
.iter_next(iter)
200 SAFE_MODE
= False # really delete things
201 #SAFE_MODE = True # print deletes, instead of performing them
203 class CachedInterface(object):
204 def __init__(self
, uri
, size
):
209 if not os
.path
.isabs(self
.uri
):
210 cached_iface
= basedir
.load_first_cache(namespaces
.config_site
,
211 'interfaces', model
.escape(self
.uri
))
214 print "Delete", cached_iface
216 os
.unlink(cached_iface
)
217 user_overrides
= basedir
.load_first_config(namespaces
.config_site
,
218 namespaces
.config_prog
,
219 'user_overrides', model
.escape(self
.uri
))
222 print "Delete", cached_iface
224 os
.unlink(user_overrides
)
226 def __cmp__(self
, other
):
227 return self
.uri
.__cmp
__(other
.uri
)
229 class ValidInterface(CachedInterface
):
230 def __init__(self
, iface
, size
):
231 CachedInterface
.__init
__(self
, iface
.uri
, size
)
235 def delete_children(self
):
236 deletable
= self
.deletable_children()
237 undeletable
= list(filter(lambda child
: not child
.may_delete
, self
.in_cache
))
238 # the only undeletable items we expect to encounter are LocalImplementations
239 unexpected_undeletable
= list(filter(lambda child
: not isinstance(child
, LocalImplementation
), undeletable
))
240 assert not unexpected_undeletable
, "unexpected undeletable items!: %r" % (unexpected_undeletable
,)
241 [child
.delete() for child
in deletable
]
244 self
.delete_children()
245 super(ValidInterface
, self
).delete()
247 def append_to(self
, model
, iter):
248 iter2
= model
.append(iter, extract_columns(
249 name
=self
.iface
.get_name(),
251 tooltip
=self
.iface
.summary
,
253 for cached_impl
in self
.in_cache
:
254 cached_impl
.append_to(model
, iter2
)
256 def launch(self
, explorer
):
257 os
.spawnlp(os
.P_NOWAIT
, '0launch', '0launch', '--gui', self
.uri
)
259 def copy_uri(self
, explorer
):
260 clipboard
= gtk
.clipboard_get()
261 clipboard
.set_text(self
.uri
)
262 primary
= gtk
.clipboard_get('PRIMARY')
263 primary
.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'), 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, extract_columns(
296 name
=self
.uri
.rsplit('/', 1)[-1],
302 class LocalImplementation
:
305 def __init__(self
, impl
):
308 def append_to(self
, model
, iter):
309 model
.append(iter, extract_columns(
310 name
=self
.impl
.local_path
,
311 tooltip
=_('This is a local version, not held in the cache.'),
315 class CachedImplementation
:
318 def __init__(self
, cache_dir
, digest
):
319 self
.impl_path
= os
.path
.join(cache_dir
, digest
)
320 self
.size
= get_size(self
.impl_path
)
325 print "Delete", self
.impl_path
327 support
.ro_rmtree(self
.impl_path
)
329 def open_rox(self
, explorer
):
330 os
.spawnlp(os
.P_WAIT
, '0launch', '0launch', ROX_IFACE
, '-d', self
.impl_path
)
332 def verify(self
, explorer
):
334 manifest
.verify(self
.impl_path
)
335 except BadDigest
, ex
:
336 box
= gtk
.MessageDialog(None, 0,
337 gtk
.MESSAGE_WARNING
, gtk
.BUTTONS_OK
, str(ex
))
339 swin
= gtk
.ScrolledWindow()
340 buffer = gtk
.TextBuffer()
341 mono
= buffer.create_tag('mono', family
= 'Monospace')
342 buffer.insert_with_tags(buffer.get_start_iter(), ex
.detail
, mono
)
343 text
= gtk
.TextView(buffer)
344 text
.set_editable(False)
345 text
.set_cursor_visible(False)
347 swin
.set_shadow_type(gtk
.SHADOW_IN
)
348 swin
.set_border_width(4)
349 box
.vbox
.pack_start(swin
)
351 box
.set_resizable(True)
353 box
= gtk
.MessageDialog(None, 0,
354 gtk
.MESSAGE_INFO
, gtk
.BUTTONS_OK
,
355 _('Contents match digest; nothing has been changed.'))
359 def prompt_delete(self
, explorer
):
360 if warn(_("Really delete implementation?"), parent
=explorer
.window
):
364 menu_items
= [(_('Open in ROX-Filer'), open_rox
),
365 (_('Verify integrity'), verify
),
366 (_('Delete'), prompt_delete
)]
368 class UnusedImplementation(CachedImplementation
):
369 def append_to(self
, model
, iter):
370 model
.append(iter, extract_columns(
373 tooltip
=self
.impl_path
,
376 class KnownImplementation(CachedImplementation
):
377 def __init__(self
, cached_iface
, cache_dir
, impl
, impl_size
, digest
):
378 CachedImplementation
.__init
__(self
, cache_dir
, digest
)
379 self
.cached_iface
= cached_iface
381 self
.size
= impl_size
385 print "Delete", self
.impl
387 CachedImplementation
.delete(self
)
388 self
.cached_iface
.in_cache
.remove(self
)
390 def append_to(self
, model
, iter):
392 label
= _('Version %(implementation_version)s (%(arch)s)') % {
393 'implementation_version': impl
.get_version(),
394 'arch': impl
.arch
or 'any platform'}
396 model
.append(iter, extract_columns(
399 tooltip
=self
.impl_path
,
402 def __cmp__(self
, other
):
403 if hasattr(other
, 'impl'):
404 return self
.impl
.__cmp
__(other
.impl
)
408 """A graphical interface for viewing the cache and deleting old items."""
410 def __init__(self
, iface_cache
):
411 widgets
= gtkutils
.Template(os
.path
.join(os
.path
.dirname(__file__
), 'cache.ui'), 'cache')
412 self
.window
= window
= widgets
.get_widget('cache')
413 window
.set_default_size(gtk
.gdk
.screen_width() / 2, gtk
.gdk
.screen_height() / 2)
414 self
.iface_cache
= iface_cache
417 self
.raw_model
= gtk
.TreeStore(*Column
.column_types())
418 self
.view_model
= self
.raw_model
.filter_new()
419 self
.model
.set_sort_column_id(URI
.idx
, gtk
.SORT_ASCENDING
)
420 self
.tree_view
= widgets
.get_widget('treeview')
421 self
.tree_view
.set_model(self
.view_model
)
422 Column
.add_all(self
.tree_view
)
424 # Sort / Filter options:
426 def init_combo(combobox
, items
, on_select
):
427 liststore
= gtk
.ListStore(str)
428 combobox
.set_model(liststore
)
429 cell
= gtk
.CellRendererText()
430 combobox
.pack_start(cell
, True)
431 combobox
.add_attribute(cell
, 'text', 0)
433 combobox
.append_text(item
[0])
434 combobox
.set_active(0)
436 selected_item
= combobox
.get_active()
437 on_select(selected_item
)
438 combobox
.connect('changed', lambda *a
: on_select(items
[combobox
.get_active()]))
440 def set_sort_order(sort_order
):
441 #print "SORT: %r" % (sort_order,)
442 name
, column
, order
= sort_order
443 self
.model
.set_sort_column_id(column
.idx
, order
)
444 self
.sort_combo
= widgets
.get_widget('sort_combo')
445 init_combo(self
.sort_combo
, SORT_OPTIONS
, set_sort_order
)
448 #print "FILTER: %r" % (f,)
449 description
, filter_func
= f
450 self
.view_model
= self
.model
.filter_new()
451 self
.view_model
.set_visible_func(filter_func
)
452 self
.tree_view
.set_model(self
.view_model
)
453 self
.set_initial_expansion()
454 self
.filter_combo
= widgets
.get_widget('filter_combo')
455 init_combo(self
.filter_combo
, FILTER_OPTIONS
, set_filter
)
457 def button_press(tree_view
, bev
):
460 pos
= tree_view
.get_path_at_pos(int(bev
.x
), int(bev
.y
))
463 path
, col
, x
, y
= pos
464 obj
= self
.model
[path
][ITEM_OBJECT
.idx
]
465 if obj
and hasattr(obj
, 'menu_items'):
466 popup_menu(bev
, obj
, model
=self
.model
, path
=path
, cache_explorer
=self
)
467 self
.tree_view
.connect('button-press-event', button_press
)
470 window
.set_default_response(gtk
.RESPONSE_CLOSE
)
472 selection
= self
.tree_view
.get_selection()
473 def selection_changed(selection
):
475 for x
in get_selected_paths(self
.tree_view
):
476 obj
= self
.model
[x
][ITEM_OBJECT
.idx
]
477 if obj
is None or not obj
.may_delete
:
478 window
.set_response_sensitive(DELETE
, False)
481 window
.set_response_sensitive(DELETE
, any_selected
)
482 selection
.set_mode(gtk
.SELECTION_MULTIPLE
)
483 selection
.connect('changed', selection_changed
)
484 selection_changed(selection
)
486 def response(dialog
, resp
):
487 if resp
== gtk
.RESPONSE_CLOSE
:
489 elif resp
== gtk
.RESPONSE_HELP
:
493 window
.connect('response', response
)
497 return self
.view_model
.get_model()
503 paths
= get_selected_paths(self
.tree_view
)
506 item
= model
[path
][ITEM_OBJECT
.idx
]
511 errors
.append(str(ex
))
513 model
.remove(model
.get_iter(path
))
517 gtkutils
.show_message_box(self
.window
, _("Failed to delete:\n%s") % '\n'.join(errors
))
520 """Display the window and scan the caches to populate it."""
522 self
.window
.window
.set_cursor(gtkutils
.get_busy_pointer())
524 self
._populate
_model
()
525 self
.set_initial_expansion()
527 def set_initial_expansion(self
):
530 i
= model
.get_iter_root()
532 # expand only "Interfaces"
533 if model
[i
][ITEM_OBJECT
.idx
] is SECTION_INTERFACES
:
534 self
.tree_view
.expand_row(model
.get_path(i
), False)
535 i
= model
.iter_next(i
)
537 self
.window
.window
.set_cursor(None)
539 def _populate_model(self
):
540 # Find cached implementations
542 unowned
= {} # Impl ID -> Store
543 duplicates
= [] # TODO
545 for s
in self
.iface_cache
.stores
.stores
:
546 if os
.path
.isdir(s
.dir):
547 for id in os
.listdir(s
.dir):
549 duplicates
.append(id)
553 error_interfaces
= []
555 # Look through cached interfaces for implementation owners
556 all
= self
.iface_cache
.list_all_interfaces()
561 if os
.path
.isabs(uri
):
563 interface_type
= LocalInterface
565 interface_type
= RemoteInterface
566 cached_iface
= basedir
.load_first_cache(namespaces
.config_site
,
567 'interfaces', model
.escape(uri
))
568 user_overrides
= basedir
.load_first_config(namespaces
.config_site
,
569 namespaces
.config_prog
,
570 'user_overrides', model
.escape(uri
))
572 iface_size
= size_if_exists(cached_iface
) + size_if_exists(user_overrides
)
573 iface
= self
.iface_cache
.get_interface(uri
)
574 except Exception, ex
:
575 error_interfaces
.append((uri
, str(ex
), iface_size
))
577 cached_iface
= interface_type(iface
, iface_size
)
578 for impl
in iface
.implementations
.values():
580 cached_iface
.in_cache
.append(LocalImplementation(impl
))
581 for digest
in impl
.digests
:
582 if digest
in unowned
:
583 cached_dir
= unowned
[digest
].dir
584 impl_path
= os
.path
.join(cached_dir
, digest
)
585 impl_size
= get_size(impl_path
)
586 cached_iface
.in_cache
.append(KnownImplementation(cached_iface
, cached_dir
, impl
, impl_size
, digest
))
588 cached_iface
.in_cache
.sort()
589 ok_interfaces
.append(cached_iface
)
592 iter = SECTION_INVALID_INTERFACES
.append_to(self
.raw_model
)
593 for uri
, ex
, size
in error_interfaces
:
594 item
= InvalidInterface(uri
, ex
, size
)
595 item
.append_to(self
.raw_model
, iter)
598 local_dir
= os
.path
.join(basedir
.xdg_cache_home
, '0install.net', 'implementations')
600 if unowned
[id].dir == local_dir
:
601 impl
= UnusedImplementation(local_dir
, id)
602 unowned_sizes
.append((impl
.size
, impl
))
604 iter = SECTION_UNOWNED_IMPLEMENTATIONS
.append_to(self
.raw_model
)
605 for size
, item
in unowned_sizes
:
606 item
.append_to(self
.raw_model
, iter)
609 iter = SECTION_INTERFACES
.append_to(self
.raw_model
)
610 for item
in ok_interfaces
:
611 item
.append_to(self
.raw_model
, iter)
614 def _update_sizes(self
):
615 """Set TOTAL_SIZE and PRETTY_SIZE to the total size, including all children."""
618 total
= m
[itr
][SELF_SIZE
.idx
]
619 total
+= sum(map(update
, all_children(m
, itr
)))
620 m
[itr
][PRETTY_SIZE
.idx
] = support
.pretty_size(total
) if total
else '-'
621 m
[itr
][TOTAL_SIZE
.idx
] = total
623 itr
= m
.get_iter_root()
626 itr
= m
.iter_next(itr
)
630 ('URI', URI
, gtk
.SORT_ASCENDING
),
631 ('Name', NAME
, gtk
.SORT_ASCENDING
),
632 ('Size', TOTAL_SIZE
, gtk
.SORT_DESCENDING
),
636 def filter_only(filterable_types
, filter_func
):
637 def _filter(model
, iter):
638 obj
= model
.get_value(iter, ITEM_OBJECT
.idx
)
639 if any((isinstance(obj
, t
) for t
in filterable_types
)):
640 result
= filter_func(model
, iter)
646 return lambda *a
: not func(*a
)
648 def is_local_feed(model
, iter):
649 return isinstance(model
[iter][ITEM_OBJECT
.idx
], LocalInterface
)
651 def has_implementations(model
, iter):
652 return model
.iter_has_child(iter)
655 ('All', lambda *a
: True),
656 ('Feeds with implementations', filter_only([ValidInterface
], has_implementations
)),
657 ('Feeds without implementations', filter_only([ValidInterface
], not_(has_implementations
))),
658 ('Local Feeds', filter_only([ValidInterface
], is_local_feed
)),
659 ('Remote Feeds', filter_only([ValidInterface
], not_(is_local_feed
))),
661 FILTER_OPTIONS
= init_filters()
664 cache_help
= help_box
.HelpBox(_("Cache Explorer Help"),
665 (_('Overview'), '\n' +
666 _("""When you run a program using Zero Install, it downloads the program's 'interface' file, \
667 which gives information about which versions of the program are available. This interface \
668 file is stored in the cache to save downloading it next time you run the program.
670 When you have chosen which version (implementation) of the program you want to \
671 run, Zero Install downloads that version and stores it in the cache too. Zero Install lets \
672 you have many different versions of each program on your computer at once. This is useful, \
673 since it lets you use an old version if needed, and different programs may need to use \
674 different versions of libraries in some cases.
676 The cache viewer shows you all the interfaces and implementations in your cache. \
677 This is useful to find versions you don't need anymore, so that you can delete them and \
678 free up some disk space.""")),
680 (_('Invalid interfaces'), '\n' +
681 _("""The cache viewer gets a list of all interfaces in your cache. However, some may not \
682 be valid; they are shown in the 'Invalid interfaces' section. It should be fine to \
683 delete these. An invalid interface may be caused by a local interface that no longer \
684 exists, by a failed attempt to download an interface (the name ends in '.new'), or \
685 by the interface file format changing since the interface was downloaded.""")),
687 (_('Unowned implementations and temporary files'), '\n' +
688 _("""The cache viewer searches through all the interfaces to find out which implementations \
689 they use. If no interface uses an implementation, it is shown in the 'Unowned implementations' \
692 Unowned implementations can result from old versions of a program no longer being listed \
693 in the interface file. Temporary files are created when unpacking an implementation after \
694 downloading it. If the archive is corrupted, the unpacked files may be left there. Unless \
695 you are currently unpacking new programs, it should be fine to delete everything in this \
698 (_('Interfaces'), '\n' +
699 _("""All remaining interfaces are listed in this section. You may wish to delete old versions of \
700 certain programs. Deleting a program which you may later want to run will require it to be downloaded \
701 again. Deleting a version of a program which is currently running may cause it to crash, so be careful!""")))