~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/transport/__init__.py

  • Committer: Patch Queue Manager
  • Date: 2016-02-01 19:56:05 UTC
  • mfrom: (6615.1.1 trunk)
  • Revision ID: pqm@pqm.ubuntu.com-20160201195605-o7rl92wf6uyum3fk
(vila) Open trunk again as 2.8b1 (Vincent Ladeuil)

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2005-2010 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
 
import re
31
32
import sys
32
33
 
33
34
from bzrlib.lazy_import import lazy_import
34
35
lazy_import(globals(), """
35
36
import errno
36
37
from stat import S_ISDIR
37
 
import urllib
38
38
import urlparse
39
39
 
40
40
from bzrlib import (
47
47
""")
48
48
 
49
49
from bzrlib.symbol_versioning import (
50
 
        DEPRECATED_PARAMETER,
51
 
        )
 
50
    DEPRECATED_PARAMETER,
 
51
    )
52
52
from bzrlib.trace import (
53
53
    mutter,
54
54
    )
55
 
from bzrlib import registry
 
55
from bzrlib import (
 
56
    hooks,
 
57
    registry,
 
58
    )
56
59
 
57
60
 
58
61
# a dictionary of open file streams. Keys are absolute paths, values are
84
87
    modules = set()
85
88
    for prefix, factory_list in transport_list_registry.items():
86
89
        for factory in factory_list:
87
 
            if hasattr(factory, "_module_name"):
88
 
                modules.add(factory._module_name)
89
 
            else:
90
 
                modules.add(factory._obj.__module__)
 
90
            modules.add(factory.get_module())
91
91
    # Add chroot and pathfilter directly, because there is no handler
92
92
    # registered for it.
93
93
    modules.add('bzrlib.transport.chroot')
122
122
    def register_transport(self, key, help=None):
123
123
        self.register(key, [], help)
124
124
 
125
 
    def set_default_transport(self, key=None):
126
 
        """Return either 'key' or the default key if key is None"""
127
 
        self._default_key = key
128
 
 
129
125
 
130
126
transport_list_registry = TransportListRegistry()
131
127
 
142
138
def register_lazy_transport(prefix, module, classname):
143
139
    if not prefix in transport_list_registry:
144
140
        register_transport_proto(prefix)
145
 
    transport_list_registry.register_lazy_transport_provider(prefix, module, classname)
146
 
 
147
 
 
148
 
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):
149
146
    if not prefix in transport_list_registry:
150
147
        register_transport_proto(prefix)
151
148
    transport_list_registry.register_transport_provider(prefix, klass)
235
232
    def _close(self):
236
233
        """A hook point for subclasses that need to take action on close."""
237
234
 
238
 
    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
239
241
        self._close()
240
242
        del _file_streams[self.transport.abspath(self.relpath)]
241
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
 
242
253
 
243
254
class FileFileStream(FileStream):
244
255
    """A file stream object returned by open_write_stream.
253
264
    def _close(self):
254
265
        self.file_handle.close()
255
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
 
256
276
    def write(self, bytes):
257
277
        osutils.pump_string_file(bytes, self.file_handle)
258
278
 
267
287
        self.transport.append_bytes(self.relpath, bytes)
268
288
 
269
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
 
270
300
class Transport(object):
271
301
    """This class encapsulates methods for retrieving or putting a file
272
302
    from/to a storage location.
291
321
    #       where the biggest benefit between combining reads and
292
322
    #       and seeking is. Consider a runtime auto-tune.
293
323
    _bytes_to_read_before_seek = 0
 
324
    
 
325
    hooks = TransportHooks()
294
326
 
295
327
    def __init__(self, base):
296
328
        super(Transport, self).__init__()
297
329
        self.base = base
 
330
        (self._raw_base, self._segment_parameters) = (
 
331
            urlutils.split_segment_parameters(base))
298
332
 
299
333
    def _translate_error(self, e, path, raise_generic=True):
300
334
        """Translate an IOError or OSError into an appropriate bzr error.
302
336
        This handles things like ENOENT, ENOTDIR, EEXIST, and EACCESS
303
337
        """
304
338
        if getattr(e, 'errno', None) is not None:
305
 
            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))
306
343
                raise errors.NoSuchFile(path, extra=e)
307
344
            # I would rather use errno.EFOO, but there doesn't seem to be
308
345
            # any matching for 267
328
365
        """
329
366
        raise NotImplementedError(self.clone)
330
367
 
331
 
    def create_prefix(self):
 
368
    def create_prefix(self, mode=None):
332
369
        """Create all the directories leading down to self.base."""
333
370
        cur_transport = self
334
371
        needed = [cur_transport]
340
377
                    "Failed to create path prefix for %s."
341
378
                    % cur_transport.base)
342
379
            try:
343
 
                new_transport.mkdir('.')
 
380
                new_transport.mkdir('.', mode=mode)
344
381
            except errors.NoSuchFile:
345
382
                needed.append(new_transport)
346
383
                cur_transport = new_transport
351
388
        # Now we only need to create child directories
352
389
        while needed:
353
390
            cur_transport = needed.pop()
354
 
            cur_transport.ensure_base()
 
391
            cur_transport.ensure_base(mode=mode)
355
392
 
356
 
    def ensure_base(self):
 
393
    def ensure_base(self, mode=None):
357
394
        """Ensure that the directory this transport references exists.
358
395
 
359
396
        This will create a directory if it doesn't exist.
363
400
        # than permission". We attempt to create the directory, and just
364
401
        # suppress FileExists and PermissionDenied (for Windows) exceptions.
365
402
        try:
366
 
            self.mkdir('.')
 
403
            self.mkdir('.', mode=mode)
367
404
        except (errors.FileExists, errors.PermissionDenied):
368
405
            return False
369
406
        else:
391
428
        """
392
429
        raise NotImplementedError(self.external_url)
393
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
 
394
452
    def _pump(self, from_file, to_file):
395
453
        """Most children will need to copy from one file-like
396
454
        object or string to another one.
461
519
        # interface ?
462
520
        raise NotImplementedError(self.abspath)
463
521
 
464
 
    def _combine_paths(self, base_path, relpath):
465
 
        """Transform a Transport-relative path to a remote absolute path.
466
 
 
467
 
        This does not handle substitution of ~ but does handle '..' and '.'
468
 
        components.
469
 
 
470
 
        Examples::
471
 
 
472
 
            t._combine_paths('/home/sarah', 'project/foo')
473
 
                => '/home/sarah/project/foo'
474
 
            t._combine_paths('/home/sarah', '../../etc')
475
 
                => '/etc'
476
 
            t._combine_paths('/home/sarah', '/etc')
477
 
                => '/etc'
478
 
 
479
 
        :param base_path: urlencoded path for the transport root; typically a
480
 
             URL but need not contain scheme/host/etc.
481
 
        :param relpath: relative url string for relative part of remote path.
482
 
        :return: urlencoded string for final path.
483
 
        """
484
 
        if not isinstance(relpath, str):
485
 
            raise errors.InvalidURL(relpath)
486
 
        if relpath.startswith('/'):
487
 
            base_parts = []
488
 
        else:
489
 
            base_parts = base_path.split('/')
490
 
        if len(base_parts) > 0 and base_parts[-1] == '':
491
 
            base_parts = base_parts[:-1]
492
 
        for p in relpath.split('/'):
493
 
            if p == '..':
494
 
                if len(base_parts) == 0:
495
 
                    # In most filesystems, a request for the parent
496
 
                    # of root, just returns root.
497
 
                    continue
498
 
                base_parts.pop()
499
 
            elif p == '.':
500
 
                continue # No-op
501
 
            elif p != '':
502
 
                base_parts.append(p)
503
 
        path = '/'.join(base_parts)
504
 
        if not path.startswith('/'):
505
 
            path = '/' + path
506
 
        return path
507
 
 
508
522
    def recommended_page_size(self):
509
523
        """Return the recommended page size for this transport.
510
524
 
676
690
 
677
691
        This uses _coalesce_offsets to issue larger reads and fewer seeks.
678
692
 
679
 
        :param fp: A file-like object that supports seek() and read(size)
 
693
        :param fp: A file-like object that supports seek() and read(size).
 
694
            Note that implementations are allowed to call .close() on this file
 
695
            handle, so don't trust that you can use it for other work.
680
696
        :param offsets: A list of offsets to be read from the given file.
681
697
        :return: yield (pos, data) tuples for each request
682
698
        """
693
709
 
694
710
        # Cache the results, but only until they have been fulfilled
695
711
        data_map = {}
696
 
        for c_offset in coalesced:
697
 
            # TODO: jam 20060724 it might be faster to not issue seek if
698
 
            #       we are already at the right location. This should be
699
 
            #       benchmarked.
700
 
            fp.seek(c_offset.start)
701
 
            data = fp.read(c_offset.length)
702
 
            if len(data) < c_offset.length:
703
 
                raise errors.ShortReadvError(relpath, c_offset.start,
704
 
                            c_offset.length, actual=len(data))
705
 
            for suboffset, subsize in c_offset.ranges:
706
 
                key = (c_offset.start+suboffset, subsize)
707
 
                data_map[key] = data[suboffset:suboffset+subsize]
 
712
        try:
 
713
            for c_offset in coalesced:
 
714
                # TODO: jam 20060724 it might be faster to not issue seek if
 
715
                #       we are already at the right location. This should be
 
716
                #       benchmarked.
 
717
                fp.seek(c_offset.start)
 
718
                data = fp.read(c_offset.length)
 
719
                if len(data) < c_offset.length:
 
720
                    raise errors.ShortReadvError(relpath, c_offset.start,
 
721
                                c_offset.length, actual=len(data))
 
722
                for suboffset, subsize in c_offset.ranges:
 
723
                    key = (c_offset.start+suboffset, subsize)
 
724
                    data_map[key] = data[suboffset:suboffset+subsize]
708
725
 
709
 
            # Now that we've read some data, see if we can yield anything back
710
 
            while cur_offset_and_size in data_map:
711
 
                this_data = data_map.pop(cur_offset_and_size)
712
 
                this_offset = cur_offset_and_size[0]
713
 
                try:
714
 
                    cur_offset_and_size = offset_stack.next()
715
 
                except StopIteration:
716
 
                    # Close the file handle as there will be no more data
717
 
                    # The handle would normally be cleaned up as this code goes
718
 
                    # out of scope, but as we are a generator, not all code
719
 
                    # will re-enter once we have consumed all the expected
720
 
                    # data. For example:
721
 
                    #   zip(range(len(requests)), readv(foo, requests))
722
 
                    # Will stop because the range is done, and not run the
723
 
                    # cleanup code for the readv().
724
 
                    fp.close()
725
 
                    cur_offset_and_size = None
726
 
                yield this_offset, this_data
 
726
                # Now that we've read some data, see if we can yield anything back
 
727
                while cur_offset_and_size in data_map:
 
728
                    this_data = data_map.pop(cur_offset_and_size)
 
729
                    this_offset = cur_offset_and_size[0]
 
730
                    try:
 
731
                        cur_offset_and_size = offset_stack.next()
 
732
                    except StopIteration:
 
733
                        fp.close()
 
734
                        cur_offset_and_size = None
 
735
                    yield this_offset, this_data
 
736
        finally:
 
737
            fp.close()
727
738
 
728
739
    def _sort_expand_and_combine(self, offsets, upper_limit):
729
740
        """Helper for readv.
862
873
            yield self.get(relpath)
863
874
            count += 1
864
875
 
865
 
    def put_bytes(self, relpath, bytes, mode=None):
 
876
    def put_bytes(self, relpath, raw_bytes, mode=None):
866
877
        """Atomically put the supplied bytes into the given location.
867
878
 
868
879
        :param relpath: The location to put the contents, relative to the
869
880
            transport base.
870
 
        :param bytes: A bytestring of data.
 
881
        :param raw_bytes: A bytestring of data.
871
882
        :param mode: Create the file with the given mode.
872
883
        :return: None
873
884
        """
874
 
        if not isinstance(bytes, str):
875
 
            raise AssertionError(
876
 
                'bytes must be a plain string, not %s' % type(bytes))
877
 
        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)
878
889
 
879
 
    def put_bytes_non_atomic(self, relpath, bytes, mode=None,
 
890
    def put_bytes_non_atomic(self, relpath, raw_bytes, mode=None,
880
891
                             create_parent_dir=False,
881
892
                             dir_mode=None):
882
893
        """Copy the string into the target location.
885
896
        Transport.put_bytes_non_atomic for more information.
886
897
 
887
898
        :param relpath: The remote location to put the contents.
888
 
        :param bytes:   A string object containing the raw bytes to write into
889
 
                        the target file.
 
899
        :param raw_bytes:   A string object containing the raw bytes to write
 
900
                        into the target file.
890
901
        :param mode:    Possible access permissions for new file.
891
902
                        None means do not set remote permissions.
892
903
        :param create_parent_dir: If we cannot create the target file because
894
905
                        create it, and then try again.
895
906
        :param dir_mode: Possible access permissions for new directories.
896
907
        """
897
 
        if not isinstance(bytes, str):
898
 
            raise AssertionError(
899
 
                'bytes must be a plain string, not %s' % type(bytes))
900
 
        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,
901
912
                                 create_parent_dir=create_parent_dir,
902
913
                                 dir_mode=dir_mode)
903
914
 
1348
1359
        """
1349
1360
        if not base.endswith('/'):
1350
1361
            base += '/'
1351
 
        (self._scheme,
1352
 
         self._user, self._password,
1353
 
         self._host, self._port,
1354
 
         self._path) = self._split_url(base)
 
1362
        self._parsed_url = self._split_url(base)
1355
1363
        if _from_transport is not None:
1356
1364
            # Copy the password as it does not appear in base and will be lost
1357
1365
            # otherwise. It can appear in the _split_url above if the user
1358
1366
            # provided it on the command line. Otherwise, daughter classes will
1359
1367
            # prompt the user for one when appropriate.
1360
 
            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)
1361
1371
 
1362
 
        base = self._unsplit_url(self._scheme,
1363
 
                                 self._user, self._password,
1364
 
                                 self._host, self._port,
1365
 
                                 self._path)
 
1372
        base = str(self._parsed_url)
1366
1373
 
1367
1374
        super(ConnectedTransport, self).__init__(base)
1368
1375
        if _from_transport is None:
1370
1377
        else:
1371
1378
            self._shared_connection = _from_transport._shared_connection
1372
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
 
1373
1404
    def clone(self, offset=None):
1374
1405
        """Return a new transport with root at self.base + offset
1375
1406
 
1383
1414
 
1384
1415
    @staticmethod
1385
1416
    def _split_url(url):
1386
 
        return urlutils.parse_url(url)
 
1417
        return urlutils.URL.from_string(url)
1387
1418
 
1388
1419
    @staticmethod
1389
1420
    def _unsplit_url(scheme, user, password, host, port, path):
1390
 
        """
1391
 
        Build the full URL for the given already URL encoded path.
 
1421
        """Build the full URL for the given already URL encoded path.
1392
1422
 
1393
1423
        user, password, host and path will be quoted if they contain reserved
1394
1424
        chars.
1395
1425
 
1396
1426
        :param scheme: protocol
1397
 
 
1398
1427
        :param user: login
1399
 
 
1400
1428
        :param password: associated password
1401
 
 
1402
1429
        :param host: the server address
1403
 
 
1404
1430
        :param port: the associated port
1405
 
 
1406
1431
        :param path: the absolute path on the server
1407
1432
 
1408
1433
        :return: The corresponding URL.
1409
1434
        """
1410
 
        netloc = urllib.quote(host)
 
1435
        netloc = urlutils.quote(host)
1411
1436
        if user is not None:
1412
1437
            # Note that we don't put the password back even if we
1413
1438
            # have one so that it doesn't get accidentally
1414
1439
            # exposed.
1415
 
            netloc = '%s@%s' % (urllib.quote(user), netloc)
 
1440
            netloc = '%s@%s' % (urlutils.quote(user), netloc)
1416
1441
        if port is not None:
1417
1442
            netloc = '%s:%d' % (netloc, port)
1418
1443
        path = urlutils.escape(path)
1420
1445
 
1421
1446
    def relpath(self, abspath):
1422
1447
        """Return the local path portion from a given absolute path"""
1423
 
        scheme, user, password, host, port, path = self._split_url(abspath)
 
1448
        parsed_url = self._split_url(abspath)
1424
1449
        error = []
1425
 
        if (scheme != self._scheme):
 
1450
        if parsed_url.scheme != self._parsed_url.scheme:
1426
1451
            error.append('scheme mismatch')
1427
 
        if (user != self._user):
 
1452
        if parsed_url.user != self._parsed_url.user:
1428
1453
            error.append('user name mismatch')
1429
 
        if (host != self._host):
 
1454
        if parsed_url.host != self._parsed_url.host:
1430
1455
            error.append('host mismatch')
1431
 
        if (port != self._port):
 
1456
        if parsed_url.port != self._parsed_url.port:
1432
1457
            error.append('port mismatch')
1433
 
        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))):
1434
1460
            error.append('path mismatch')
1435
1461
        if error:
1436
1462
            extra = ', '.join(error)
1437
1463
            raise errors.PathNotChild(abspath, self.base, extra=extra)
1438
 
        pl = len(self._path)
1439
 
        return path[pl:].strip('/')
 
1464
        pl = len(self._parsed_url.path)
 
1465
        return parsed_url.path[pl:].strip('/')
1440
1466
 
1441
1467
    def abspath(self, relpath):
1442
1468
        """Return the full url to the given relative path.
1445
1471
 
1446
1472
        :returns: the Unicode version of the absolute path for relpath.
1447
1473
        """
1448
 
        relative = urlutils.unescape(relpath).encode('utf-8')
1449
 
        path = self._combine_paths(self._path, relative)
1450
 
        return self._unsplit_url(self._scheme, self._user, self._password,
1451
 
                                 self._host, self._port,
1452
 
                                 path)
 
1474
        return str(self._parsed_url.clone(relpath))
1453
1475
 
1454
1476
    def _remote_path(self, relpath):
1455
1477
        """Return the absolute path part of the url to the given relative path.
1462
1484
 
1463
1485
        :return: the absolute Unicode path on the server,
1464
1486
        """
1465
 
        relative = urlutils.unescape(relpath).encode('utf-8')
1466
 
        remote_path = self._combine_paths(self._path, relative)
1467
 
        return remote_path
 
1487
        return self._parsed_url.clone(relpath).path
1468
1488
 
1469
1489
    def _get_shared_connection(self):
1470
1490
        """Get the object shared amongst cloned transports.
1492
1512
        """
1493
1513
        self._shared_connection.connection = connection
1494
1514
        self._shared_connection.credentials = credentials
 
1515
        for hook in self.hooks["post_connect"]:
 
1516
            hook(self)
1495
1517
 
1496
1518
    def _get_connection(self):
1497
1519
        """Returns the transport specific connection object."""
1529
1551
        :return: A new transport or None if the connection cannot be shared.
1530
1552
        """
1531
1553
        try:
1532
 
            (scheme, user, password,
1533
 
             host, port, path) = self._split_url(other_base)
 
1554
            parsed_url = self._split_url(other_base)
1534
1555
        except errors.InvalidURL:
1535
1556
            # No hope in trying to reuse an existing transport for an invalid
1536
1557
            # URL
1539
1560
        transport = None
1540
1561
        # Don't compare passwords, they may be absent from other_base or from
1541
1562
        # self and they don't carry more information than user anyway.
1542
 
        if (scheme == self._scheme
1543
 
            and user == self._user
1544
 
            and host == self._host
1545
 
            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
1546
1568
            if not path.endswith('/'):
1547
1569
                # This normally occurs at __init__ time, but it's easier to do
1548
1570
                # it now to avoid creating two transports for the same base.
1549
1571
                path += '/'
1550
 
            if self._path  == path:
 
1572
            if self._parsed_url.path  == path:
1551
1573
                # shortcut, it's really the same transport
1552
1574
                return self
1553
1575
            # We don't call clone here because the intent is different: we
1564
1586
        raise NotImplementedError(self.disconnect)
1565
1587
 
1566
1588
 
1567
 
def get_transport(base, possible_transports=None):
1568
 
    """Open a transport to access a URL or directory.
1569
 
 
1570
 
    :param base: either a URL or a directory name.
1571
 
 
1572
 
    :param transports: optional reusable transports list. If not None, created
1573
 
        transports will be added to the list.
1574
 
 
1575
 
    :return: A new transport optionally sharing its connection with one of
1576
 
        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
1577
1598
    """
1578
 
    if base is None:
1579
 
        base = '.'
1580
 
    last_err = None
 
1599
    if not isinstance(location, basestring):
 
1600
        raise AssertionError("location not a byte or unicode string")
1581
1601
    from bzrlib.directory_service import directories
1582
 
    base = directories.dereference(base)
1583
 
 
1584
 
    def convert_path_to_url(base, error_str):
1585
 
        if urlutils.is_url(base):
1586
 
            # This looks like a URL, but we weren't able to
1587
 
            # instantiate it as such raise an appropriate error
1588
 
            # FIXME: we have a 'error_str' unused and we use last_err below
1589
 
            raise errors.UnsupportedProtocol(base, last_err)
1590
 
        # This doesn't look like a protocol, consider it a local path
1591
 
        new_base = urlutils.local_path_to_url(base)
1592
 
        # mutter('converting os path %r => url %s', base, new_base)
1593
 
        return new_base
 
1602
    location = directories.dereference(location)
1594
1603
 
1595
1604
    # Catch any URLs which are passing Unicode rather than ASCII
1596
1605
    try:
1597
 
        base = base.encode('ascii')
 
1606
        location = location.encode('ascii')
1598
1607
    except UnicodeError:
1599
 
        # Only local paths can be Unicode
1600
 
        base = convert_path_to_url(base,
1601
 
            'URLs must be properly escaped (protocol: %s)')
1602
 
 
 
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
    """
1603
1642
    transport = None
1604
1643
    if possible_transports is not None:
1605
1644
        for t in possible_transports:
1606
 
            t_same_connection = t._reuse_for(base)
 
1645
            t_same_connection = t._reuse_for(url)
1607
1646
            if t_same_connection is not None:
1608
1647
                # Add only new transports
1609
1648
                if t_same_connection not in possible_transports:
1610
1649
                    possible_transports.append(t_same_connection)
1611
1650
                return t_same_connection
1612
1651
 
 
1652
    last_err = None
1613
1653
    for proto, factory_list in transport_list_registry.items():
1614
 
        if proto is not None and base.startswith(proto):
1615
 
            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)
1616
1656
            if transport:
1617
1657
                if possible_transports is not None:
1618
1658
                    if transport in possible_transports:
1619
1659
                        raise AssertionError()
1620
1660
                    possible_transports.append(transport)
1621
1661
                return transport
1622
 
 
1623
 
    # We tried all the different protocols, now try one last time
1624
 
    # as a local protocol
1625
 
    base = convert_path_to_url(base, 'Unsupported protocol: %s')
1626
 
 
1627
 
    # The default handler is the filesystem handler, stored as protocol None
1628
 
    factory_list = transport_list_registry.get(None)
1629
 
    transport, last_err = _try_transport_factories(base, factory_list)
1630
 
 
1631
 
    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)
1632
1681
 
1633
1682
 
1634
1683
def _try_transport_factories(base, factory_list):
1703
1752
register_transport_proto('file://',
1704
1753
            help="Access using the standard filesystem (default)")
1705
1754
register_lazy_transport('file://', 'bzrlib.transport.local', 'LocalTransport')
1706
 
transport_list_registry.set_default_transport("file://")
1707
1755
 
1708
1756
register_transport_proto('sftp://',
1709
1757
            help="Access using SFTP (most SSH servers provide SFTP).",
1735
1783
                 help="Read-only access of branches exported on the web.")
1736
1784
register_transport_proto('https://',
1737
1785
            help="Read-only access of branches exported on the web using SSL.")
1738
 
# The default http implementation is urllib, but https is pycurl if available
 
1786
# The default http implementation is urllib
1739
1787
register_lazy_transport('http://', 'bzrlib.transport.http._pycurl',
1740
1788
                        'PyCurlTransport')
1741
1789
register_lazy_transport('http://', 'bzrlib.transport.http._urllib',
1742
1790
                        'HttpTransport_urllib')
 
1791
register_lazy_transport('https://', 'bzrlib.transport.http._pycurl',
 
1792
                        'PyCurlTransport')
1743
1793
register_lazy_transport('https://', 'bzrlib.transport.http._urllib',
1744
1794
                        'HttpTransport_urllib')
1745
 
register_lazy_transport('https://', 'bzrlib.transport.http._pycurl',
1746
 
                        'PyCurlTransport')
1747
1795
 
1748
1796
register_transport_proto('ftp://', help="Access using passive FTP.")
1749
1797
register_lazy_transport('ftp://', 'bzrlib.transport.ftp', 'FtpTransport')
1776
1824
register_lazy_transport('memory://', 'bzrlib.transport.memory',
1777
1825
                        'MemoryTransport')
1778
1826
 
1779
 
# chroots cannot be implicitly accessed, they must be explicitly created:
1780
 
register_transport_proto('chroot+')
1781
 
 
1782
1827
register_transport_proto('readonly+',
1783
1828
#              help="This modifier converts any transport to be readonly."
1784
1829
            )
1813
1858
register_lazy_transport('nosmart+', 'bzrlib.transport.nosmart',
1814
1859
                        'NoSmartTransportDecorator')
1815
1860
 
1816
 
# These two schemes were registered, but don't seem to have an actual transport
1817
 
# protocol registered
1818
 
for scheme in ['ssh', 'bzr+loopback']:
1819
 
    register_urlparse_netloc_protocol(scheme)
1820
 
del scheme
1821
 
 
1822
1861
register_transport_proto('bzr://',
1823
1862
            help="Fast access using the Bazaar smart server.",
1824
1863
                         register_netloc=True)