Rev 5259: (lifeless) Add a gio based transport using gio+ as a prefix to get at the in file:///home/pqm/archives/thelove/bzr/%2Btrunk/

Canonical.com Patch Queue Manager pqm at pqm.ubuntu.com
Thu May 27 02:58:47 BST 2010


At file:///home/pqm/archives/thelove/bzr/%2Btrunk/

------------------------------------------------------------
revno: 5259 [merge]
revision-id: pqm at pqm.ubuntu.com-20100527015844-ya4jahnwjx5y8ej2
parent: pqm at pqm.ubuntu.com-20100526124300-yclyk499ecg4jari
parent: snaggen at acc.umu.se-20100525091535-yduavr8ygqdvs8rw
committer: Canonical.com Patch Queue Manager <pqm at pqm.ubuntu.com>
branch nick: +trunk
timestamp: Thu 2010-05-27 02:58:44 +0100
message:
  (lifeless) Add a gio based transport using gio+ as a prefix to get at the
   gio logic. (Mattias Eriksson)
added:
  bzrlib/transport/gio_transport.py __init__.py-20100430125031-jb4f7q7mtyz55kz3-2
modified:
  bzrlib/transport/__init__.py   transport.py-20050711165921-4978aa7ce1285ad5
=== modified file 'bzrlib/transport/__init__.py'
--- a/bzrlib/transport/__init__.py	2010-05-04 06:22:51 +0000
+++ b/bzrlib/transport/__init__.py	2010-05-27 01:58:44 +0000
@@ -1743,6 +1743,9 @@
 register_transport_proto('aftp://', help="Access using active FTP.")
 register_lazy_transport('aftp://', 'bzrlib.transport.ftp', 'FtpTransport')
 
+register_transport_proto('gio+', help="Access using any GIO supported protocols.")
+register_lazy_transport('gio+', 'bzrlib.transport.gio_transport', 'GioTransport')
+
 try:
     import kerberos
     kerberos_available = True

=== added file 'bzrlib/transport/gio_transport.py'
--- a/bzrlib/transport/gio_transport.py	1970-01-01 00:00:00 +0000
+++ b/bzrlib/transport/gio_transport.py	2010-05-25 09:15:35 +0000
@@ -0,0 +1,582 @@
+# Copyright (C) 2010 Canonical Ltd.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+#
+# Author: Mattias Eriksson
+
+"""Implementation of Transport over gio.
+
+Written by Mattias Eriksson <snaggen at acc.umu.se> based on the ftp transport.
+
+It provides the gio+XXX:// protocols where XXX is any of the protocols
+supported by gio.
+"""
+from cStringIO import StringIO
+import getpass
+import os
+import random
+import socket
+import stat
+import urllib
+import time
+import sys
+import getpass
+import urlparse
+
+from bzrlib import (
+    config,
+    errors,
+    osutils,
+    urlutils,
+    debug,
+    ui,
+    )
+from bzrlib.trace import mutter, warning
+from bzrlib.transport import (
+    FileStream,
+    ConnectedTransport,
+    _file_streams,
+    Server,
+    )
+
+from bzrlib.tests.test_server import TestServer
+
+try:
+    import glib
+except ImportError, e:
+    raise errors.DependencyNotPresent('glib', e)
+try:
+    import gio
+except ImportError, e:
+    raise errors.DependencyNotPresent('gio', e)
+
+
+class GioLocalURLServer(TestServer):
+    """A pretend server for local transports, using file:// urls.
+
+    Of course no actual server is required to access the local filesystem, so
+    this just exists to tell the test code how to get to it.
+    """
+
+    def start_server(self):
+        pass
+
+    def get_url(self):
+        """See Transport.Server.get_url."""
+        return "gio+" + urlutils.local_path_to_url('')
+
+
+class GioFileStream(FileStream):
+    """A file stream object returned by open_write_stream.
+
+    This version uses GIO to perform writes.
+    """
+
+    def __init__(self, transport, relpath):
+        FileStream.__init__(self, transport, relpath)
+        self.gio_file = transport._get_GIO(relpath)
+        self.stream = self.gio_file.create()
+
+    def _close(self):
+        self.stream.close()
+
+    def write(self, bytes):
+        try:
+            #Using pump_string_file seems to make things crash
+            osutils.pumpfile(StringIO(bytes), self.stream)
+        except gio.Error, e:
+            #self.transport._translate_gio_error(e,self.relpath)
+            raise errors.BzrError(str(e))
+
+
+class GioStatResult(object):
+
+    def __init__(self, f):
+        info = f.query_info('standard::size,standard::type')
+        self.st_size = info.get_size()
+        type = info.get_file_type()
+        if (type == gio.FILE_TYPE_REGULAR):
+            self.st_mode = stat.S_IFREG
+        elif type == gio.FILE_TYPE_DIRECTORY:
+            self.st_mode = stat.S_IFDIR
+
+
+class GioTransport(ConnectedTransport):
+    """This is the transport agent for gio+XXX:// access."""
+
+    def __init__(self, base, _from_transport=None):
+        """Initialize the GIO transport and make sure the url is correct."""
+
+        if not base.startswith('gio+'):
+            raise ValueError(base)
+
+        (scheme, netloc, path, params, query, fragment) = \
+                urlparse.urlparse(base[len('gio+'):], allow_fragments=False)
+        if '@' in netloc:
+            user, netloc = netloc.rsplit('@', 1)
+        #Seems it is not possible to list supported backends for GIO
+        #so a hardcoded list it is then.
+        gio_backends = ['dav', 'file', 'ftp', 'obex', 'sftp', 'ssh', 'smb']
+        if scheme not in gio_backends:
+            raise errors.InvalidURL(base,
+                    extra="GIO support is only available for " + \
+                    ', '.join(gio_backends))
+
+        #Remove the username and password from the url we send to GIO
+        #by rebuilding the url again.
+        u = (scheme, netloc, path, '', '', '')
+        self.url = urlparse.urlunparse(u)
+
+        # And finally initialize super
+        super(GioTransport, self).__init__(base,
+            _from_transport=_from_transport)
+
+    def _relpath_to_url(self, relpath):
+        full_url = urlutils.join(self.url, relpath)
+        if isinstance(full_url, unicode):
+            raise errors.InvalidURL(full_url)
+        return full_url
+
+    def _get_GIO(self, relpath):
+        """Return the ftplib.GIO instance for this object."""
+        # Ensures that a connection is established
+        connection = self._get_connection()
+        if connection is None:
+            # First connection ever
+            connection, credentials = self._create_connection()
+            self._set_connection(connection, credentials)
+        fileurl = self._relpath_to_url(relpath)
+        file = gio.File(fileurl)
+        return file
+
+    def _auth_cb(self, op, message, default_user, default_domain, flags):
+        #really use bzrlib.auth get_password for this
+        #or possibly better gnome-keyring?
+        auth = config.AuthenticationConfig()
+        (scheme, urluser, urlpassword, host, port, urlpath) = \
+           urlutils.parse_url(self.url)
+        user = None
+        if (flags & gio.ASK_PASSWORD_NEED_USERNAME and
+                flags & gio.ASK_PASSWORD_NEED_DOMAIN):
+            prompt = scheme.upper() + ' %(host)s DOMAIN\username'
+            user_and_domain = auth.get_user(scheme, host,
+                    port=port, ask=True, prompt=prompt)
+            (domain, user) = user_and_domain.split('\\', 1)
+            op.set_username(user)
+            op.set_domain(domain)
+        elif flags & gio.ASK_PASSWORD_NEED_USERNAME:
+            user = auth.get_user(scheme, host,
+                    port=port, ask=True)
+            op.set_username(user)
+        elif flags & gio.ASK_PASSWORD_NEED_DOMAIN:
+            #Don't know how common this case is, but anyway
+            #a DOMAIN and a username prompt should be the
+            #same so I will missuse the ui_factory get_username
+            #a little bit here.
+            prompt = scheme.upper() + ' %(host)s DOMAIN'
+            domain = ui.ui_factory.get_username(prompt=prompt)
+            op.set_domain(domain)
+
+        if flags & gio.ASK_PASSWORD_NEED_PASSWORD:
+            if user is None:
+                user = op.get_username()
+            password = auth.get_password(scheme, host,
+                    user, port=port)
+            op.set_password(password)
+        op.reply(gio.MOUNT_OPERATION_HANDLED)
+
+    def _mount_done_cb(self, obj, res):
+        try:
+            obj.mount_enclosing_volume_finish(res)
+            self.loop.quit()
+        except gio.Error, e:
+            self.loop.quit()
+            raise errors.BzrError("Failed to mount the given location: " + str(e));
+
+    def _create_connection(self, credentials=None):
+        if credentials is None:
+            user, password = self._user, self._password
+        else:
+            user, password = credentials
+
+        try:
+            connection = gio.File(self.url)
+            mount = None
+            try:
+                mount = connection.find_enclosing_mount()
+            except gio.Error, e:
+                if (e.code == gio.ERROR_NOT_MOUNTED):
+                    self.loop = glib.MainLoop()
+                    ui.ui_factory.show_message('Mounting %s using GIO' % \
+                            self.url)
+                    op = gio.MountOperation()
+                    if user:
+                        op.set_username(user)
+                    if password:
+                        op.set_password(password)
+                    op.connect('ask-password', self._auth_cb)
+                    m = connection.mount_enclosing_volume(op,
+                            self._mount_done_cb)
+                    self.loop.run()
+        except gio.Error, e:
+            raise errors.TransportError(msg="Error setting up connection:"
+                                        " %s" % str(e), orig_error=e)
+        return connection, (user, password)
+
+    def _reconnect(self):
+        """Create a new connection with the previously used credentials"""
+        credentials = self._get_credentials()
+        connection, credentials = self._create_connection(credentials)
+        self._set_connection(connection, credentials)
+
+    def _remote_path(self, relpath):
+        relative = urlutils.unescape(relpath).encode('utf-8')
+        remote_path = self._combine_paths(self._path, relative)
+        return remote_path
+
+    def has(self, relpath):
+        """Does the target location exist?"""
+        try:
+            if 'gio' in debug.debug_flags:
+                mutter('GIO has check: %s' % relpath)
+            f = self._get_GIO(relpath)
+            st = GioStatResult(f)
+            if stat.S_ISREG(st.st_mode) or stat.S_ISDIR(st.st_mode):
+                return True
+            return False
+        except gio.Error, e:
+            if e.code == gio.ERROR_NOT_FOUND:
+                return False
+            else:
+                self._translate_gio_error(e, relpath)
+
+    def get(self, relpath, decode=False, retries=0):
+        """Get the file at the given relative path.
+
+        :param relpath: The relative path to the file
+        :param retries: Number of retries after temporary failures so far
+                        for this operation.
+
+        We're meant to return a file-like object which bzr will
+        then read from. For now we do this via the magic of StringIO
+        """
+        try:
+            if 'gio' in debug.debug_flags:
+                mutter("GIO get: %s" % relpath)
+            f = self._get_GIO(relpath)
+            fin = f.read()
+            buf = fin.read()
+            fin.close()
+            ret = StringIO(buf)
+            return ret
+        except gio.Error, e:
+            #If we get a not mounted here it might mean
+            #that a bad path has been entered (or that mount failed)
+            if (e.code == gio.ERROR_NOT_MOUNTED):
+                raise errors.PathError(relpath,
+                  extra='Failed to get file, make sure the path is correct. ' \
+                  + str(e))
+            else:
+                self._translate_gio_error(e, relpath)
+
+    def put_file(self, relpath, fp, mode=None):
+        """Copy the file-like object into the location.
+
+        :param relpath: Location to put the contents, relative to base.
+        :param fp:       File-like or string object.
+        """
+        if 'gio' in debug.debug_flags:
+            mutter("GIO put_file %s" % relpath)
+        tmppath = '%s.tmp.%.9f.%d.%d' % (relpath, time.time(),
+                    os.getpid(), random.randint(0, 0x7FFFFFFF))
+        f = None
+        fout = None
+        try:
+            f = self._get_GIO(tmppath)
+            fout = f.create()
+            closed = False
+            length = self._pump(fp, fout)
+            fout.close()
+            closed = True
+            self.stat(tmppath)
+            dest = self._get_GIO(relpath)
+            f.move(dest, flags=gio.FILE_COPY_OVERWRITE)
+            f = None
+            if mode is not None:
+                self._setmode(relpath, mode)
+            return length
+        except gio.Error, e:
+            self._translate_gio_error(e, relpath)
+        finally:
+            if not closed and fout is not None:
+                fout.close()
+            if f is not None and f.query_exists():
+                f.delete()
+
+    def mkdir(self, relpath, mode=None):
+        """Create a directory at the given path."""
+        try:
+            if 'gio' in debug.debug_flags:
+                mutter("GIO mkdir: %s" % relpath)
+            f = self._get_GIO(relpath)
+            f.make_directory()
+            self._setmode(relpath, mode)
+        except gio.Error, e:
+            self._translate_gio_error(e, relpath)
+
+    def open_write_stream(self, relpath, mode=None):
+        """See Transport.open_write_stream."""
+        if 'gio' in debug.debug_flags:
+            mutter("GIO open_write_stream %s" % relpath)
+        if mode is not None:
+            self._setmode(relpath, mode)
+        result = GioFileStream(self, relpath)
+        _file_streams[self.abspath(relpath)] = result
+        return result
+
+    def recommended_page_size(self):
+        """See Transport.recommended_page_size().
+
+        For FTP we suggest a large page size to reduce the overhead
+        introduced by latency.
+        """
+        if 'gio' in debug.debug_flags:
+            mutter("GIO recommended_page")
+        return 64 * 1024
+
+    def rmdir(self, relpath):
+        """Delete the directory at rel_path"""
+        try:
+            if 'gio' in debug.debug_flags:
+                mutter("GIO rmdir %s" % relpath)
+            st = self.stat(relpath)
+            if stat.S_ISDIR(st.st_mode):
+                f = self._get_GIO(relpath)
+                f.delete()
+            else:
+                raise errors.NotADirectory(relpath)
+        except gio.Error, e:
+            self._translate_gio_error(e, relpath)
+        except errors.NotADirectory, e:
+            #just pass it forward
+            raise e
+        except Exception, e:
+            mutter('failed to rmdir %s: %s' % (relpath, e))
+            raise errors.PathError(relpath)
+
+    def append_file(self, relpath, file, mode=None):
+        """Append the text in the file-like object into the final
+        location.
+        """
+        #GIO append_to seems not to append but to truncate
+        #Work around this.
+        if 'gio' in debug.debug_flags:
+            mutter("GIO append_file: %s" % relpath)
+        tmppath = '%s.tmp.%.9f.%d.%d' % (relpath, time.time(),
+                    os.getpid(), random.randint(0, 0x7FFFFFFF))
+        try:
+            result = 0
+            fo = self._get_GIO(tmppath)
+            fi = self._get_GIO(relpath)
+            fout = fo.create()
+            try:
+                info = GioStatResult(fi)
+                result = info.st_size
+                fin = fi.read()
+                self._pump(fin, fout)
+                fin.close()
+            #This separate except is to catch and ignore the
+            #gio.ERROR_NOT_FOUND for the already existing file.
+            #It is valid to open a non-existing file for append.
+            #This is caused by the broken gio append_to...
+            except gio.Error, e:
+                if e.code != gio.ERROR_NOT_FOUND:
+                    self._translate_gio_error(e, relpath)
+            length = self._pump(file, fout)
+            fout.close()
+            info = GioStatResult(fo)
+            if info.st_size != result + length:
+                raise errors.BzrError("Failed to append size after " \
+                      "(%d) is not original (%d) + written (%d) total (%d)" % \
+                      (info.st_size, result, length, result + length))
+            fo.move(fi, flags=gio.FILE_COPY_OVERWRITE)
+            return result
+        except gio.Error, e:
+            self._translate_gio_error(e, relpath)
+
+    def _setmode(self, relpath, mode):
+        """Set permissions on a path.
+
+        Only set permissions on Unix systems
+        """
+        if 'gio' in debug.debug_flags:
+            mutter("GIO _setmode %s" % relpath)
+        if mode:
+            try:
+                f = self._get_GIO(relpath)
+                f.set_attribute_uint32(gio.FILE_ATTRIBUTE_UNIX_MODE, mode)
+            except gio.Error, e:
+                if e.code == gio.ERROR_NOT_SUPPORTED:
+                    # Command probably not available on this server
+                    mutter("GIO Could not set permissions to %s on %s. %s",
+                        oct(mode), self._remote_path(relpath), str(e))
+                else:
+                    self._translate_gio_error(e, relpath)
+
+    def rename(self, rel_from, rel_to):
+        """Rename without special overwriting"""
+        try:
+            if 'gio' in debug.debug_flags:
+                mutter("GIO move (rename): %s => %s", rel_from, rel_to)
+            f = self._get_GIO(rel_from)
+            t = self._get_GIO(rel_to)
+            f.move(t)
+        except gio.Error, e:
+            self._translate_gio_error(e, rel_from)
+
+    def move(self, rel_from, rel_to):
+        """Move the item at rel_from to the location at rel_to"""
+        try:
+            if 'gio' in debug.debug_flags:
+                mutter("GIO move: %s => %s", rel_from, rel_to)
+            f = self._get_GIO(rel_from)
+            t = self._get_GIO(rel_to)
+            f.move(t, flags=gio.FILE_COPY_OVERWRITE)
+        except gio.Error, e:
+            self._translate_gio_error(e, relfrom)
+
+    def delete(self, relpath):
+        """Delete the item at relpath"""
+        try:
+            if 'gio' in debug.debug_flags:
+                mutter("GIO delete: %s", relpath)
+            f = self._get_GIO(relpath)
+            f.delete()
+        except gio.Error, e:
+            self._translate_gio_error(e, relpath)
+
+    def external_url(self):
+        """See bzrlib.transport.Transport.external_url."""
+        if 'gio' in debug.debug_flags:
+            mutter("GIO external_url", self.base)
+        # GIO external url
+        return self.base
+
+    def listable(self):
+        """See Transport.listable."""
+        if 'gio' in debug.debug_flags:
+            mutter("GIO listable")
+        return True
+
+    def list_dir(self, relpath):
+        """See Transport.list_dir."""
+        if 'gio' in debug.debug_flags:
+            mutter("GIO list_dir")
+        try:
+            entries = []
+            f = self._get_GIO(relpath)
+            children = f.enumerate_children(gio.FILE_ATTRIBUTE_STANDARD_NAME)
+            for child in children:
+                entries.append(urlutils.escape(child.get_name()))
+            return entries
+        except gio.Error, e:
+            self._translate_gio_error(e, relpath)
+
+    def iter_files_recursive(self):
+        """See Transport.iter_files_recursive.
+
+        This is cargo-culted from the SFTP transport"""
+        if 'gio' in debug.debug_flags:
+            mutter("GIO iter_files_recursive")
+        queue = list(self.list_dir("."))
+        while queue:
+            relpath = queue.pop(0)
+            st = self.stat(relpath)
+            if stat.S_ISDIR(st.st_mode):
+                for i, basename in enumerate(self.list_dir(relpath)):
+                    queue.insert(i, relpath + "/" + basename)
+            else:
+                yield relpath
+
+    def stat(self, relpath):
+        """Return the stat information for a file."""
+        try:
+            if 'gio' in debug.debug_flags:
+                mutter("GIO stat: %s", relpath)
+            f = self._get_GIO(relpath)
+            return GioStatResult(f)
+        except gio.Error, e:
+            self._translate_gio_error(e, relpath, extra='error w/ stat')
+
+    def lock_read(self, relpath):
+        """Lock the given file for shared (read) access.
+        :return: A lock object, which should be passed to Transport.unlock()
+        """
+        if 'gio' in debug.debug_flags:
+            mutter("GIO lock_read", relpath)
+
+        class BogusLock(object):
+            # The old RemoteBranch ignore lock for reading, so we will
+            # continue that tradition and return a bogus lock object.
+
+            def __init__(self, path):
+                self.path = path
+
+            def unlock(self):
+                pass
+
+        return BogusLock(relpath)
+
+    def lock_write(self, relpath):
+        """Lock the given file for exclusive (write) access.
+        WARNING: many transports do not support this, so trying avoid using it
+
+        :return: A lock object, whichshould be passed to Transport.unlock()
+        """
+        if 'gio' in debug.debug_flags:
+            mutter("GIO lock_write", relpath)
+        return self.lock_read(relpath)
+
+    def _translate_gio_error(self, err, path, extra=None):
+        if 'gio' in debug.debug_flags:
+            mutter("GIO Error: %s %s" % (str(err), path))
+        if extra is None:
+            extra = str(err)
+        if err.code == gio.ERROR_NOT_FOUND:
+            raise errors.NoSuchFile(path, extra=extra)
+        elif err.code == gio.ERROR_EXISTS:
+            raise errors.FileExists(path, extra=extra)
+        elif err.code == gio.ERROR_NOT_DIRECTORY:
+            raise errors.NotADirectory(path, extra=extra)
+        elif err.code == gio.ERROR_NOT_EMPTY:
+            raise errors.DirectoryNotEmpty(path, extra=extra)
+        elif err.code == gio.ERROR_BUSY:
+            raise errors.ResourceBusy(path, extra=extra)
+        elif err.code == gio.ERROR_PERMISSION_DENIED:
+            raise errors.PermissionDenied(path, extra=extra)
+        elif err.code == gio.ERROR_HOST_NOT_FOUND:
+            raise errors.PathError(path, extra=extra)
+        elif err.code == gio.ERROR_IS_DIRECTORY:
+            raise errors.PathError(path, extra=extra)
+        else:
+            mutter('unable to understand error for path: %s: %s', path, err)
+            raise errors.PathError(path,
+                    extra="Unhandled gio error: " + str(err))
+
+
+def get_test_permutations():
+    """Return the permutations to be used in testing."""
+    from bzrlib.tests import test_server
+    return [(GioTransport, GioLocalURLServer)]




More information about the bazaar-commits mailing list