1 """Display the contents of the implementation cache."""
2
3
4
5 from __future__ import print_function
6
7 from zeroinstall import _
8 import os, sys
9 import gtk
10
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, tasks
15 from zeroinstall.gtkui import help_box, gtkutils
16
17 __all__ = ['CacheExplorer']
18
19 ROX_IFACE = 'http://rox.sourceforge.net/2005/interfaces/ROX-Filer'
23 columns = []
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)
27 self.name = name
28 self.column_type = column_type
29 self.props = props
30 self.resizable = resizable
31 self.hide = hide
32 self.markup = markup
33
34 @classmethod
36 return [col.column_type for col in cls.columns]
37
38 @classmethod
40 [col.add(tree_view) for col in cls.columns]
41
43 cell = gtk.CellRendererText()
44 self.set_props(cell, self.props)
45 return cell
46
48 for k,v in props.items():
49 obj.set_property(k, v)
50
52 if self.markup:
53 kwargs = {'markup': self.idx}
54 else:
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']})
59 return column
60
61 - def add(self, tree_view):
62 if self.hide:
63 return
64 column = self.get_column()
65 if self.resizable: column.set_resizable(True)
66 tree_view.append_column(column)
67
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)
76
77 ACTION_REMOVE = object()
80 may_delete = False
82 self.name = name
83 self.tooltip = tooltip
84
86 return model.append(None, extract_columns(
87 name=self.name,
88 tooltip=self.tooltip,
89 object=self,
90 ))
91
92 SECTION_INTERFACES = Section(
93 _("Feeds"),
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."))
101
102 import cgi
104 vals = list(map(lambda x:None, Column.columns))
105 def setcol(column, val):
106 vals[column.idx] = val
107
108 name = d.get('name', None)
109 desc = d.get('desc', None)
110 uri = d.get('uri', None)
111
112 setcol(NAME, name)
113 setcol(URI, uri)
114 if name and uri:
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))))
117 else:
118 setcol(ITEM_VIEW, cgi.escape(name or desc))
119
120 size = d.get('size', 0)
121 setcol(SELF_SIZE, size)
122 setcol(TOTAL_SIZE, 0)
123 setcol(TOOLTIP, d.get('tooltip', None))
124 setcol(ITEM_OBJECT, d.get('object', None))
125 return vals
126
127 menu = None
129 global menu
130 menu = gtk.Menu()
131 for i in obj.menu_items:
132 if i is None:
133 item = gtk.SeparatorMenuItem()
134 else:
135 name, cb = i
136 item = gtk.MenuItem()
137 item.set_label(name)
138 def _cb(item, cb=cb):
139 action_required = cb(obj, cache_explorer)
140 if action_required is ACTION_REMOVE:
141 model.remove(model.get_iter(path))
142 item.connect('activate', _cb)
143 item.show()
144 menu.append(item)
145 if gtk.pygtk_version >= (2, 90):
146 menu.popup(None, None, None, None, bev.button, bev.time)
147 else:
148 menu.popup(None, None, None, bev.button, bev.time)
149
150 -def warn(message, parent=None):
151 "Present a blocking warning message with OK/Cancel buttons, and return True if OK was pressed"
152 dialog = gtk.MessageDialog(parent=parent, buttons=gtk.BUTTONS_OK_CANCEL, type=gtk.MESSAGE_WARNING)
153 dialog.set_property('text', message)
154 response = []
155 def _response(dialog, resp):
156 if resp == gtk.RESPONSE_OK:
157 response.append(True)
158 dialog.connect('response', _response)
159 dialog.run()
160 dialog.destroy()
161 return bool(response)
162
164 "Get the size for a file, or 0 if it doesn't exist."
165 if path and os.path.isfile(path):
166 return os.path.getsize(path)
167 return 0
168
170 "Get the size for a directory tree. Get the size from the .manifest if possible."
171 man = os.path.join(path, '.manifest')
172 if os.path.exists(man):
173 size = os.path.getsize(man)
174 with open(man, 'rt') as stream:
175 for line in stream:
176 if line[:1] in "XF":
177 size += int(line.split(' ', 4)[3])
178 else:
179 size = 0
180 for root, dirs, files in os.walk(path):
181 for name in files:
182 size += os.path.getsize(os.path.join(root, name))
183 return size
184
189
191 model, paths = tree_view.get_selection().get_selected_rows()
192 return paths
193
195 "make a python generator out of the children of `iter`"
196 iter = model.iter_children(iter)
197 while iter:
198 yield iter
199 iter = model.iter_next(iter)
200
201
202 DELETE = 0
203 SAFE_MODE = False
231
237
239 deletable = self.deletable_children()
240 undeletable = list(filter(lambda child: not child.may_delete, self.in_cache))
241
242 unexpected_undeletable = list(filter(lambda child: not isinstance(child, LocalImplementation), undeletable))
243 assert not unexpected_undeletable, "unexpected undeletable items!: %r" % (unexpected_undeletable,)
244 [child.delete() for child in deletable]
245
247 self.delete_children()
248 super(ValidFeed, self).delete()
249
251 iter2 = model.append(iter, extract_columns(
252 name=self.feed.get_name(),
253 uri=self.uri,
254 tooltip=self.feed.summary,
255 object=self))
256 for cached_impl in self.in_cache:
257 cached_impl.append_to(model, iter2)
258
260 os.spawnlp(os.P_NOWAIT, '0launch', '0launch', '--gui', self.uri)
261
263 clipboard = gtk.clipboard_get()
264 clipboard.set_text(self.uri)
265 primary = gtk.clipboard_get('PRIMARY')
266 primary.set_text(self.uri)
267
269 return list(filter(lambda child: child.may_delete, self.in_cache))
270
272 description = "\"%s\"" % (self.feed.get_name(),)
273 num_children = len(self.deletable_children())
274 if self.in_cache:
275 description += _(" (and %s %s)") % (num_children, _("implementation") if num_children == 1 else _("implementations"))
276 if warn(_("Really delete %s?") % (description,), parent=cache_explorer.window):
277 self.delete()
278 return ACTION_REMOVE
279
280 menu_items = [(_('Launch with GUI'), launch),
281 (_('Copy URI'), copy_uri),
282 (_('Delete'), prompt_delete)]
283
286
289
291 may_delete = True
292
296
298 model.append(iter, extract_columns(
299 name=self.uri.rsplit('/', 1)[-1],
300 uri=self.uri,
301 size=self.size,
302 tooltip=self.ex,
303 object=self))
304
306 may_delete = False
307
310
312 model.append(iter, extract_columns(
313 name=self.impl.local_path,
314 tooltip=_('This is a local version, not held in the cache.'),
315 object=self))
316
319 may_delete = True
320
325
331
333 os.spawnlp(os.P_WAIT, '0launch', '0launch', ROX_IFACE, '-d', self.impl_path)
334
336 try:
337 manifest.verify(self.impl_path)
338 except BadDigest as ex:
339 box = gtk.MessageDialog(None, 0,
340 gtk.MESSAGE_WARNING, gtk.BUTTONS_OK, str(ex))
341 if ex.detail:
342 swin = gtk.ScrolledWindow()
343 buffer = gtk.TextBuffer()
344 mono = buffer.create_tag('mono', family = 'Monospace')
345 buffer.insert_with_tags(buffer.get_start_iter(), ex.detail, mono)
346 text = gtk.TextView(buffer)
347 text.set_editable(False)
348 text.set_cursor_visible(False)
349 swin.add(text)
350 swin.set_shadow_type(gtk.SHADOW_IN)
351 swin.set_border_width(4)
352 box.vbox.pack_start(swin)
353 swin.show_all()
354 box.set_resizable(True)
355 else:
356 box = gtk.MessageDialog(None, 0,
357 gtk.MESSAGE_INFO, gtk.BUTTONS_OK,
358 _('Contents match digest; nothing has been changed.'))
359 box.run()
360 box.destroy()
361
363 if warn(_("Really delete implementation?"), parent=explorer.window):
364 self.delete()
365 return ACTION_REMOVE
366
367 if sys.version_info[0] > 2:
370
373
374 menu_items = [(_('Open in ROX-Filer'), open_rox),
375 (_('Verify integrity'), verify),
376 (_('Delete'), prompt_delete)]
377
380 model.append(iter, extract_columns(
381 name=self.digest,
382 size=self.size,
383 tooltip=self.impl_path,
384 object=self))
385
387 - def __init__(self, cached_iface, cache_dir, impl, impl_size, digest):
388 CachedImplementation.__init__(self, cache_dir, digest)
389 self.cached_iface = cached_iface
390 self.impl = impl
391 self.size = impl_size
392
394 if SAFE_MODE:
395 print("Delete", self.impl)
396 else:
397 CachedImplementation.delete(self)
398 self.cached_iface.in_cache.remove(self)
399
401 impl = self.impl
402 label = _('Version %(implementation_version)s (%(arch)s)') % {
403 'implementation_version': impl.get_version(),
404 'arch': impl.arch or 'any platform'}
405
406 model.append(iter, extract_columns(
407 name=label,
408 size=self.size,
409 tooltip=self.impl_path,
410 object=self))
411
413 if hasattr(other, 'impl'):
414 return self.impl.__cmp__(other.impl)
415 return -1
416
417 if sys.version_info[0] > 2:
419 return self.impl.__lt__(other.impl)
420
422 return self.impl.__eq__(other.impl)
423
425 """A graphical interface for viewing the cache and deleting old items."""
426
428 widgets = gtkutils.Template(os.path.join(os.path.dirname(__file__), 'cache.ui'), 'cache')
429 self.window = window = widgets.get_widget('cache')
430 window.set_default_size(gtk.gdk.screen_width() / 2, gtk.gdk.screen_height() / 2)
431 self.iface_cache = iface_cache
432
433
434 self.raw_model = gtk.TreeStore(*Column.column_types())
435 self.view_model = self.raw_model.filter_new()
436 self.model.set_sort_column_id(URI.idx, gtk.SORT_ASCENDING)
437 self.tree_view = widgets.get_widget('treeview')
438 Column.add_all(self.tree_view)
439
440
441
442 def init_combo(combobox, items, on_select):
443 liststore = gtk.ListStore(str)
444 combobox.set_model(liststore)
445 cell = gtk.CellRendererText()
446 combobox.pack_start(cell, True)
447 combobox.add_attribute(cell, 'text', 0)
448 for item in items:
449 combobox.append_text(item[0])
450 combobox.set_active(0)
451 def _on_select(*a):
452 selected_item = combobox.get_active()
453 on_select(selected_item)
454 combobox.connect('changed', lambda *a: on_select(items[combobox.get_active()]))
455
456 def set_sort_order(sort_order):
457
458 name, column, order = sort_order
459 self.model.set_sort_column_id(column.idx, order)
460 self.sort_combo = widgets.get_widget('sort_combo')
461 init_combo(self.sort_combo, SORT_OPTIONS, set_sort_order)
462
463 def set_filter(f):
464
465 description, filter_func = f
466 self.view_model = self.model.filter_new()
467 self.view_model.set_visible_func(filter_func)
468 self.tree_view.set_model(self.view_model)
469 self.set_initial_expansion()
470 self.filter_combo = widgets.get_widget('filter_combo')
471 init_combo(self.filter_combo, FILTER_OPTIONS, set_filter)
472
473 def button_press(tree_view, bev):
474 if bev.button != 3:
475 return False
476 pos = tree_view.get_path_at_pos(int(bev.x), int(bev.y))
477 if not pos:
478 return False
479 path, col, x, y = pos
480 obj = self.model[path][ITEM_OBJECT.idx]
481 if obj and hasattr(obj, 'menu_items'):
482 popup_menu(bev, obj, model=self.model, path=path, cache_explorer=self)
483 self.tree_view.connect('button-press-event', button_press)
484
485
486 window.set_default_response(gtk.RESPONSE_CLOSE)
487
488 selection = self.tree_view.get_selection()
489 def selection_changed(selection):
490 any_selected = False
491 for x in get_selected_paths(self.tree_view):
492 obj = self.model[x][ITEM_OBJECT.idx]
493 if obj is None or not obj.may_delete:
494 window.set_response_sensitive(DELETE, False)
495 return
496 any_selected = True
497 window.set_response_sensitive(DELETE, any_selected)
498 selection.set_mode(gtk.SELECTION_MULTIPLE)
499 selection.connect('changed', selection_changed)
500 selection_changed(selection)
501
502 def response(dialog, resp):
503 if resp == gtk.RESPONSE_CLOSE:
504 window.destroy()
505 elif resp == gtk.RESPONSE_HELP:
506 cache_help.display()
507 elif resp == DELETE:
508 self._delete()
509 window.connect('response', response)
510
511 @property
513 return self.view_model.get_model()
514
516 errors = []
517
518 model = self.model
519 paths = get_selected_paths(self.tree_view)
520 paths.reverse()
521 for path in paths:
522 item = model[path][ITEM_OBJECT.idx]
523 assert item.delete
524 try:
525 item.delete()
526 except OSError as ex:
527 errors.append(str(ex))
528 else:
529 model.remove(model.get_iter(path))
530 self._update_sizes()
531
532 if errors:
533 gtkutils.show_message_box(self.window, _("Failed to delete:\n%s") % '\n'.join(errors))
534
536 """Display the window and scan the caches to populate it."""
537 self.window.show()
538 self.window.get_window().set_cursor(gtkutils.get_busy_pointer())
539 gtk.gdk.flush()
540
541 @tasks.async
542 def populate():
543 populate = self._populate_model()
544 yield populate
545 try:
546 tasks.check(populate)
547 except:
548 import logging
549 logging.warn("fail", exc_info = True)
550 raise
551
552 self.tree_view.set_model(self.view_model)
553 self.set_initial_expansion()
554 return populate()
555
567
568 @tasks.async
570
571
572 unowned = {}
573 duplicates = []
574
575 for s in self.iface_cache.stores.stores:
576 if os.path.isdir(s.dir):
577 for id in os.listdir(s.dir):
578 if id in unowned:
579 duplicates.append(id)
580 unowned[id] = s
581
582 ok_feeds = []
583 error_feeds = []
584
585
586 all_interfaces = self.iface_cache.list_all_interfaces()
587 all_feeds = {}
588 for uri in all_interfaces:
589 try:
590 iface = self.iface_cache.get_interface(uri)
591 except Exception as ex:
592 error_feeds.append((uri, str(ex), 0))
593 else:
594 all_feeds.update(self.iface_cache.get_feeds(iface))
595
596 for url, feed in all_feeds.items():
597 if not feed: continue
598 yield
599 feed_size = 0
600 try:
601 if url != feed.url:
602
603 raise Exception('Incorrect URL for feed (%s vs %s)' % (url, feed.url))
604
605 if os.path.isabs(url):
606 cached_feed = url
607 feed_type = LocalFeed
608 else:
609 feed_type = RemoteFeed
610 cached_feed = basedir.load_first_cache(namespaces.config_site,
611 'interfaces', model.escape(url))
612 user_overrides = basedir.load_first_config(namespaces.config_site,
613 namespaces.config_prog,
614 'interfaces', model._pretty_escape(url))
615
616 feed_size = size_if_exists(cached_feed) + size_if_exists(user_overrides)
617 except Exception as ex:
618 error_feeds.append((url, str(ex), feed_size))
619 else:
620 cached_feed = feed_type(feed, feed_size)
621 for impl in feed.implementations.values():
622 if impl.local_path:
623 cached_feed.in_cache.append(LocalImplementation(impl))
624 for digest in impl.digests:
625 if digest in unowned:
626 cached_dir = unowned[digest].dir
627 impl_path = os.path.join(cached_dir, digest)
628 impl_size = get_size(impl_path)
629 cached_feed.in_cache.append(KnownImplementation(cached_feed, cached_dir, impl, impl_size, digest))
630 del unowned[digest]
631 cached_feed.in_cache.sort()
632 ok_feeds.append(cached_feed)
633
634 if error_feeds:
635 iter = SECTION_INVALID_INTERFACES.append_to(self.raw_model)
636 for uri, ex, size in error_feeds:
637 item = InvalidFeed(uri, ex, size)
638 item.append_to(self.raw_model, iter)
639
640 unowned_sizes = []
641 local_dir = os.path.join(basedir.xdg_cache_home, '0install.net', 'implementations')
642 for id in unowned:
643 if unowned[id].dir == local_dir:
644 impl = UnusedImplementation(local_dir, id)
645 unowned_sizes.append((impl.size, impl))
646 if unowned_sizes:
647 iter = SECTION_UNOWNED_IMPLEMENTATIONS.append_to(self.raw_model)
648 for size, item in unowned_sizes:
649 item.append_to(self.raw_model, iter)
650
651 if ok_feeds:
652 iter = SECTION_INTERFACES.append_to(self.raw_model)
653 for item in ok_feeds:
654 yield
655 item.append_to(self.raw_model, iter)
656 self._update_sizes()
657
659 """Set TOTAL_SIZE and PRETTY_SIZE to the total size, including all children."""
660 m = self.raw_model
661 def update(itr):
662 total = m[itr][SELF_SIZE.idx]
663 total += sum(map(update, all_children(m, itr)))
664 m[itr][PRETTY_SIZE.idx] = support.pretty_size(total) if total else '-'
665 m[itr][TOTAL_SIZE.idx] = total
666 return total
667 itr = m.get_iter_root()
668 while itr:
669 update(itr)
670 itr = m.iter_next(itr)
671
672
673 SORT_OPTIONS = [
674 ('URI', URI, gtk.SORT_ASCENDING),
675 ('Name', NAME, gtk.SORT_ASCENDING),
676 ('Size', TOTAL_SIZE, gtk.SORT_DESCENDING),
677 ]
680 def filter_only(filterable_types, filter_func):
681 def _filter(model, iter):
682 obj = model.get_value(iter, ITEM_OBJECT.idx)
683 if any((isinstance(obj, t) for t in filterable_types)):
684 result = filter_func(model, iter)
685 return result
686 return True
687 return _filter
688
689 def not_(func):
690 return lambda *a: not func(*a)
691
692 def is_local_feed(model, iter):
693 return isinstance(model[iter][ITEM_OBJECT.idx], LocalFeed)
694
695 def has_implementations(model, iter):
696 return model.iter_has_child(iter)
697
698 return [
699 ('All', lambda *a: True),
700 ('Feeds with implementations', filter_only([ValidFeed], has_implementations)),
701 ('Feeds without implementations', filter_only([ValidFeed], not_(has_implementations))),
702 ('Local Feeds', filter_only([ValidFeed], is_local_feed)),
703 ('Remote Feeds', filter_only([ValidFeed], not_(is_local_feed))),
704 ]
705 FILTER_OPTIONS = init_filters()
706
707
708 cache_help = help_box.HelpBox(_("Cache Explorer Help"),
709 (_('Overview'), '\n' +
710 _("""When you run a program using Zero Install, it downloads the program's 'feed' file, \
711 which gives information about which versions of the program are available. This feed \
712 file is stored in the cache to save downloading it next time you run the program.
713
714 When you have chosen which version (implementation) of the program you want to \
715 run, Zero Install downloads that version and stores it in the cache too. Zero Install lets \
716 you have many different versions of each program on your computer at once. This is useful, \
717 since it lets you use an old version if needed, and different programs may need to use \
718 different versions of libraries in some cases.
719
720 The cache viewer shows you all the feeds and implementations in your cache. \
721 This is useful to find versions you don't need anymore, so that you can delete them and \
722 free up some disk space.""")),
723
724 (_('Invalid feeds'), '\n' +
725 _("""The cache viewer gets a list of all feeds in your cache. However, some may not \
726 be valid; they are shown in the 'Invalid feeds' section. It should be fine to \
727 delete these. An invalid feed may be caused by a local feed that no longer \
728 exists or by a failed attempt to download a feed (the name ends in '.new').""")),
729
730 (_('Unowned implementations and temporary files'), '\n' +
731 _("""The cache viewer searches through all the feeds to find out which implementations \
732 they use. If no feed uses an implementation, it is shown in the 'Unowned implementations' \
733 section.
734
735 Unowned implementations can result from old versions of a program no longer being listed \
736 in the feed file. Temporary files are created when unpacking an implementation after \
737 downloading it. If the archive is corrupted, the unpacked files may be left there. Unless \
738 you are currently unpacking new programs, it should be fine to delete everything in this \
739 section.""")),
740
741 (_('Feeds'), '\n' +
742 _("""All remaining feeds are listed in this section. You may wish to delete old versions of \
743 certain programs. Deleting a program which you may later want to run will require it to be downloaded \
744 again. Deleting a version of a program which is currently running may cause it to crash, so be careful!""")))
745