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 __future__
import print_function
7 from zeroinstall
import _
11 from zeroinstall
.injector
import namespaces
, model
12 from zeroinstall
.zerostore
import BadDigest
, manifest
13 from zeroinstall
import support
14 from zeroinstall
.support
import basedir
15 from zeroinstall
.gtkui
import help_box
, gtkutils
17 __all__
= ['CacheExplorer']
19 ROX_IFACE
= 'http://rox.sourceforge.net/2005/interfaces/ROX-Filer'
24 def __init__(self
, name
, column_type
, resizable
=False, props
={}, hide
=False, markup
=False):
25 self
.idx
= len(self
.columns
)
26 self
.columns
.append(self
)
28 self
.column_type
= column_type
30 self
.resizable
= resizable
35 def column_types(cls
):
36 return [col
.column_type
for col
in cls
.columns
]
39 def add_all(cls
, tree_view
):
40 [col
.add(tree_view
) for col
in cls
.columns
]
43 cell
= gtk
.CellRendererText()
44 self
.set_props(cell
, self
.props
)
47 def set_props(self
, obj
, props
):
48 for k
,v
in props
.items():
49 obj
.set_property(k
, v
)
53 kwargs
= {'markup': self
.idx
}
55 kwargs
= {'text': self
.idx
}
56 column
= gtk
.TreeViewColumn(self
.name
, self
.get_cell(), **kwargs
)
57 if 'xalign' in self
.props
:
58 self
.set_props(column
, {'alignment': self
.props
['xalign']})
61 def add(self
, tree_view
):
64 column
= self
.get_column()
65 if self
.resizable
: column
.set_resizable(True)
66 tree_view
.append_column(column
)
68 NAME
= Column(_('Name'), str, hide
=True)
69 URI
= Column(_('URI'), str, hide
=True)
70 TOOLTIP
= Column(_('Description'), str, hide
=True)
71 ITEM_VIEW
= Column(_('Item'), str, props
={'ypad': 6, 'yalign': 0}, resizable
=True, markup
=True)
72 SELF_SIZE
= Column(_('Self Size'), int, hide
=True)
73 TOTAL_SIZE
= Column(_('Total Size'), int, hide
=True)
74 PRETTY_SIZE
= Column(_('Size'), str, props
={'xalign':1.0})
75 ITEM_OBJECT
= Column(_('Object'), object, hide
=True)
77 ACTION_REMOVE
= object() # just make a unique value
79 class Section(object):
81 def __init__(self
, name
, tooltip
):
83 self
.tooltip
= tooltip
85 def append_to(self
, model
):
86 return model
.append(None, extract_columns(
92 SECTION_INTERFACES
= Section(
94 _("Feeds in the cache"))
95 SECTION_UNOWNED_IMPLEMENTATIONS
= Section(
96 _("Unowned implementations and temporary files"),
97 _("These probably aren't needed any longer. You can delete them."))
98 SECTION_INVALID_INTERFACES
= Section(
99 _("Invalid feeds (unreadable)"),
100 _("These feeds exist in the cache but cannot be read. You should probably delete them."))
103 def extract_columns(**d
):
104 vals
= list(map(lambda x
:None, Column
.columns
))
105 def setcol(column
, val
):
106 vals
[column
.idx
] = val
108 name
= d
.get('name', None)
109 desc
= d
.get('desc', None)
110 uri
= d
.get('uri', None)
115 setcol(ITEM_VIEW
, '<span font-size="larger" weight="bold">%s</span>\n'
116 '<span color="#666666">%s</span>' % tuple(map(cgi
.escape
, (name
, uri
))))
118 setcol(ITEM_VIEW
, cgi
.escape(name
or desc
))
120 size
= d
.get('size', 0)
121 setcol(SELF_SIZE
, size
)
122 setcol(TOTAL_SIZE
, 0) # must be set to prevent type error
123 setcol(TOOLTIP
, d
.get('tooltip', None))
124 setcol(ITEM_OBJECT
, d
.get('object', None))
128 def popup_menu(bev
, obj
, model
, path
, cache_explorer
):
130 for i
in obj
.menu_items
:
132 item
= gtk
.SeparatorMenuItem()
135 item
= gtk
.MenuItem(name
)
136 def _cb(item
, cb
=cb
):
137 action_required
= cb(obj
, cache_explorer
)
138 if action_required
is ACTION_REMOVE
:
139 model
.remove(model
.get_iter(path
))
140 item
.connect('activate', _cb
)
143 menu
.popup(None, None, None, bev
.button
, bev
.time
)
145 def warn(message
, parent
=None):
146 "Present a blocking warning message with OK/Cancel buttons, and return True if OK was pressed"
147 dialog
= gtk
.MessageDialog(parent
=parent
, buttons
=gtk
.BUTTONS_OK_CANCEL
, type=gtk
.MESSAGE_WARNING
)
148 dialog
.set_property('text', message
)
150 def _response(dialog
, resp
):
151 if resp
== gtk
.RESPONSE_OK
:
152 response
.append(True)
153 dialog
.connect('response', _response
)
156 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 feed
.get_name() + ' - ' + feed
.summary
182 return feed
.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 CachedFeed(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 'interfaces', model
._pretty
_escape
(self
.uri
))
224 print("Delete", user_overrides
)
226 os
.unlink(user_overrides
)
228 def __cmp__(self
, other
):
229 return self
.uri
.__cmp
__(other
.uri
)
231 class ValidFeed(CachedFeed
):
232 def __init__(self
, feed
, size
):
233 CachedFeed
.__init
__(self
, feed
.url
, 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(ValidFeed
, self
).delete()
249 def append_to(self
, model
, iter):
250 iter2
= model
.append(iter, extract_columns(
251 name
=self
.feed
.get_name(),
253 tooltip
=self
.feed
.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
)
264 primary
= gtk
.clipboard_get('PRIMARY')
265 primary
.set_text(self
.uri
)
267 def deletable_children(self
):
268 return list(filter(lambda child
: child
.may_delete
, self
.in_cache
))
270 def prompt_delete(self
, cache_explorer
):
271 description
= "\"%s\"" % (self
.feed
.get_name(),)
272 num_children
= len(self
.deletable_children())
274 description
+= _(" (and %s %s)") % (num_children
, _("implementation") if num_children
== 1 else _("implementations"))
275 if warn(_("Really delete %s?") % (description
,), parent
=cache_explorer
.window
):
279 menu_items
= [(_('Launch with GUI'), launch
),
280 (_('Copy URI'), copy_uri
),
281 (_('Delete'), prompt_delete
)]
283 class RemoteFeed(ValidFeed
):
286 class LocalFeed(ValidFeed
):
289 class InvalidFeed(CachedFeed
):
292 def __init__(self
, uri
, ex
, size
):
293 CachedFeed
.__init
__(self
, uri
, size
)
296 def append_to(self
, model
, iter):
297 model
.append(iter, extract_columns(
298 name
=self
.uri
.rsplit('/', 1)[-1],
304 class LocalImplementation
:
307 def __init__(self
, impl
):
310 def append_to(self
, model
, iter):
311 model
.append(iter, extract_columns(
312 name
=self
.impl
.local_path
,
313 tooltip
=_('This is a local version, not held in the cache.'),
317 class CachedImplementation
:
320 def __init__(self
, cache_dir
, digest
):
321 self
.impl_path
= os
.path
.join(cache_dir
, digest
)
322 self
.size
= get_size(self
.impl_path
)
327 print("Delete", self
.impl_path
)
329 support
.ro_rmtree(self
.impl_path
)
331 def open_rox(self
, explorer
):
332 os
.spawnlp(os
.P_WAIT
, '0launch', '0launch', ROX_IFACE
, '-d', self
.impl_path
)
334 def verify(self
, explorer
):
336 manifest
.verify(self
.impl_path
)
337 except BadDigest
as ex
:
338 box
= gtk
.MessageDialog(None, 0,
339 gtk
.MESSAGE_WARNING
, gtk
.BUTTONS_OK
, str(ex
))
341 swin
= gtk
.ScrolledWindow()
342 buffer = gtk
.TextBuffer()
343 mono
= buffer.create_tag('mono', family
= 'Monospace')
344 buffer.insert_with_tags(buffer.get_start_iter(), ex
.detail
, mono
)
345 text
= gtk
.TextView(buffer)
346 text
.set_editable(False)
347 text
.set_cursor_visible(False)
349 swin
.set_shadow_type(gtk
.SHADOW_IN
)
350 swin
.set_border_width(4)
351 box
.vbox
.pack_start(swin
)
353 box
.set_resizable(True)
355 box
= gtk
.MessageDialog(None, 0,
356 gtk
.MESSAGE_INFO
, gtk
.BUTTONS_OK
,
357 _('Contents match digest; nothing has been changed.'))
361 def prompt_delete(self
, explorer
):
362 if warn(_("Really delete implementation?"), parent
=explorer
.window
):
366 menu_items
= [(_('Open in ROX-Filer'), open_rox
),
367 (_('Verify integrity'), verify
),
368 (_('Delete'), prompt_delete
)]
370 class UnusedImplementation(CachedImplementation
):
371 def append_to(self
, model
, iter):
372 model
.append(iter, extract_columns(
375 tooltip
=self
.impl_path
,
378 class KnownImplementation(CachedImplementation
):
379 def __init__(self
, cached_iface
, cache_dir
, impl
, impl_size
, digest
):
380 CachedImplementation
.__init
__(self
, cache_dir
, digest
)
381 self
.cached_iface
= cached_iface
383 self
.size
= impl_size
387 print("Delete", self
.impl
)
389 CachedImplementation
.delete(self
)
390 self
.cached_iface
.in_cache
.remove(self
)
392 def append_to(self
, model
, iter):
394 label
= _('Version %(implementation_version)s (%(arch)s)') % {
395 'implementation_version': impl
.get_version(),
396 'arch': impl
.arch
or 'any platform'}
398 model
.append(iter, extract_columns(
401 tooltip
=self
.impl_path
,
404 def __cmp__(self
, other
):
405 if hasattr(other
, 'impl'):
406 return self
.impl
.__cmp
__(other
.impl
)
410 """A graphical interface for viewing the cache and deleting old items."""
412 def __init__(self
, iface_cache
):
413 widgets
= gtkutils
.Template(os
.path
.join(os
.path
.dirname(__file__
), 'cache.ui'), 'cache')
414 self
.window
= window
= widgets
.get_widget('cache')
415 window
.set_default_size(gtk
.gdk
.screen_width() / 2, gtk
.gdk
.screen_height() / 2)
416 self
.iface_cache
= iface_cache
419 self
.raw_model
= gtk
.TreeStore(*Column
.column_types())
420 self
.view_model
= self
.raw_model
.filter_new()
421 self
.model
.set_sort_column_id(URI
.idx
, gtk
.SORT_ASCENDING
)
422 self
.tree_view
= widgets
.get_widget('treeview')
423 self
.tree_view
.set_model(self
.view_model
)
424 Column
.add_all(self
.tree_view
)
426 # Sort / Filter options:
428 def init_combo(combobox
, items
, on_select
):
429 liststore
= gtk
.ListStore(str)
430 combobox
.set_model(liststore
)
431 cell
= gtk
.CellRendererText()
432 combobox
.pack_start(cell
, True)
433 combobox
.add_attribute(cell
, 'text', 0)
435 combobox
.append_text(item
[0])
436 combobox
.set_active(0)
438 selected_item
= combobox
.get_active()
439 on_select(selected_item
)
440 combobox
.connect('changed', lambda *a
: on_select(items
[combobox
.get_active()]))
442 def set_sort_order(sort_order
):
443 #print "SORT: %r" % (sort_order,)
444 name
, column
, order
= sort_order
445 self
.model
.set_sort_column_id(column
.idx
, order
)
446 self
.sort_combo
= widgets
.get_widget('sort_combo')
447 init_combo(self
.sort_combo
, SORT_OPTIONS
, set_sort_order
)
450 #print "FILTER: %r" % (f,)
451 description
, filter_func
= f
452 self
.view_model
= self
.model
.filter_new()
453 self
.view_model
.set_visible_func(filter_func
)
454 self
.tree_view
.set_model(self
.view_model
)
455 self
.set_initial_expansion()
456 self
.filter_combo
= widgets
.get_widget('filter_combo')
457 init_combo(self
.filter_combo
, FILTER_OPTIONS
, set_filter
)
459 def button_press(tree_view
, bev
):
462 pos
= tree_view
.get_path_at_pos(int(bev
.x
), int(bev
.y
))
465 path
, col
, x
, y
= pos
466 obj
= self
.model
[path
][ITEM_OBJECT
.idx
]
467 if obj
and hasattr(obj
, 'menu_items'):
468 popup_menu(bev
, obj
, model
=self
.model
, path
=path
, cache_explorer
=self
)
469 self
.tree_view
.connect('button-press-event', button_press
)
472 window
.set_default_response(gtk
.RESPONSE_CLOSE
)
474 selection
= self
.tree_view
.get_selection()
475 def selection_changed(selection
):
477 for x
in get_selected_paths(self
.tree_view
):
478 obj
= self
.model
[x
][ITEM_OBJECT
.idx
]
479 if obj
is None or not obj
.may_delete
:
480 window
.set_response_sensitive(DELETE
, False)
483 window
.set_response_sensitive(DELETE
, any_selected
)
484 selection
.set_mode(gtk
.SELECTION_MULTIPLE
)
485 selection
.connect('changed', selection_changed
)
486 selection_changed(selection
)
488 def response(dialog
, resp
):
489 if resp
== gtk
.RESPONSE_CLOSE
:
491 elif resp
== gtk
.RESPONSE_HELP
:
495 window
.connect('response', response
)
499 return self
.view_model
.get_model()
505 paths
= get_selected_paths(self
.tree_view
)
508 item
= model
[path
][ITEM_OBJECT
.idx
]
512 except OSError as ex
:
513 errors
.append(str(ex
))
515 model
.remove(model
.get_iter(path
))
519 gtkutils
.show_message_box(self
.window
, _("Failed to delete:\n%s") % '\n'.join(errors
))
522 """Display the window and scan the caches to populate it."""
524 self
.window
.window
.set_cursor(gtkutils
.get_busy_pointer())
526 self
._populate
_model
()
527 self
.set_initial_expansion()
529 def set_initial_expansion(self
):
532 i
= model
.get_iter_root()
534 # expand only "Feeds"
535 if model
[i
][ITEM_OBJECT
.idx
] is SECTION_INTERFACES
:
536 self
.tree_view
.expand_row(model
.get_path(i
), False)
537 i
= model
.iter_next(i
)
539 self
.window
.window
.set_cursor(None)
541 def _populate_model(self
):
542 # Find cached implementations
544 unowned
= {} # Impl ID -> Store
545 duplicates
= [] # TODO
547 for s
in self
.iface_cache
.stores
.stores
:
548 if os
.path
.isdir(s
.dir):
549 for id in os
.listdir(s
.dir):
551 duplicates
.append(id)
557 # Look through cached feeds for implementation owners
558 all_interfaces
= self
.iface_cache
.list_all_interfaces()
560 for uri
in all_interfaces
:
562 iface
= self
.iface_cache
.get_interface(uri
)
563 except Exception as ex
:
564 error_feeds
.append((uri
, str(ex
), 0))
566 all_feeds
.update(self
.iface_cache
.get_feeds(iface
))
568 for url
, feed
in all_feeds
.iteritems():
569 if not feed
: continue
573 # (e.g. for .new feeds)
574 raise Exception('Incorrect URL for feed (%s vs %s)' % (url
, feed
.url
))
576 if os
.path
.isabs(url
):
578 feed_type
= LocalFeed
580 feed_type
= RemoteFeed
581 cached_feed
= basedir
.load_first_cache(namespaces
.config_site
,
582 'interfaces', model
.escape(url
))
583 user_overrides
= basedir
.load_first_config(namespaces
.config_site
,
584 namespaces
.config_prog
,
585 'interfaces', model
._pretty
_escape
(url
))
587 feed_size
= size_if_exists(cached_feed
) + size_if_exists(user_overrides
)
588 except Exception as ex
:
589 error_feeds
.append((url
, str(ex
), feed_size
))
591 cached_feed
= feed_type(feed
, feed_size
)
592 for impl
in feed
.implementations
.values():
594 cached_feed
.in_cache
.append(LocalImplementation(impl
))
595 for digest
in impl
.digests
:
596 if digest
in unowned
:
597 cached_dir
= unowned
[digest
].dir
598 impl_path
= os
.path
.join(cached_dir
, digest
)
599 impl_size
= get_size(impl_path
)
600 cached_feed
.in_cache
.append(KnownImplementation(cached_feed
, cached_dir
, impl
, impl_size
, digest
))
602 cached_feed
.in_cache
.sort()
603 ok_feeds
.append(cached_feed
)
606 iter = SECTION_INVALID_INTERFACES
.append_to(self
.raw_model
)
607 for uri
, ex
, size
in error_feeds
:
608 item
= InvalidFeed(uri
, ex
, size
)
609 item
.append_to(self
.raw_model
, iter)
612 local_dir
= os
.path
.join(basedir
.xdg_cache_home
, '0install.net', 'implementations')
614 if unowned
[id].dir == local_dir
:
615 impl
= UnusedImplementation(local_dir
, id)
616 unowned_sizes
.append((impl
.size
, impl
))
618 iter = SECTION_UNOWNED_IMPLEMENTATIONS
.append_to(self
.raw_model
)
619 for size
, item
in unowned_sizes
:
620 item
.append_to(self
.raw_model
, iter)
623 iter = SECTION_INTERFACES
.append_to(self
.raw_model
)
624 for item
in ok_feeds
:
625 item
.append_to(self
.raw_model
, iter)
628 def _update_sizes(self
):
629 """Set TOTAL_SIZE and PRETTY_SIZE to the total size, including all children."""
632 total
= m
[itr
][SELF_SIZE
.idx
]
633 total
+= sum(map(update
, all_children(m
, itr
)))
634 m
[itr
][PRETTY_SIZE
.idx
] = support
.pretty_size(total
) if total
else '-'
635 m
[itr
][TOTAL_SIZE
.idx
] = total
637 itr
= m
.get_iter_root()
640 itr
= m
.iter_next(itr
)
644 ('URI', URI
, gtk
.SORT_ASCENDING
),
645 ('Name', NAME
, gtk
.SORT_ASCENDING
),
646 ('Size', TOTAL_SIZE
, gtk
.SORT_DESCENDING
),
650 def filter_only(filterable_types
, filter_func
):
651 def _filter(model
, iter):
652 obj
= model
.get_value(iter, ITEM_OBJECT
.idx
)
653 if any((isinstance(obj
, t
) for t
in filterable_types
)):
654 result
= filter_func(model
, iter)
660 return lambda *a
: not func(*a
)
662 def is_local_feed(model
, iter):
663 return isinstance(model
[iter][ITEM_OBJECT
.idx
], LocalFeed
)
665 def has_implementations(model
, iter):
666 return model
.iter_has_child(iter)
669 ('All', lambda *a
: True),
670 ('Feeds with implementations', filter_only([ValidFeed
], has_implementations
)),
671 ('Feeds without implementations', filter_only([ValidFeed
], not_(has_implementations
))),
672 ('Local Feeds', filter_only([ValidFeed
], is_local_feed
)),
673 ('Remote Feeds', filter_only([ValidFeed
], not_(is_local_feed
))),
675 FILTER_OPTIONS
= init_filters()
678 cache_help
= help_box
.HelpBox(_("Cache Explorer Help"),
679 (_('Overview'), '\n' +
680 _("""When you run a program using Zero Install, it downloads the program's 'feed' file, \
681 which gives information about which versions of the program are available. This feed \
682 file is stored in the cache to save downloading it next time you run the program.
684 When you have chosen which version (implementation) of the program you want to \
685 run, Zero Install downloads that version and stores it in the cache too. Zero Install lets \
686 you have many different versions of each program on your computer at once. This is useful, \
687 since it lets you use an old version if needed, and different programs may need to use \
688 different versions of libraries in some cases.
690 The cache viewer shows you all the feeds and implementations in your cache. \
691 This is useful to find versions you don't need anymore, so that you can delete them and \
692 free up some disk space.""")),
694 (_('Invalid feeds'), '\n' +
695 _("""The cache viewer gets a list of all feeds in your cache. However, some may not \
696 be valid; they are shown in the 'Invalid feeds' section. It should be fine to \
697 delete these. An invalid feed may be caused by a local feed that no longer \
698 exists or by a failed attempt to download a feed (the name ends in '.new').""")),
700 (_('Unowned implementations and temporary files'), '\n' +
701 _("""The cache viewer searches through all the feeds to find out which implementations \
702 they use. If no feed uses an implementation, it is shown in the 'Unowned implementations' \
705 Unowned implementations can result from old versions of a program no longer being listed \
706 in the feed file. Temporary files are created when unpacking an implementation after \
707 downloading it. If the archive is corrupted, the unpacked files may be left there. Unless \
708 you are currently unpacking new programs, it should be fine to delete everything in this \
712 _("""All remaining feeds are listed in this section. You may wish to delete old versions of \
713 certain programs. Deleting a program which you may later want to run will require it to be downloaded \
714 again. Deleting a version of a program which is currently running may cause it to crash, so be careful!""")))