Rev 2504: Add a new ConnectedTransport class refactored from [s]ftp and http. in file:///v/home/vila/src/experimental/reuse.transports/

Vincent Ladeuil v.ladeuil+lp at free.fr
Thu May 31 19:01:13 BST 2007


At file:///v/home/vila/src/experimental/reuse.transports/

------------------------------------------------------------
revno: 2504
revision-id: v.ladeuil+lp at free.fr-20070531180111-xef34xmn8l3hjyz0
parent: v.ladeuil+lp at free.fr-20070531175656-uwy1n9l8x3im9evb
committer: Vincent Ladeuil <v.ladeuil+lp at free.fr>
branch nick: reuse.transports
timestamp: Thu 2007-05-31 20:01:11 +0200
message:
  Add a new ConnectedTransport class refactored from [s]ftp and http.
  
  * bzrlib/transport/__init__.py:
  (split_url): Deprecated.
  (ConnectedTransport): New class.
  
  * bzrlib/tests/test_transport.py:
  (TestConnectedTransport): New class.
modified:
  bzrlib/tests/test_transport.py testtransport.py-20050718175618-e5cdb99f4555ddce
  bzrlib/transport/__init__.py   transport.py-20050711165921-4978aa7ce1285ad5
-------------- next part --------------
=== modified file 'bzrlib/tests/test_transport.py'
--- a/bzrlib/tests/test_transport.py	2007-05-29 13:17:41 +0000
+++ b/bzrlib/tests/test_transport.py	2007-05-31 18:01:11 +0000
@@ -36,6 +36,7 @@
                            )
 from bzrlib.tests import TestCase, TestCaseInTempDir
 from bzrlib.transport import (_CoalescedOffset,
+                              ConnectedTransport,
                               _get_protocol_handlers,
                               _set_protocol_handlers,
                               _get_transport_modules,
@@ -593,8 +594,58 @@
         t = t.clone('..')
         self.assertEquals(t.base, 'file://HOST/')
 
+class TestConnectedTransport(TestCase):
+    """Tests for connected to remote server transports"""
+
+    def test_parse_url(self):
+        t = ConnectedTransport('sftp://simple.example.com/home/source')
+        self.assertEquals(t._host, 'simple.example.com')
+        self.assertEquals(t._port, None)
+        self.assertEquals(t._path, '/home/source/')
+        self.failUnless(t._user is None)
+        self.failUnless(t._password is None)
+
+        self.assertEquals(t.base, 'sftp://simple.example.com/home/source/')
+
+    def test_parse_quoted_url(self):
+        t = ConnectedTransport('http://ro%62ey:h%40t@ex%41mple.com:2222/path')
+        self.assertEquals(t._host, 'exAmple.com')
+        self.assertEquals(t._port, 2222)
+        self.assertEquals(t._user, 'robey')
+        self.assertEquals(t._password, 'h at t')
+        self.assertEquals(t._path, '/path/')
+
+        # Base should not keep track of the password
+        self.assertEquals(t.base, 'http://robey@exAmple.com:2222/path/')
+
+    def test_parse_invalid_url(self):
+        self.assertRaises(errors.InvalidURL,
+                          ConnectedTransport,
+                          'sftp://lily.org:~janneke/public/bzr/gub')
+
+    def test_relpath(self):
+        t = ConnectedTransport('sftp://user@host.com/abs/path')
+
+        self.assertEquals(t.relpath('sftp://user@host.com/abs/path/sub'), 'sub')
+        self.assertRaises(errors.PathNotChild, t.relpath,
+                          'http://user@host.com/abs/path/sub')
+        self.assertRaises(errors.PathNotChild, t.relpath,
+                          'sftp://user2@host.com/abs/path/sub')
+        self.assertRaises(errors.PathNotChild, t.relpath,
+                          'sftp://user@otherhost.com/abs/path/sub')
+        self.assertRaises(errors.PathNotChild, t.relpath,
+                          'sftp://user@host.com:33/abs/path/sub')
+        # Make sure it works when we don't supply a username
+        t = ConnectedTransport('sftp://host.com/abs/path')
+        self.assertEquals(t.relpath('sftp://host.com/abs/path/sub'), 'sub')
+
+        # Make sure it works when parts of the path will be url encoded
+        t = ConnectedTransport('sftp://host.com/dev/%path')
+        self.assertEquals(t.relpath('sftp://host.com/dev/%path/sub'), 'sub')
+
 
 class TestReusedTransports(TestCase):
+    """Tests for transport reuse"""
 
     def test_reuse_same_transport(self):
         t = get_transport('http://foo/')

=== modified file 'bzrlib/transport/__init__.py'
--- a/bzrlib/transport/__init__.py	2007-05-30 07:15:16 +0000
+++ b/bzrlib/transport/__init__.py	2007-05-31 18:01:11 +0000
@@ -57,6 +57,7 @@
         DEPRECATED_PARAMETER,
         zero_eight,
         zero_eleven,
+        zero_seventeen,
         )
 from bzrlib.trace import (
     note,
@@ -169,6 +170,7 @@
 
 
 
+ at deprecated_function(zero_seventeen)
 def split_url(url):
     # TODO: jam 20060606 urls should only be ascii, or they should raise InvalidURL
     if isinstance(url, unicode):
@@ -377,13 +379,8 @@
         :param relpath: relative url string for relative part of remote path.
         :return: urlencoded string for final path.
         """
-        # FIXME: share the common code across more transports; variants of
-        # this likely occur in http and sftp too.
-        #
-        # TODO: Also need to consider handling of ~, which might vary between
-        # transports?
         if not isinstance(relpath, str):
-            raise errors.InvalidURL("not a valid url: %r" % relpath)
+            raise errors.InvalidURL(relpath)
         if relpath.startswith('/'):
             base_parts = []
         else:
@@ -1032,6 +1029,135 @@
         return False
 
 
+class ConnectedTransport(Transport):
+    """A transport connected to a remote server.
+
+    Base class for transports that connect to a remote server
+    with optional user and password. Cloning preserves existing
+    connection and credentials.
+    """
+
+    def __init__(self, base, from_transport=None):
+        if base[-1] != '/':
+            base += '/'
+        if from_transport is not None:
+            # Copy the passowrd as it does not appear in base
+            self._password = from_transport._password
+        (self._scheme,
+         self._user, self._password,
+         self._host, self._port,
+         self._path) = self._split_url(base)
+        # Rebuild base taken any special handling into account
+        base = self._unsplit_url(self._scheme,
+                                 self._user, self._password,
+                                 self._host, self._port,
+                                 self._urlencode_abspath(self._path))
+        super(ConnectedTransport, self).__init__(base)
+        if from_transport is not None:
+            connection = from_transport.get_connection()
+        else:
+            connection = None
+        self.set_connection(connection)
+
+    def _split_url(self, url):
+        if isinstance(url, unicode):
+            import pdb; pdb.set_trace()
+            raise errors.InvalidURL('should be ascii:\n%r' % url)
+        url = url.encode('utf-8')
+        (scheme, netloc, path, params,
+         query, fragment) = urlparse.urlparse(url, allow_fragments=False)
+        username = password = host = port = None
+        if '@' in netloc:
+            username, host = netloc.split('@', 1)
+            if ':' in username:
+                username, password = username.split(':', 1)
+                password = urllib.unquote(password)
+            username = urllib.unquote(username)
+        else:
+            host = netloc
+
+        if ':' in host:
+            host, port = host.rsplit(':', 1)
+            try:
+                port = int(port)
+            except ValueError:
+                raise errors.InvalidURL('invalid port number %s in url:\n%s' %
+                                        (port, url))
+        host = urllib.unquote(host)
+        path = self._urldecode_abspath(path)
+
+        return (scheme, username, password, host, port, path)
+
+    def _unsplit_url(self, scheme, user, password, host, port, path):
+        netloc = urllib.quote(host)
+        if user is not None:
+            # Note that we don't put the password back even if we
+            # have one so that it doesn't get accidentally
+            # exposed.
+            netloc = '%s@%s' % (urllib.quote(user), netloc)
+        if port is not None:
+            netloc = '%s:%d' % (netloc, port)
+        return urlparse.urlunparse((scheme, netloc, path, None, None, None))
+
+    def _urlencode_abspath(self, abspath):
+        return urllib.quote(abspath)
+
+    def _urldecode_abspath(self, abspath):
+        return urllib.unquote(abspath)
+
+    def relpath(self, abspath):
+        scheme, user, password, host, port, path = self._split_url(abspath)
+        error = []
+        if (scheme != self._scheme):
+            error.append('scheme mismatch')
+        if (user != self._user):
+            error.append('username mismatch')
+        if (host != self._host):
+            error.append('host mismatch')
+        if (port != self._port):
+            error.append('port mismatch')
+        if not (path == self._path[:-1] or path.startswith(self._path)):
+            error.append('path mismatch')
+        if error:
+            extra = ', '.join(error)
+            raise errors.PathNotChild(abspath, self.base, extra=extra)
+        pl = len(self._path)
+        return path[pl:].strip('/')
+
+    def abspath(self, relpath):
+        """Return the full url to the given relative path.
+        
+        :param relpath: the relative path urlencoded
+        """
+        path = self._remote_path(relpath)
+        return self._unsplit_url(self._scheme, self._user, self._password,
+                                 self._host, self._port,
+                                 self._urlencode_abspath(path))
+
+    def _remote_path(self, relpath):
+        """Return the absolute path part of the url to the given relative path.
+        
+        :param relpath: is a urlencoded string.
+        """
+        relative = urlutils.unescape(relpath).encode('utf-8')
+        remote_path = self._combine_paths(self._path, relative)
+        return remote_path
+
+    def get_connection(self):
+        """Returns the transport specific connection object"""
+        return self._connection
+
+    def set_connection(self, connection):
+        """Set the transport specific connection object.
+
+        Note: daughter classes should ensure that the connection
+        is still shared if the connection is reset during the
+        transport lifetime (using a list containing the single
+        connection can help avoid aliasing bugs).
+        """
+        self._connection = connection
+
+
 # jam 20060426 For compatibility we copy the functions here
 # TODO: The should be marked as deprecated
 urlescape = urlutils.escape
@@ -1147,10 +1273,6 @@
     return None, last_err
 
 
-class ConnectedTransport(Transport):
-    """A transport connected to a remote server"""
-
-
 class Server(object):
     """A Transport Server.
     



More information about the bazaar-commits mailing list