~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/transport/__init__.py

  • Committer: Tarmac
  • Author(s): Vincent Ladeuil
  • Date: 2017-01-30 14:42:05 UTC
  • mfrom: (6620.1.1 trunk)
  • Revision ID: tarmac-20170130144205-r8fh2xpmiuxyozpv
Merge  2.7 into trunk including fix for bug #1657238 [r=vila]

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2005-2011 Canonical Ltd
 
1
# Copyright (C) 2005-2012, 2016 Canonical Ltd
2
2
#
3
3
# This program is free software; you can redistribute it and/or modify
4
4
# it under the terms of the GNU General Public License as published by
26
26
it.
27
27
"""
28
28
 
 
29
from __future__ import absolute_import
 
30
 
29
31
from cStringIO import StringIO
30
32
import sys
31
33
 
33
35
lazy_import(globals(), """
34
36
import errno
35
37
from stat import S_ISDIR
36
 
import urllib
37
38
import urlparse
38
39
 
39
40
from bzrlib import (
51
52
from bzrlib.trace import (
52
53
    mutter,
53
54
    )
54
 
from bzrlib import registry
 
55
from bzrlib import (
 
56
    hooks,
 
57
    registry,
 
58
    )
55
59
 
56
60
 
57
61
# a dictionary of open file streams. Keys are absolute paths, values are
118
122
    def register_transport(self, key, help=None):
119
123
        self.register(key, [], help)
120
124
 
121
 
    def set_default_transport(self, key=None):
122
 
        """Return either 'key' or the default key if key is None"""
123
 
        self._default_key = key
124
 
 
125
125
 
126
126
transport_list_registry = TransportListRegistry()
127
127
 
138
138
def register_lazy_transport(prefix, module, classname):
139
139
    if not prefix in transport_list_registry:
140
140
        register_transport_proto(prefix)
141
 
    transport_list_registry.register_lazy_transport_provider(prefix, module, classname)
142
 
 
143
 
 
144
 
def register_transport(prefix, klass, override=DEPRECATED_PARAMETER):
 
141
    transport_list_registry.register_lazy_transport_provider(
 
142
        prefix, module, classname)
 
143
 
 
144
 
 
145
def register_transport(prefix, klass):
145
146
    if not prefix in transport_list_registry:
146
147
        register_transport_proto(prefix)
147
148
    transport_list_registry.register_transport_provider(prefix, klass)
231
232
    def _close(self):
232
233
        """A hook point for subclasses that need to take action on close."""
233
234
 
234
 
    def close(self):
 
235
    def close(self, want_fdatasync=False):
 
236
        if want_fdatasync:
 
237
            try:
 
238
                self.fdatasync()
 
239
            except errors.TransportNotPossible:
 
240
                pass
235
241
        self._close()
236
242
        del _file_streams[self.transport.abspath(self.relpath)]
237
243
 
 
244
    def fdatasync(self):
 
245
        """Force data out to physical disk if possible.
 
246
 
 
247
        :raises TransportNotPossible: If this transport has no way to 
 
248
            flush to disk.
 
249
        """
 
250
        raise errors.TransportNotPossible(
 
251
            "%s cannot fdatasync" % (self.transport,))
 
252
 
238
253
 
239
254
class FileFileStream(FileStream):
240
255
    """A file stream object returned by open_write_stream.
249
264
    def _close(self):
250
265
        self.file_handle.close()
251
266
 
 
267
    def fdatasync(self):
 
268
        """Force data out to physical disk if possible."""
 
269
        self.file_handle.flush()
 
270
        try:
 
271
            fileno = self.file_handle.fileno()
 
272
        except AttributeError:
 
273
            raise errors.TransportNotPossible()
 
274
        osutils.fdatasync(fileno)
 
275
 
252
276
    def write(self, bytes):
253
277
        osutils.pump_string_file(bytes, self.file_handle)
254
278
 
263
287
        self.transport.append_bytes(self.relpath, bytes)
264
288
 
265
289
 
 
290
class TransportHooks(hooks.Hooks):
 
291
    """Mapping of hook names to registered callbacks for transport hooks"""
 
292
    def __init__(self):
 
293
        super(TransportHooks, self).__init__()
 
294
        self.add_hook("post_connect",
 
295
            "Called after a new connection is established or a reconnect "
 
296
            "occurs. The sole argument passed is either the connected "
 
297
            "transport or smart medium instance.", (2, 5))
 
298
 
 
299
 
266
300
class Transport(object):
267
301
    """This class encapsulates methods for retrieving or putting a file
268
302
    from/to a storage location.
287
321
    #       where the biggest benefit between combining reads and
288
322
    #       and seeking is. Consider a runtime auto-tune.
289
323
    _bytes_to_read_before_seek = 0
 
324
    
 
325
    hooks = TransportHooks()
290
326
 
291
327
    def __init__(self, base):
292
328
        super(Transport, self).__init__()
293
329
        self.base = base
 
330
        (self._raw_base, self._segment_parameters) = (
 
331
            urlutils.split_segment_parameters(base))
294
332
 
295
333
    def _translate_error(self, e, path, raise_generic=True):
296
334
        """Translate an IOError or OSError into an appropriate bzr error.
298
336
        This handles things like ENOENT, ENOTDIR, EEXIST, and EACCESS
299
337
        """
300
338
        if getattr(e, 'errno', None) is not None:
301
 
            if e.errno in (errno.ENOENT, errno.ENOTDIR, errno.EINVAL):
 
339
            if e.errno in (errno.ENOENT, errno.ENOTDIR):
 
340
                raise errors.NoSuchFile(path, extra=e)
 
341
            elif e.errno == errno.EINVAL:
 
342
                mutter("EINVAL returned on path %s: %r" % (path, e))
302
343
                raise errors.NoSuchFile(path, extra=e)
303
344
            # I would rather use errno.EFOO, but there doesn't seem to be
304
345
            # any matching for 267
324
365
        """
325
366
        raise NotImplementedError(self.clone)
326
367
 
327
 
    def create_prefix(self):
 
368
    def create_prefix(self, mode=None):
328
369
        """Create all the directories leading down to self.base."""
329
370
        cur_transport = self
330
371
        needed = [cur_transport]
336
377
                    "Failed to create path prefix for %s."
337
378
                    % cur_transport.base)
338
379
            try:
339
 
                new_transport.mkdir('.')
 
380
                new_transport.mkdir('.', mode=mode)
340
381
            except errors.NoSuchFile:
341
382
                needed.append(new_transport)
342
383
                cur_transport = new_transport
347
388
        # Now we only need to create child directories
348
389
        while needed:
349
390
            cur_transport = needed.pop()
350
 
            cur_transport.ensure_base()
 
391
            cur_transport.ensure_base(mode=mode)
351
392
 
352
 
    def ensure_base(self):
 
393
    def ensure_base(self, mode=None):
353
394
        """Ensure that the directory this transport references exists.
354
395
 
355
396
        This will create a directory if it doesn't exist.
359
400
        # than permission". We attempt to create the directory, and just
360
401
        # suppress FileExists and PermissionDenied (for Windows) exceptions.
361
402
        try:
362
 
            self.mkdir('.')
 
403
            self.mkdir('.', mode=mode)
363
404
        except (errors.FileExists, errors.PermissionDenied):
364
405
            return False
365
406
        else:
387
428
        """
388
429
        raise NotImplementedError(self.external_url)
389
430
 
 
431
    def get_segment_parameters(self):
 
432
        """Return the segment parameters for the top segment of the URL.
 
433
        """
 
434
        return self._segment_parameters
 
435
 
 
436
    def set_segment_parameter(self, name, value):
 
437
        """Set a segment parameter.
 
438
 
 
439
        :param name: Segment parameter name (urlencoded string)
 
440
        :param value: Segment parameter value (urlencoded string)
 
441
        """
 
442
        if value is None:
 
443
            try:
 
444
                del self._segment_parameters[name]
 
445
            except KeyError:
 
446
                pass
 
447
        else:
 
448
            self._segment_parameters[name] = value
 
449
        self.base = urlutils.join_segment_parameters(
 
450
            self._raw_base, self._segment_parameters)
 
451
 
390
452
    def _pump(self, from_file, to_file):
391
453
        """Most children will need to copy from one file-like
392
454
        object or string to another one.
457
519
        # interface ?
458
520
        raise NotImplementedError(self.abspath)
459
521
 
460
 
    def _combine_paths(self, base_path, relpath):
461
 
        """Transform a Transport-relative path to a remote absolute path.
462
 
 
463
 
        This does not handle substitution of ~ but does handle '..' and '.'
464
 
        components.
465
 
 
466
 
        Examples::
467
 
 
468
 
            t._combine_paths('/home/sarah', 'project/foo')
469
 
                => '/home/sarah/project/foo'
470
 
            t._combine_paths('/home/sarah', '../../etc')
471
 
                => '/etc'
472
 
            t._combine_paths('/home/sarah', '/etc')
473
 
                => '/etc'
474
 
 
475
 
        :param base_path: urlencoded path for the transport root; typically a
476
 
             URL but need not contain scheme/host/etc.
477
 
        :param relpath: relative url string for relative part of remote path.
478
 
        :return: urlencoded string for final path.
479
 
        """
480
 
        if not isinstance(relpath, str):
481
 
            raise errors.InvalidURL(relpath)
482
 
        if relpath.startswith('/'):
483
 
            base_parts = []
484
 
        else:
485
 
            base_parts = base_path.split('/')
486
 
        if len(base_parts) > 0 and base_parts[-1] == '':
487
 
            base_parts = base_parts[:-1]
488
 
        for p in relpath.split('/'):
489
 
            if p == '..':
490
 
                if len(base_parts) == 0:
491
 
                    # In most filesystems, a request for the parent
492
 
                    # of root, just returns root.
493
 
                    continue
494
 
                base_parts.pop()
495
 
            elif p == '.':
496
 
                continue # No-op
497
 
            elif p != '':
498
 
                base_parts.append(p)
499
 
        path = '/'.join(base_parts)
500
 
        if not path.startswith('/'):
501
 
            path = '/' + path
502
 
        return path
503
 
 
504
522
    def recommended_page_size(self):
505
523
        """Return the recommended page size for this transport.
506
524
 
855
873
            yield self.get(relpath)
856
874
            count += 1
857
875
 
858
 
    def put_bytes(self, relpath, bytes, mode=None):
 
876
    def put_bytes(self, relpath, raw_bytes, mode=None):
859
877
        """Atomically put the supplied bytes into the given location.
860
878
 
861
879
        :param relpath: The location to put the contents, relative to the
862
880
            transport base.
863
 
        :param bytes: A bytestring of data.
 
881
        :param raw_bytes: A bytestring of data.
864
882
        :param mode: Create the file with the given mode.
865
883
        :return: None
866
884
        """
867
 
        if not isinstance(bytes, str):
868
 
            raise AssertionError(
869
 
                'bytes must be a plain string, not %s' % type(bytes))
870
 
        return self.put_file(relpath, StringIO(bytes), mode=mode)
 
885
        if not isinstance(raw_bytes, str):
 
886
            raise TypeError(
 
887
                'raw_bytes must be a plain string, not %s' % type(raw_bytes))
 
888
        return self.put_file(relpath, StringIO(raw_bytes), mode=mode)
871
889
 
872
 
    def put_bytes_non_atomic(self, relpath, bytes, mode=None,
 
890
    def put_bytes_non_atomic(self, relpath, raw_bytes, mode=None,
873
891
                             create_parent_dir=False,
874
892
                             dir_mode=None):
875
893
        """Copy the string into the target location.
878
896
        Transport.put_bytes_non_atomic for more information.
879
897
 
880
898
        :param relpath: The remote location to put the contents.
881
 
        :param bytes:   A string object containing the raw bytes to write into
882
 
                        the target file.
 
899
        :param raw_bytes:   A string object containing the raw bytes to write
 
900
                        into the target file.
883
901
        :param mode:    Possible access permissions for new file.
884
902
                        None means do not set remote permissions.
885
903
        :param create_parent_dir: If we cannot create the target file because
887
905
                        create it, and then try again.
888
906
        :param dir_mode: Possible access permissions for new directories.
889
907
        """
890
 
        if not isinstance(bytes, str):
891
 
            raise AssertionError(
892
 
                'bytes must be a plain string, not %s' % type(bytes))
893
 
        self.put_file_non_atomic(relpath, StringIO(bytes), mode=mode,
 
908
        if not isinstance(raw_bytes, str):
 
909
            raise TypeError(
 
910
                'raw_bytes must be a plain string, not %s' % type(raw_bytes))
 
911
        self.put_file_non_atomic(relpath, StringIO(raw_bytes), mode=mode,
894
912
                                 create_parent_dir=create_parent_dir,
895
913
                                 dir_mode=dir_mode)
896
914
 
1341
1359
        """
1342
1360
        if not base.endswith('/'):
1343
1361
            base += '/'
1344
 
        (self._scheme,
1345
 
         self._user, self._password,
1346
 
         self._host, self._port,
1347
 
         self._path) = self._split_url(base)
 
1362
        self._parsed_url = self._split_url(base)
1348
1363
        if _from_transport is not None:
1349
1364
            # Copy the password as it does not appear in base and will be lost
1350
1365
            # otherwise. It can appear in the _split_url above if the user
1351
1366
            # provided it on the command line. Otherwise, daughter classes will
1352
1367
            # prompt the user for one when appropriate.
1353
 
            self._password = _from_transport._password
 
1368
            self._parsed_url.password = _from_transport._parsed_url.password
 
1369
            self._parsed_url.quoted_password = (
 
1370
                _from_transport._parsed_url.quoted_password)
1354
1371
 
1355
 
        base = self._unsplit_url(self._scheme,
1356
 
                                 self._user, self._password,
1357
 
                                 self._host, self._port,
1358
 
                                 self._path)
 
1372
        base = str(self._parsed_url)
1359
1373
 
1360
1374
        super(ConnectedTransport, self).__init__(base)
1361
1375
        if _from_transport is None:
1363
1377
        else:
1364
1378
            self._shared_connection = _from_transport._shared_connection
1365
1379
 
 
1380
    @property
 
1381
    def _user(self):
 
1382
        return self._parsed_url.user
 
1383
 
 
1384
    @property
 
1385
    def _password(self):
 
1386
        return self._parsed_url.password
 
1387
 
 
1388
    @property
 
1389
    def _host(self):
 
1390
        return self._parsed_url.host
 
1391
 
 
1392
    @property
 
1393
    def _port(self):
 
1394
        return self._parsed_url.port
 
1395
 
 
1396
    @property
 
1397
    def _path(self):
 
1398
        return self._parsed_url.path
 
1399
 
 
1400
    @property
 
1401
    def _scheme(self):
 
1402
        return self._parsed_url.scheme
 
1403
 
1366
1404
    def clone(self, offset=None):
1367
1405
        """Return a new transport with root at self.base + offset
1368
1406
 
1376
1414
 
1377
1415
    @staticmethod
1378
1416
    def _split_url(url):
1379
 
        return urlutils.parse_url(url)
 
1417
        return urlutils.URL.from_string(url)
1380
1418
 
1381
1419
    @staticmethod
1382
1420
    def _unsplit_url(scheme, user, password, host, port, path):
1383
 
        """
1384
 
        Build the full URL for the given already URL encoded path.
 
1421
        """Build the full URL for the given already URL encoded path.
1385
1422
 
1386
1423
        user, password, host and path will be quoted if they contain reserved
1387
1424
        chars.
1388
1425
 
1389
1426
        :param scheme: protocol
1390
 
 
1391
1427
        :param user: login
1392
 
 
1393
1428
        :param password: associated password
1394
 
 
1395
1429
        :param host: the server address
1396
 
 
1397
1430
        :param port: the associated port
1398
 
 
1399
1431
        :param path: the absolute path on the server
1400
1432
 
1401
1433
        :return: The corresponding URL.
1402
1434
        """
1403
 
        netloc = urllib.quote(host)
 
1435
        netloc = urlutils.quote(host)
1404
1436
        if user is not None:
1405
1437
            # Note that we don't put the password back even if we
1406
1438
            # have one so that it doesn't get accidentally
1407
1439
            # exposed.
1408
 
            netloc = '%s@%s' % (urllib.quote(user), netloc)
 
1440
            netloc = '%s@%s' % (urlutils.quote(user), netloc)
1409
1441
        if port is not None:
1410
1442
            netloc = '%s:%d' % (netloc, port)
1411
1443
        path = urlutils.escape(path)
1413
1445
 
1414
1446
    def relpath(self, abspath):
1415
1447
        """Return the local path portion from a given absolute path"""
1416
 
        scheme, user, password, host, port, path = self._split_url(abspath)
 
1448
        parsed_url = self._split_url(abspath)
1417
1449
        error = []
1418
 
        if (scheme != self._scheme):
 
1450
        if parsed_url.scheme != self._parsed_url.scheme:
1419
1451
            error.append('scheme mismatch')
1420
 
        if (user != self._user):
 
1452
        if parsed_url.user != self._parsed_url.user:
1421
1453
            error.append('user name mismatch')
1422
 
        if (host != self._host):
 
1454
        if parsed_url.host != self._parsed_url.host:
1423
1455
            error.append('host mismatch')
1424
 
        if (port != self._port):
 
1456
        if parsed_url.port != self._parsed_url.port:
1425
1457
            error.append('port mismatch')
1426
 
        if not (path == self._path[:-1] or path.startswith(self._path)):
 
1458
        if (not (parsed_url.path == self._parsed_url.path[:-1] or
 
1459
            parsed_url.path.startswith(self._parsed_url.path))):
1427
1460
            error.append('path mismatch')
1428
1461
        if error:
1429
1462
            extra = ', '.join(error)
1430
1463
            raise errors.PathNotChild(abspath, self.base, extra=extra)
1431
 
        pl = len(self._path)
1432
 
        return path[pl:].strip('/')
 
1464
        pl = len(self._parsed_url.path)
 
1465
        return parsed_url.path[pl:].strip('/')
1433
1466
 
1434
1467
    def abspath(self, relpath):
1435
1468
        """Return the full url to the given relative path.
1438
1471
 
1439
1472
        :returns: the Unicode version of the absolute path for relpath.
1440
1473
        """
1441
 
        relative = urlutils.unescape(relpath).encode('utf-8')
1442
 
        path = self._combine_paths(self._path, relative)
1443
 
        return self._unsplit_url(self._scheme, self._user, self._password,
1444
 
                                 self._host, self._port,
1445
 
                                 path)
 
1474
        return str(self._parsed_url.clone(relpath))
1446
1475
 
1447
1476
    def _remote_path(self, relpath):
1448
1477
        """Return the absolute path part of the url to the given relative path.
1455
1484
 
1456
1485
        :return: the absolute Unicode path on the server,
1457
1486
        """
1458
 
        relative = urlutils.unescape(relpath).encode('utf-8')
1459
 
        remote_path = self._combine_paths(self._path, relative)
1460
 
        return remote_path
 
1487
        return self._parsed_url.clone(relpath).path
1461
1488
 
1462
1489
    def _get_shared_connection(self):
1463
1490
        """Get the object shared amongst cloned transports.
1485
1512
        """
1486
1513
        self._shared_connection.connection = connection
1487
1514
        self._shared_connection.credentials = credentials
 
1515
        for hook in self.hooks["post_connect"]:
 
1516
            hook(self)
1488
1517
 
1489
1518
    def _get_connection(self):
1490
1519
        """Returns the transport specific connection object."""
1522
1551
        :return: A new transport or None if the connection cannot be shared.
1523
1552
        """
1524
1553
        try:
1525
 
            (scheme, user, password,
1526
 
             host, port, path) = self._split_url(other_base)
 
1554
            parsed_url = self._split_url(other_base)
1527
1555
        except errors.InvalidURL:
1528
1556
            # No hope in trying to reuse an existing transport for an invalid
1529
1557
            # URL
1532
1560
        transport = None
1533
1561
        # Don't compare passwords, they may be absent from other_base or from
1534
1562
        # self and they don't carry more information than user anyway.
1535
 
        if (scheme == self._scheme
1536
 
            and user == self._user
1537
 
            and host == self._host
1538
 
            and port == self._port):
 
1563
        if (parsed_url.scheme == self._parsed_url.scheme
 
1564
            and parsed_url.user == self._parsed_url.user
 
1565
            and parsed_url.host == self._parsed_url.host
 
1566
            and parsed_url.port == self._parsed_url.port):
 
1567
            path = parsed_url.path
1539
1568
            if not path.endswith('/'):
1540
1569
                # This normally occurs at __init__ time, but it's easier to do
1541
1570
                # it now to avoid creating two transports for the same base.
1542
1571
                path += '/'
1543
 
            if self._path  == path:
 
1572
            if self._parsed_url.path  == path:
1544
1573
                # shortcut, it's really the same transport
1545
1574
                return self
1546
1575
            # We don't call clone here because the intent is different: we
1557
1586
        raise NotImplementedError(self.disconnect)
1558
1587
 
1559
1588
 
1560
 
def get_transport(base, possible_transports=None):
1561
 
    """Open a transport to access a URL or directory.
1562
 
 
1563
 
    :param base: either a URL or a directory name.
1564
 
 
1565
 
    :param transports: optional reusable transports list. If not None, created
1566
 
        transports will be added to the list.
1567
 
 
1568
 
    :return: A new transport optionally sharing its connection with one of
1569
 
        possible_transports.
 
1589
def location_to_url(location):
 
1590
    """Determine a fully qualified URL from a location string.
 
1591
 
 
1592
    This will try to interpret location as both a URL and a directory path. It
 
1593
    will also lookup the location in directories.
 
1594
 
 
1595
    :param location: Unicode or byte string object with a location
 
1596
    :raise InvalidURL: If the location is already a URL, but not valid.
 
1597
    :return: Byte string with resulting URL
1570
1598
    """
1571
 
    if base is None:
1572
 
        base = '.'
1573
 
    last_err = None
 
1599
    if not isinstance(location, basestring):
 
1600
        raise AssertionError("location not a byte or unicode string")
1574
1601
    from bzrlib.directory_service import directories
1575
 
    base = directories.dereference(base)
1576
 
 
1577
 
    def convert_path_to_url(base, error_str):
1578
 
        if urlutils.is_url(base):
1579
 
            # This looks like a URL, but we weren't able to
1580
 
            # instantiate it as such raise an appropriate error
1581
 
            # FIXME: we have a 'error_str' unused and we use last_err below
1582
 
            raise errors.UnsupportedProtocol(base, last_err)
1583
 
        # This doesn't look like a protocol, consider it a local path
1584
 
        new_base = urlutils.local_path_to_url(base)
1585
 
        # mutter('converting os path %r => url %s', base, new_base)
1586
 
        return new_base
 
1602
    location = directories.dereference(location)
1587
1603
 
1588
1604
    # Catch any URLs which are passing Unicode rather than ASCII
1589
1605
    try:
1590
 
        base = base.encode('ascii')
 
1606
        location = location.encode('ascii')
1591
1607
    except UnicodeError:
1592
 
        # Only local paths can be Unicode
1593
 
        base = convert_path_to_url(base,
1594
 
            'URLs must be properly escaped (protocol: %s)')
1595
 
 
 
1608
        if urlutils.is_url(location):
 
1609
            raise errors.InvalidURL(path=location,
 
1610
                extra='URLs must be properly escaped')
 
1611
        location = urlutils.local_path_to_url(location)
 
1612
 
 
1613
    if location.startswith("file:") and not location.startswith("file://"):
 
1614
        return urlutils.join(urlutils.local_path_to_url("."), location[5:])
 
1615
 
 
1616
    if not urlutils.is_url(location):
 
1617
        return urlutils.local_path_to_url(location)
 
1618
 
 
1619
    return location
 
1620
 
 
1621
 
 
1622
def get_transport_from_path(path, possible_transports=None):
 
1623
    """Open a transport for a local path.
 
1624
 
 
1625
    :param path: Local path as byte or unicode string
 
1626
    :return: Transport object for path
 
1627
    """
 
1628
    return get_transport_from_url(urlutils.local_path_to_url(path),
 
1629
        possible_transports)
 
1630
 
 
1631
 
 
1632
def get_transport_from_url(url, possible_transports=None):
 
1633
    """Open a transport to access a URL.
 
1634
    
 
1635
    :param base: a URL
 
1636
    :param transports: optional reusable transports list. If not None, created
 
1637
        transports will be added to the list.
 
1638
 
 
1639
    :return: A new transport optionally sharing its connection with one of
 
1640
        possible_transports.
 
1641
    """
1596
1642
    transport = None
1597
1643
    if possible_transports is not None:
1598
1644
        for t in possible_transports:
1599
 
            t_same_connection = t._reuse_for(base)
 
1645
            t_same_connection = t._reuse_for(url)
1600
1646
            if t_same_connection is not None:
1601
1647
                # Add only new transports
1602
1648
                if t_same_connection not in possible_transports:
1603
1649
                    possible_transports.append(t_same_connection)
1604
1650
                return t_same_connection
1605
1651
 
 
1652
    last_err = None
1606
1653
    for proto, factory_list in transport_list_registry.items():
1607
 
        if proto is not None and base.startswith(proto):
1608
 
            transport, last_err = _try_transport_factories(base, factory_list)
 
1654
        if proto is not None and url.startswith(proto):
 
1655
            transport, last_err = _try_transport_factories(url, factory_list)
1609
1656
            if transport:
1610
1657
                if possible_transports is not None:
1611
1658
                    if transport in possible_transports:
1612
1659
                        raise AssertionError()
1613
1660
                    possible_transports.append(transport)
1614
1661
                return transport
1615
 
 
1616
 
    # We tried all the different protocols, now try one last time
1617
 
    # as a local protocol
1618
 
    base = convert_path_to_url(base, 'Unsupported protocol: %s')
1619
 
 
1620
 
    # The default handler is the filesystem handler, stored as protocol None
1621
 
    factory_list = transport_list_registry.get(None)
1622
 
    transport, last_err = _try_transport_factories(base, factory_list)
1623
 
 
1624
 
    return transport
 
1662
    if not urlutils.is_url(url):
 
1663
        raise errors.InvalidURL(path=url)
 
1664
    raise errors.UnsupportedProtocol(url, last_err)
 
1665
 
 
1666
 
 
1667
def get_transport(base, possible_transports=None):
 
1668
    """Open a transport to access a URL or directory.
 
1669
 
 
1670
    :param base: either a URL or a directory name.
 
1671
 
 
1672
    :param transports: optional reusable transports list. If not None, created
 
1673
        transports will be added to the list.
 
1674
 
 
1675
    :return: A new transport optionally sharing its connection with one of
 
1676
        possible_transports.
 
1677
    """
 
1678
    if base is None:
 
1679
        base = '.'
 
1680
    return get_transport_from_url(location_to_url(base), possible_transports)
1625
1681
 
1626
1682
 
1627
1683
def _try_transport_factories(base, factory_list):
1696
1752
register_transport_proto('file://',
1697
1753
            help="Access using the standard filesystem (default)")
1698
1754
register_lazy_transport('file://', 'bzrlib.transport.local', 'LocalTransport')
1699
 
transport_list_registry.set_default_transport("file://")
1700
1755
 
1701
1756
register_transport_proto('sftp://',
1702
1757
            help="Access using SFTP (most SSH servers provide SFTP).",
1728
1783
                 help="Read-only access of branches exported on the web.")
1729
1784
register_transport_proto('https://',
1730
1785
            help="Read-only access of branches exported on the web using SSL.")
1731
 
# The default http implementation is urllib, but https is pycurl if available
 
1786
# The default http implementation is urllib
1732
1787
register_lazy_transport('http://', 'bzrlib.transport.http._pycurl',
1733
1788
                        'PyCurlTransport')
1734
1789
register_lazy_transport('http://', 'bzrlib.transport.http._urllib',
1735
1790
                        'HttpTransport_urllib')
 
1791
register_lazy_transport('https://', 'bzrlib.transport.http._pycurl',
 
1792
                        'PyCurlTransport')
1736
1793
register_lazy_transport('https://', 'bzrlib.transport.http._urllib',
1737
1794
                        'HttpTransport_urllib')
1738
 
register_lazy_transport('https://', 'bzrlib.transport.http._pycurl',
1739
 
                        'PyCurlTransport')
1740
1795
 
1741
1796
register_transport_proto('ftp://', help="Access using passive FTP.")
1742
1797
register_lazy_transport('ftp://', 'bzrlib.transport.ftp', 'FtpTransport')