Logo Search packages:      
Sourcecode: zeroinstall-injector version File versions

cache.py

import os, shutil
import gtk, gobject

import gui
import help_box
from dialog import Dialog
from zeroinstall.injector.iface_cache import iface_cache
from zeroinstall.injector import basedir, namespaces, model
from zeroinstall.zerostore import BadDigest, manifest
from treetips import TreeTips

ROX_IFACE = 'http://rox.sourceforge.net/2005/interfaces/ROX-Filer'

# Model columns
ITEM = 0
SELF_SIZE = 1
PRETTY_SIZE = 2
TOOLTIP = 3
ITEM_OBJECT = 4

# This is a copy of zerostore.manifest.verify, but that's only in version 0.20 and later...
def _verify(root):
      """Ensure that directory 'dir' generates the given digest.
      Raises BadDigest if not. For a non-error return:
      - Dir's name must be a digest (in the form "alg=value")
      - The calculated digest of the contents must match this name.
      - If there is a .manifest file, then its digest must also match."""

      if hasattr(manifest, 'verify'):
            # Use the main version if available
            return manifest.verify(root)

      import sha
      
      required_digest = os.path.basename(root)
      if not required_digest.startswith('sha1='):
            raise BadDigest("Directory name '%s' does not start with 'sha1='" %
                  required_digest)

      digest = sha.new()
      lines = []
      for line in manifest.generate_manifest(root):
            line += '\n'
            digest.update(line)
            lines.append(line)
      actual_digest = 'sha1=' + digest.hexdigest()

      manifest_file = os.path.join(root, '.manifest')
      if os.path.isfile(manifest_file):
            digest = sha.new()
            digest.update(file(manifest_file).read())
            manifest_digest = 'sha1=' + digest.hexdigest()
      else:
            manifest_digest = None

      if required_digest == actual_digest == manifest_digest:
            return

      error = BadDigest("Cached item does NOT verify.")
      
      error.detail = " Expected digest: " + required_digest + "\n" + \
                   "   Actual digest: " + actual_digest + "\n" + \
                   ".manifest digest: " + (manifest_digest or 'No .manifest file') + "\n\n"

      if manifest_digest is None:
            error.detail += "No .manifest, so no further details available."
      elif manifest_digest == actual_digest:
            error.detail += "The .manifest file matches the actual contents. Very strange!"
      elif manifest_digest == required_digest:
            import difflib
            diff = difflib.unified_diff(file(manifest_file).readlines(), lines,
                                  'Recorded', 'Actual')
            error.detail += "The .manifest file matches the directory name.\n" \
                        "The contents of the directory have changed:\n" + \
                        ''.join(diff)
      elif required_digest == actual_digest:
            error.detail += "The directory contents are correct, but the .manifest file is wrong!"
      else:
            error.detail += "The .manifest file matches neither of the other digests. Odd."
      raise error

def popup_menu(bev, obj):
      menu = gtk.Menu()
      for i in obj.menu_items:
            if i is None:
                  item = gtk.SeparatorMenuItem()
            else:
                  name, cb = i
                  item = gtk.MenuItem(name)
                  item.connect('activate', lambda item, cb=cb: cb(obj))
            item.show()
            menu.append(item)
      menu.popup(None, None, None, bev.button, bev.time)

def pretty_size(size):
      if size == 0: return ''
      return gui.pretty_size(size)

def size_if_exists(path):
      "Get the size for a file, or 0 if it doesn't exist."
      if path and os.path.isfile(path):
            return os.path.getsize(path)
      return 0

def get_size(path):
      "Get the size for a directory tree. Get the size from the .manifest if possible."
      man = os.path.join(path, '.manifest')
      if os.path.exists(man):
            size = os.path.getsize(man)
            for line in file(man):
                  if line[:1] in "XF":
                        size += long(line.split(' ', 4)[3])
      else:
            size = 0
            for root, dirs, files in os.walk(path):
                  for name in files:
                        size += os.path.getsize(os.path.join(root, name))
      return size

def summary(iface):
      if iface.summary:
            return iface.get_name() + ' - ' + iface.summary
      return iface.get_name()

def get_selected_paths(tree_view):
      "GTK 2.0 doesn't have this built-in"
      selection = tree_view.get_selection()
      paths = []
      def add(model, path, iter):
            paths.append(path)
      selection.selected_foreach(add)
      return paths

tips = TreeTips()

# Responses
DELETE = 0

class CachedInterface:
      def __init__(self, uri, size):
            self.uri = uri
            self.size = size

      def delete(self):
            if not self.uri.startswith('/'):
                  cached_iface = basedir.load_first_cache(namespaces.config_site,
                              'interfaces', model.escape(self.uri))
                  if cached_iface:
                        #print "Delete", cached_iface
                        os.unlink(cached_iface)
            user_overrides = basedir.load_first_config(namespaces.config_site,
                              namespaces.config_prog,
                              'user_overrides', model.escape(self.uri))
            if user_overrides:
                  #print "Delete", user_overrides
                  os.unlink(user_overrides)
      
      def __cmp__(self, other):
            return self.uri.__cmp__(other.uri)

class ValidInterface(CachedInterface):
      def __init__(self, iface, size):
            CachedInterface.__init__(self, iface.uri, size)
            self.iface = iface
            self.in_cache = []

      def append_to(self, model, iter):
            iter2 = model.append(iter,
                          [self.uri, self.size, None, summary(self.iface), self])
            for cached_impl in self.in_cache:
                  cached_impl.append_to(model, iter2)
      
      def get_may_delete(self):
            for c in self.in_cache:
                  if not isinstance(c, LocalImplementation):
                        return False      # Still some impls cached
            return True

      may_delete = property(get_may_delete)
      
class InvalidInterface(CachedInterface):
      may_delete = True

      def __init__(self, uri, ex, size):
            CachedInterface.__init__(self, uri, size)
            self.ex = ex

      def append_to(self, model, iter):
            model.append(iter, [self.uri, self.size, None, self.ex, self])
      
class LocalImplementation:
      may_delete = False

      def __init__(self, impl):
            self.impl = impl

      def append_to(self, model, iter):
            model.append(iter, [self.impl.id, 0, None, 'This is a local version, not held in the cache.', self])

class CachedImplementation:
      may_delete = True

      def __init__(self, cache_dir, name):
            self.impl_path = os.path.join(cache_dir, name)
            self.size = get_size(self.impl_path)
            self.name = name

      def delete(self):
            #print "Delete", self.impl_path
            shutil.rmtree(self.impl_path)
      
      def open_rox(self):
            os.spawnlp(os.P_WAIT, '0launch', '0launch', ROX_IFACE, '-d', self.impl_path)
      
      def verify(self):
            try:
                  _verify(self.impl_path)
            except BadDigest, ex:
                  box = gtk.MessageDialog(None, 0,
                                    gtk.MESSAGE_WARNING, gtk.BUTTONS_OK, str(ex))
                  if ex.detail:
                        swin = gtk.ScrolledWindow()
                        buffer = gtk.TextBuffer()
                        mono = buffer.create_tag('mono', family = 'Monospace')
                        buffer.insert_with_tags(buffer.get_start_iter(), ex.detail, mono)
                        text = gtk.TextView(buffer)
                        text.set_editable(False)
                        text.set_cursor_visible(False)
                        swin.add(text)
                        swin.set_shadow_type(gtk.SHADOW_IN)
                        swin.set_border_width(4)
                        box.vbox.pack_start(swin)
                        swin.show_all()
                        box.set_resizable(True)
            else:
                  box = gtk.MessageDialog(None, 0,
                                    gtk.MESSAGE_INFO, gtk.BUTTONS_OK,
                                    'Contents match digest; nothing has been changed.')
            box.run()
            box.destroy()

      menu_items = [('Open in ROX-Filer', open_rox),
                  ('Verify integrity', verify)]

class UnusedImplementation(CachedImplementation):
      def append_to(self, model, iter):
            model.append(iter, [self.name, self.size, None, self.impl_path, self])

class KnownImplementation(CachedImplementation):
      def __init__(self, cached_iface, cache_dir, impl, impl_size):
            CachedImplementation.__init__(self, cache_dir, impl.id)
            self.cached_iface = cached_iface
            self.impl = impl
            self.size = impl_size
      
      def delete(self):
            CachedImplementation.delete(self)
            self.cached_iface.in_cache.remove(self)

      def append_to(self, model, iter):
            model.append(iter,
                  ['Version %s : %s' % (self.impl.get_version(), self.impl.id),
                   self.size, None,
                   None,
                   self])
      
      def __cmp__(self, other):
            if hasattr(other, 'impl'):
                  return self.impl.__cmp__(other.impl)
            return -1

class CacheExplorer(Dialog):
      def __init__(self):
            Dialog.__init__(self)
            self.set_title('Zero Install Cache')
            self.set_default_size(gtk.gdk.screen_width() / 2, gtk.gdk.screen_height() / 2)

            # Model
            self.model = gtk.TreeStore(str, int, str, str, object)
            self.tree_view = gtk.TreeView(self.model)

            # Tree view
            swin = gtk.ScrolledWindow()
            swin.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_ALWAYS)
            swin.set_shadow_type(gtk.SHADOW_IN)
            swin.add(self.tree_view)
            self.vbox.pack_start(swin, True, True, 0)
            self.tree_view.set_rules_hint(True)
            swin.show_all()

            column = gtk.TreeViewColumn('Item', gtk.CellRendererText(), text = ITEM)
            column.set_resizable(True)
            self.tree_view.append_column(column)

            cell = gtk.CellRendererText()
            cell.set_property('xalign', 1.0)
            column = gtk.TreeViewColumn('Size', cell, text = PRETTY_SIZE)
            self.tree_view.append_column(column)

            def button_press(tree_view, bev):
                  if bev.button != 3:
                        return False
                  pos = tree_view.get_path_at_pos(int(bev.x), int(bev.y))
                  if not pos:
                        return False
                  path, col, x, y = pos
                  obj = self.model[path][ITEM_OBJECT]
                  if obj and hasattr(obj, 'menu_items'):
                        popup_menu(bev, obj)
            self.tree_view.connect('button-press-event', button_press)

            # Tree tooltips
            def motion(tree_view, ev):
                  if ev.window is not tree_view.get_bin_window():
                        return False
                  pos = tree_view.get_path_at_pos(int(ev.x), int(ev.y))
                  if pos:
                        path = pos[0]
                        row = self.model[path]
                        tip = row[TOOLTIP]
                        if tip:
                              if tip != tips.item:
                                    tips.prime(tree_view, tip)
                        else:
                              tips.hide()
                  else:
                        tips.hide()

            self.tree_view.connect('motion-notify-event', motion)
            self.tree_view.connect('leave-notify-event', lambda tv, ev: tips.hide())

            # Responses

            self.add_button(gtk.STOCK_HELP, gtk.RESPONSE_HELP)
            self.add_button(gtk.STOCK_CLOSE, gtk.RESPONSE_OK)
            self.add_button(gtk.STOCK_DELETE, DELETE)
            self.set_default_response(gtk.RESPONSE_OK)

            selection = self.tree_view.get_selection()
            def selection_changed(selection):
                  any_selected = False
                  for x in get_selected_paths(self.tree_view):
                        obj = self.model[x][ITEM_OBJECT]
                        if obj is None or not obj.may_delete:
                              self.set_response_sensitive(DELETE, False)
                              return
                        any_selected = True
                  self.set_response_sensitive(DELETE, any_selected)
            selection.set_mode(gtk.SELECTION_MULTIPLE)
            selection.connect('changed', selection_changed)
            selection_changed(selection)

            def response(dialog, resp):
                  if resp == gtk.RESPONSE_OK:
                        self.destroy()
                  elif resp == gtk.RESPONSE_HELP:
                        cache_help.display()
                  elif resp == DELETE:
                        self.delete()
            self.connect('response', response)
      
      def delete(self):
            model = self.model
            paths = get_selected_paths(self.tree_view)
            paths.reverse()
            for path in paths:
                  item = model[path][ITEM_OBJECT]
                  assert item.delete
                  item.delete()
                  model.remove(model.get_iter(path))
            self.update_sizes()

      def populate_model(self):
            # Find cached implementations

            unowned = {}      # Impl ID -> Store
            duplicates = [] # TODO

            for s in iface_cache.stores.stores:
                  if os.path.isdir(s.dir):
                        for id in os.listdir(s.dir):
                              if id in unowned:
                                    duplicates.append(id)
                              unowned[id] = s

            ok_interfaces = []
            error_interfaces = []

            # Look through cached interfaces for implementation owners
            all = iface_cache.list_all_interfaces()
            all.sort()
            for uri in all:
                  iface_size = 0
                  try:
                        if uri.startswith('/'):
                              cached_iface = uri
                        else:
                              cached_iface = basedir.load_first_cache(namespaces.config_site,
                                          'interfaces', model.escape(uri))
                        user_overrides = basedir.load_first_config(namespaces.config_site,
                                          namespaces.config_prog,
                                          'user_overrides', model.escape(uri))

                        iface_size = size_if_exists(cached_iface) + size_if_exists(user_overrides)
                        iface = iface_cache.get_interface(uri)
                  except Exception, ex:
                        error_interfaces.append((uri, str(ex), iface_size))
                  else:
                        cached_iface = ValidInterface(iface, iface_size)
                        for impl in iface.implementations.values():
                              if impl.id.startswith('/') or impl.id.startswith('.'):
                                    cached_iface.in_cache.append(LocalImplementation(impl))
                              if impl.id in unowned:
                                    cached_dir = unowned[impl.id].dir
                                    impl_path = os.path.join(cached_dir, impl.id)
                                    impl_size = get_size(impl_path)
                                    cached_iface.in_cache.append(KnownImplementation(cached_iface, cached_dir, impl, impl_size))
                                    del unowned[impl.id]
                        cached_iface.in_cache.sort()
                        ok_interfaces.append(cached_iface)

            if error_interfaces:
                  iter = self.model.append(None, [_("Invalid interfaces (unreadable)"),
                                     0, None,
                                     _("These interfaces exist in the cache but cannot be "
                                       "read. You should probably delete them."),
                                       None])
                  for uri, ex, size in error_interfaces:
                        item = InvalidInterface(uri, ex, size)
                        item.append_to(self.model, iter)

            unowned_sizes = []
            local_dir = os.path.join(basedir.xdg_cache_home, '0install.net', 'implementations')
            for id in unowned:
                  if unowned[id].dir == local_dir:
                        impl = UnusedImplementation(local_dir, id)
                        unowned_sizes.append((impl.size, impl))
            if unowned_sizes:
                  iter = self.model.append(None, [_("Unowned implementations and temporary files"),
                                    0, None,
                                    _("These probably aren't needed any longer. You can "
                                      "delete them."), None])
                  unowned_sizes.sort()
                  unowned_sizes.reverse()
                  for size, item in unowned_sizes:
                        item.append_to(self.model, iter)

            if ok_interfaces:
                  iter = self.model.append(None,
                        [_("Interfaces"),
                         0, None,
                         _("Interfaces in the cache"),
                           None])
                  for item in ok_interfaces:
                        item.append_to(self.model, iter)
            self.update_sizes()
      
      def update_sizes(self):
            """Set PRETTY_SIZE to the total size, including all children."""
            m = self.model
            def update(itr):
                  total = m[itr][SELF_SIZE]
                  child = m.iter_children(itr)
                  while child:
                        total += update(child)
                        child = m.iter_next(child)
                  m[itr][PRETTY_SIZE] = pretty_size(total)
                  return total
            itr = m.get_iter_root()
            while itr:
                  update(itr)
                  itr = m.iter_next(itr)

cache_help = help_box.HelpBox("Cache Explorer Help",
('Overview', """
When you run a program using Zero Install, it downloads the program's 'interface' file, \
which gives information about which versions of the program are available. This interface \
file is stored in the cache to save downloading it next time you run the program.

When you have chosen which version (implementation) of the program you want to \
run, Zero Install downloads that version and stores it in the cache too. Zero Install lets \
you have many different versions of each program on your computer at once. This is useful, \
since it lets you use an old version if needed, and different programs may need to use \
different versions of libraries in some cases.

The cache viewer shows you all the interfaces and implementations in your cache. \
This is useful to find versions you don't need anymore, so that you can delete them and \
free up some disk space."""),

('Invalid interfaces', """
The cache viewer gets a list of all interfaces in your cache. However, some may not \
be valid; they are shown in the 'Invalid interfaces' section. It should be fine to \
delete these. An invalid interface may be caused by a local interface that no longer \
exists, by a failed attempt to download an interface (the name ends in '.new'), or \
by the interface file format changing since the interface was downloaded."""),

('Unowned implementations and temporary files', """
The cache viewer searches through all the interfaces to find out which implementations \
they use. If no interface uses an implementation, it is shown in the 'Unowned implementations' \
section.

Unowned implementations can result from old versions of a program no longer being listed \
in the interface file. Temporary files are created when unpacking an implementation after \
downloading it. If the archive is corrupted, the unpacked files may be left there. Unless \
you are currently unpacking new programs, it should be fine to delete everything in this \
section."""),

('Interfaces', """
All remaining interfaces are listed in this section. You may wish to delete old versions of \
certain programs. Deleting a program which you may later want to run will require it to be downloaded \
again. Deleting a version of a program which is currently running may cause it to crash, so be careful!
"""))

Generated by  Doxygen 1.6.0   Back to index