~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/transport/__init__.py

  • Committer: Canonical.com Patch Queue Manager
  • Date: 2007-07-22 18:09:04 UTC
  • mfrom: (2485.8.63 bzr.connection.sharing)
  • Revision ID: pqm@pqm.ubuntu.com-20070722180904-wy7y7oyi32wbghgf
Transport connection sharing

Show diffs side-by-side

added added

removed removed

Lines of Context:
56
56
        DEPRECATED_PARAMETER,
57
57
        zero_eight,
58
58
        zero_eleven,
 
59
        zero_nineteen,
59
60
        )
60
61
from bzrlib.trace import (
61
62
    note,
108
109
    2) register the protocol provider with the function
109
110
    register_transport_provider( ) ( and the "lazy" variant )
110
111
 
111
 
    This in needed because:
 
112
    This is needed because:
112
113
    a) a single provider can support multple protcol ( like the ftp
113
 
    privider which supports both the ftp:// and the aftp:// protocols )
 
114
    provider which supports both the ftp:// and the aftp:// protocols )
114
115
    b) a single protocol can have multiple providers ( like the http://
115
 
    protocol which is supported by both the urllib and pycurl privider )
 
116
    protocol which is supported by both the urllib and pycurl provider )
116
117
    """
117
118
 
118
119
    def register_transport_provider(self, key, obj):
155
156
        urlparse.uses_netloc.append(protocol)
156
157
 
157
158
 
 
159
def _unregister_urlparse_netloc_protocol(protocol):
 
160
    """Remove protocol from urlparse netloc parsing.
 
161
 
 
162
    Except for tests, you should never use that function. Using it with 'http',
 
163
    for example, will break all http transports.
 
164
    """
 
165
    if protocol in urlparse.uses_netloc:
 
166
        urlparse.uses_netloc.remove(protocol)
 
167
 
 
168
 
158
169
def unregister_transport(scheme, factory):
159
170
    """Unregister a transport."""
160
171
    l = transport_list_registry.get(scheme)
168
179
 
169
180
 
170
181
 
 
182
@deprecated_function(zero_nineteen)
171
183
def split_url(url):
172
184
    # TODO: jam 20060606 urls should only be ascii, or they should raise InvalidURL
173
185
    if isinstance(url, unicode):
424
436
        :param relpath: relative url string for relative part of remote path.
425
437
        :return: urlencoded string for final path.
426
438
        """
427
 
        # FIXME: share the common code across more transports; variants of
428
 
        # this likely occur in http and sftp too.
429
 
        #
430
 
        # TODO: Also need to consider handling of ~, which might vary between
431
 
        # transports?
432
439
        if not isinstance(relpath, str):
433
 
            raise errors.InvalidURL("not a valid url: %r" % relpath)
 
440
            raise errors.InvalidURL(relpath)
434
441
        if relpath.startswith('/'):
435
442
            base_parts = []
436
443
        else:
563
570
        """
564
571
        raise errors.NoSmartMedium(self)
565
572
 
 
573
    def get_shared_medium(self):
 
574
        """Return a smart client shared medium for this transport if possible.
 
575
 
 
576
        A smart medium doesn't imply the presence of a smart server: it implies
 
577
        that the smart protocol can be tunnelled via this transport.
 
578
 
 
579
        :raises NoSmartMedium: if no smart server medium is available.
 
580
        """
 
581
        raise errors.NoSmartMedium(self)
 
582
 
566
583
    def readv(self, relpath, offsets):
567
584
        """Get parts of the file at the given relative path.
568
585
 
1089
1106
        # several questions about the transport.
1090
1107
        return False
1091
1108
 
1092
 
 
1093
 
# jam 20060426 For compatibility we copy the functions here
1094
 
# TODO: The should be marked as deprecated
1095
 
urlescape = urlutils.escape
1096
 
urlunescape = urlutils.unescape
1097
 
_urlRE = re.compile(r'^(?P<proto>[^:/\\]+)://(?P<path>.*)$')
1098
 
 
1099
 
 
1100
 
def get_transport(base):
 
1109
    def _reuse_for(self, other_base):
 
1110
        # This is really needed for ConnectedTransport only, but it's easier to
 
1111
        # have Transport refuses to be reused than testing that the reuse
 
1112
        # should be asked to ConnectedTransport only.
 
1113
        return None
 
1114
 
 
1115
 
 
1116
class _SharedConnection(object):
 
1117
    """A connection shared between several transports."""
 
1118
 
 
1119
    def __init__(self, connection=None, credentials=None):
 
1120
        """Constructor.
 
1121
 
 
1122
        :param connection: An opaque object specific to each transport.
 
1123
 
 
1124
        :param credentials: An opaque object containing the credentials used to
 
1125
            create the connection.
 
1126
        """
 
1127
        self.connection = connection
 
1128
        self.credentials = credentials
 
1129
 
 
1130
 
 
1131
class ConnectedTransport(Transport):
 
1132
    """A transport connected to a remote server.
 
1133
 
 
1134
    This class provide the basis to implement transports that need to connect
 
1135
    to a remote server.
 
1136
 
 
1137
    Host and credentials are available as private attributes, cloning preserves
 
1138
    them and share the underlying, protocol specific, connection.
 
1139
    """
 
1140
 
 
1141
    def __init__(self, base, _from_transport=None):
 
1142
        """Constructor.
 
1143
 
 
1144
        The caller should ensure that _from_transport points at the same host
 
1145
        as the new base.
 
1146
 
 
1147
        :param base: transport root URL
 
1148
 
 
1149
        :param _from_transport: optional transport to build from. The built
 
1150
            transport will share the connection with this transport.
 
1151
        """
 
1152
        if not base.endswith('/'):
 
1153
            base += '/'
 
1154
        (self._scheme,
 
1155
         self._user, self._password,
 
1156
         self._host, self._port,
 
1157
         self._path) = self._split_url(base)
 
1158
        if _from_transport is not None:
 
1159
            # Copy the password as it does not appear in base and will be lost
 
1160
            # otherwise. It can appear in the _split_url above if the user
 
1161
            # provided it on the command line. Otherwise, daughter classes will
 
1162
            # prompt the user for one when appropriate.
 
1163
            self._password = _from_transport._password
 
1164
 
 
1165
        base = self._unsplit_url(self._scheme,
 
1166
                                 self._user, self._password,
 
1167
                                 self._host, self._port,
 
1168
                                 self._path)
 
1169
 
 
1170
        super(ConnectedTransport, self).__init__(base)
 
1171
        if _from_transport is None:
 
1172
            self._shared_connection = _SharedConnection()
 
1173
        else:
 
1174
            self._shared_connection = _from_transport._shared_connection
 
1175
 
 
1176
    def clone(self, offset=None):
 
1177
        """Return a new transport with root at self.base + offset
 
1178
 
 
1179
        We leave the daughter classes take advantage of the hint
 
1180
        that it's a cloning not a raw creation.
 
1181
        """
 
1182
        if offset is None:
 
1183
            return self.__class__(self.base, _from_transport=self)
 
1184
        else:
 
1185
            return self.__class__(self.abspath(offset), _from_transport=self)
 
1186
 
 
1187
    @staticmethod
 
1188
    def _split_url(url):
 
1189
        """
 
1190
        Extract the server address, the credentials and the path from the url.
 
1191
 
 
1192
        user, password, host and path should be quoted if they contain reserved
 
1193
        chars.
 
1194
 
 
1195
        :param url: an quoted url
 
1196
 
 
1197
        :return: (scheme, user, password, host, port, path) tuple, all fields
 
1198
            are unquoted.
 
1199
        """
 
1200
        if isinstance(url, unicode):
 
1201
            raise errors.InvalidURL('should be ascii:\n%r' % url)
 
1202
        url = url.encode('utf-8')
 
1203
        (scheme, netloc, path, params,
 
1204
         query, fragment) = urlparse.urlparse(url, allow_fragments=False)
 
1205
        user = password = host = port = None
 
1206
        if '@' in netloc:
 
1207
            user, host = netloc.split('@', 1)
 
1208
            if ':' in user:
 
1209
                user, password = user.split(':', 1)
 
1210
                password = urllib.unquote(password)
 
1211
            user = urllib.unquote(user)
 
1212
        else:
 
1213
            host = netloc
 
1214
 
 
1215
        if ':' in host:
 
1216
            host, port = host.rsplit(':', 1)
 
1217
            try:
 
1218
                port = int(port)
 
1219
            except ValueError:
 
1220
                raise errors.InvalidURL('invalid port number %s in url:\n%s' %
 
1221
                                        (port, url))
 
1222
        host = urllib.unquote(host)
 
1223
        path = urllib.unquote(path)
 
1224
 
 
1225
        return (scheme, user, password, host, port, path)
 
1226
 
 
1227
    @staticmethod
 
1228
    def _unsplit_url(scheme, user, password, host, port, path):
 
1229
        """
 
1230
        Build the full URL for the given already URL encoded path.
 
1231
 
 
1232
        user, password, host and path will be quoted if they contain reserved
 
1233
        chars.
 
1234
 
 
1235
        :param scheme: protocol
 
1236
 
 
1237
        :param user: login
 
1238
 
 
1239
        :param password: associated password
 
1240
 
 
1241
        :param host: the server address
 
1242
 
 
1243
        :param port: the associated port
 
1244
 
 
1245
        :param path: the absolute path on the server
 
1246
 
 
1247
        :return: The corresponding URL.
 
1248
        """
 
1249
        netloc = urllib.quote(host)
 
1250
        if user is not None:
 
1251
            # Note that we don't put the password back even if we
 
1252
            # have one so that it doesn't get accidentally
 
1253
            # exposed.
 
1254
            netloc = '%s@%s' % (urllib.quote(user), netloc)
 
1255
        if port is not None:
 
1256
            netloc = '%s:%d' % (netloc, port)
 
1257
        path = urllib.quote(path)
 
1258
        return urlparse.urlunparse((scheme, netloc, path, None, None, None))
 
1259
 
 
1260
    def relpath(self, abspath):
 
1261
        """Return the local path portion from a given absolute path"""
 
1262
        scheme, user, password, host, port, path = self._split_url(abspath)
 
1263
        error = []
 
1264
        if (scheme != self._scheme):
 
1265
            error.append('scheme mismatch')
 
1266
        if (user != self._user):
 
1267
            error.append('user name mismatch')
 
1268
        if (host != self._host):
 
1269
            error.append('host mismatch')
 
1270
        if (port != self._port):
 
1271
            error.append('port mismatch')
 
1272
        if not (path == self._path[:-1] or path.startswith(self._path)):
 
1273
            error.append('path mismatch')
 
1274
        if error:
 
1275
            extra = ', '.join(error)
 
1276
            raise errors.PathNotChild(abspath, self.base, extra=extra)
 
1277
        pl = len(self._path)
 
1278
        return path[pl:].strip('/')
 
1279
 
 
1280
    def abspath(self, relpath):
 
1281
        """Return the full url to the given relative path.
 
1282
        
 
1283
        :param relpath: the relative path urlencoded
 
1284
 
 
1285
        :returns: the Unicode version of the absolute path for relpath.
 
1286
        """
 
1287
        relative = urlutils.unescape(relpath).encode('utf-8')
 
1288
        path = self._combine_paths(self._path, relative)
 
1289
        return self._unsplit_url(self._scheme, self._user, self._password,
 
1290
                                 self._host, self._port,
 
1291
                                 path)
 
1292
 
 
1293
    def _remote_path(self, relpath):
 
1294
        """Return the absolute path part of the url to the given relative path.
 
1295
 
 
1296
        This is the path that the remote server expect to receive in the
 
1297
        requests, daughter classes should redefine this method if needed and
 
1298
        use the result to build their requests.
 
1299
 
 
1300
        :param relpath: the path relative to the transport base urlencoded.
 
1301
 
 
1302
        :return: the absolute Unicode path on the server,
 
1303
        """
 
1304
        relative = urlutils.unescape(relpath).encode('utf-8')
 
1305
        remote_path = self._combine_paths(self._path, relative)
 
1306
        return remote_path
 
1307
 
 
1308
    def _get_shared_connection(self):
 
1309
        """Get the object shared amongst cloned transports.
 
1310
 
 
1311
        This should be used only by classes that needs to extend the sharing
 
1312
        with other objects than tramsports.
 
1313
 
 
1314
        Use _get_connection to get the connection itself.
 
1315
        """
 
1316
        return self._shared_connection
 
1317
 
 
1318
    def _set_connection(self, connection, credentials=None):
 
1319
        """Record a newly created connection with its associated credentials.
 
1320
 
 
1321
        Note: To ensure that connection is still shared after a temporary
 
1322
        failure and a new one needs to be created, daughter classes should
 
1323
        always call this method to set the connection and do so each time a new
 
1324
        connection is created.
 
1325
 
 
1326
        :param connection: An opaque object representing the connection used by
 
1327
            the daughter class.
 
1328
 
 
1329
        :param credentials: An opaque object representing the credentials
 
1330
            needed to create the connection.
 
1331
        """
 
1332
        self._shared_connection.connection = connection
 
1333
        self._shared_connection.credentials = credentials
 
1334
 
 
1335
    def _get_connection(self):
 
1336
        """Returns the transport specific connection object."""
 
1337
        return self._shared_connection.connection
 
1338
 
 
1339
    def _get_credentials(self):
 
1340
        """Returns the credentials used to establish the connection."""
 
1341
        return self._shared_connection.credentials
 
1342
 
 
1343
    def _update_credentials(self, credentials):
 
1344
        """Update the credentials of the current connection.
 
1345
 
 
1346
        Some protocols can renegociate the credentials within a connection,
 
1347
        this method allows daughter classes to share updated credentials.
 
1348
        
 
1349
        :param credentials: the updated credentials.
 
1350
        """
 
1351
        # We don't want to call _set_connection here as we are only updating
 
1352
        # the credentials not creating a new connection.
 
1353
        self._shared_connection.credentials = credentials
 
1354
 
 
1355
    def _reuse_for(self, other_base):
 
1356
        """Returns a transport sharing the same connection if possible.
 
1357
 
 
1358
        Note: we share the connection if the expected credentials are the
 
1359
        same: (host, port, user). Some protocols may disagree and redefine the
 
1360
        criteria in daughter classes.
 
1361
 
 
1362
        Note: we don't compare the passwords here because other_base may have
 
1363
        been obtained from an existing transport.base which do not mention the
 
1364
        password.
 
1365
 
 
1366
        :param other_base: the URL we want to share the connection with.
 
1367
 
 
1368
        :return: A new transport or None if the connection cannot be shared.
 
1369
        """
 
1370
        (scheme, user, password, host, port, path) = self._split_url(other_base)
 
1371
        transport = None
 
1372
        # Don't compare passwords, they may be absent from other_base or from
 
1373
        # self and they don't carry more information than user anyway.
 
1374
        if (scheme == self._scheme
 
1375
            and user == self._user
 
1376
            and host == self._host
 
1377
            and port == self._port):
 
1378
            if not path.endswith('/'):
 
1379
                # This normally occurs at __init__ time, but it's easier to do
 
1380
                # it now to avoid creating two transports for the same base.
 
1381
                path += '/'
 
1382
            if self._path  == path:
 
1383
                # shortcut, it's really the same transport
 
1384
                return self
 
1385
            # We don't call clone here because the intent is different: we
 
1386
            # build a new transport on a different base (which may be totally
 
1387
            # unrelated) but we share the connection.
 
1388
            transport = self.__class__(other_base, _from_transport=self)
 
1389
        return transport
 
1390
 
 
1391
 
 
1392
@deprecated_function(zero_nineteen)
 
1393
def urlescape(relpath):
 
1394
    urlutils.escape(relpath)
 
1395
@deprecated_function(zero_nineteen)
 
1396
def urlunescape(url):
 
1397
    urlutils.unescape(url)
 
1398
 
 
1399
# We try to recognize an url lazily (ignoring user, password, etc)
 
1400
_urlRE = re.compile(r'^(?P<proto>[^:/\\]+)://(?P<rest>.*)$')
 
1401
 
 
1402
def get_transport(base, possible_transports=None):
1101
1403
    """Open a transport to access a URL or directory.
1102
1404
 
1103
 
    base is either a URL or a directory name.  
 
1405
    :param base: either a URL or a directory name.
 
1406
 
 
1407
    :param transports: optional reusable transports list. If not None, created
 
1408
        transports will be added to the list.
 
1409
 
 
1410
    :return: A new transport optionally sharing its connection with one of
 
1411
        possible_transports.
1104
1412
    """
1105
 
 
1106
1413
    if base is None:
1107
1414
        base = '.'
1108
1415
    last_err = None
1112
1419
        if m:
1113
1420
            # This looks like a URL, but we weren't able to 
1114
1421
            # instantiate it as such raise an appropriate error
 
1422
            # FIXME: we have a 'error_str' unused and we use last_err below
1115
1423
            raise errors.UnsupportedProtocol(base, last_err)
1116
1424
        # This doesn't look like a protocol, consider it a local path
1117
1425
        new_base = urlutils.local_path_to_url(base)
1125
1433
        # Only local paths can be Unicode
1126
1434
        base = convert_path_to_url(base,
1127
1435
            'URLs must be properly escaped (protocol: %s)')
1128
 
    
 
1436
 
 
1437
    transport = None
 
1438
    if possible_transports:
 
1439
        for t in possible_transports:
 
1440
            t_same_connection = t._reuse_for(base)
 
1441
            if t_same_connection is not None:
 
1442
                # Add only new transports
 
1443
                if t_same_connection not in possible_transports:
 
1444
                    possible_transports.append(t_same_connection)
 
1445
                return t_same_connection
 
1446
 
1129
1447
    for proto, factory_list in transport_list_registry.iteritems():
1130
1448
        if proto is not None and base.startswith(proto):
1131
 
            t, last_err = _try_transport_factories(base, factory_list)
1132
 
            if t:
1133
 
                return t
 
1449
            transport, last_err = _try_transport_factories(base, factory_list)
 
1450
            if transport:
 
1451
                if possible_transports:
 
1452
                    assert transport not in possible_transports
 
1453
                    possible_transports.append(transport)
 
1454
                return transport
1134
1455
 
1135
1456
    # We tried all the different protocols, now try one last time
1136
1457
    # as a local protocol
1137
1458
    base = convert_path_to_url(base, 'Unsupported protocol: %s')
1138
1459
 
1139
1460
    # The default handler is the filesystem handler, stored as protocol None
1140
 
    return _try_transport_factories(base,
1141
 
                    transport_list_registry.get(None))[0]
1142
 
                                                   
 
1461
    factory_list = transport_list_registry.get(None)
 
1462
    transport, last_err = _try_transport_factories(base, factory_list)
 
1463
 
 
1464
    return transport
 
1465
 
 
1466
 
 
1467
def _try_transport_factories(base, factory_list):
 
1468
    last_err = None
 
1469
    for factory in factory_list:
 
1470
        try:
 
1471
            return factory.get_obj()(base), None
 
1472
        except errors.DependencyNotPresent, e:
 
1473
            mutter("failed to instantiate transport %r for %r: %r" %
 
1474
                    (factory, base, e))
 
1475
            last_err = e
 
1476
            continue
 
1477
    return None, last_err
 
1478
 
 
1479
 
1143
1480
def do_catching_redirections(action, transport, redirected):
1144
1481
    """Execute an action with given transport catching redirections.
1145
1482
 
1182
1519
        raise errors.TooManyRedirections
1183
1520
 
1184
1521
 
1185
 
def _try_transport_factories(base, factory_list):
1186
 
    last_err = None
1187
 
    for factory in factory_list:
1188
 
        try:
1189
 
            return factory.get_obj()(base), None
1190
 
        except errors.DependencyNotPresent, e:
1191
 
            mutter("failed to instantiate transport %r for %r: %r" %
1192
 
                    (factory, base, e))
1193
 
            last_err = e
1194
 
            continue
1195
 
    return None, last_err
1196
 
 
1197
 
 
1198
1522
class Server(object):
1199
1523
    """A Transport Server.
1200
1524