/brz/remove-bazaar

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/brz/remove-bazaar

« back to all changes in this revision

Viewing changes to bzrlib/transport/__init__.py

  • Committer: Robert Collins
  • Date: 2008-08-20 02:07:36 UTC
  • mfrom: (3640 +trunk)
  • mto: This revision was merged to the branch mainline in revision 3682.
  • Revision ID: robertc@robertcollins.net-20080820020736-g2xe4921zzxtymle
Merge bzr.dev

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2005, 2006 Canonical Ltd
2
 
 
 
1
# Copyright (C) 2005, 2006, 2007 Canonical Ltd
 
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
5
5
# the Free Software Foundation; either version 2 of the License, or
6
6
# (at your option) any later version.
7
 
 
 
7
#
8
8
# This program is distributed in the hope that it will be useful,
9
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11
11
# GNU General Public License for more details.
12
 
 
 
12
#
13
13
# You should have received a copy of the GNU General Public License
14
14
# along with this program; if not, write to the Free Software
15
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
26
26
it.
27
27
"""
28
28
 
29
 
import errno
30
 
from collections import deque
31
 
from copy import deepcopy
 
29
from cStringIO import StringIO
32
30
import re
33
 
from stat import *
34
31
import sys
35
 
from unittest import TestSuite
 
32
 
 
33
from bzrlib.lazy_import import lazy_import
 
34
lazy_import(globals(), """
 
35
import errno
 
36
from stat import S_ISDIR
36
37
import urllib
37
38
import urlparse
38
39
 
39
 
import bzrlib
40
 
import bzrlib.errors as errors
41
 
from bzrlib.errors import DependencyNotPresent
42
 
import bzrlib.osutils as osutils
43
 
from bzrlib.osutils import pumpfile
44
 
from bzrlib.symbol_versioning import *
45
 
from bzrlib.trace import mutter, warning
46
 
import bzrlib.urlutils as urlutils
47
 
 
48
 
# {prefix: [transport_classes]}
49
 
# Transports are inserted onto the list LIFO and tried in order; as a result
50
 
# transports provided by plugins are tried first, which is usually what we
51
 
# want.
52
 
_protocol_handlers = {
53
 
}
54
 
 
55
 
def register_transport(prefix, klass, override=DEPRECATED_PARAMETER):
56
 
    """Register a transport that can be used to open URLs
57
 
 
58
 
    Normally you should use register_lazy_transport, which defers loading the
59
 
    implementation until it's actually used, and so avoids pulling in possibly
60
 
    large implementation libraries.
61
 
    """
62
 
    # Note that this code runs very early in library setup -- trace may not be
63
 
    # working, etc.
64
 
    global _protocol_handlers
65
 
    if deprecated_passed(override):
66
 
        warn("register_transport(override) is deprecated")
67
 
    _protocol_handlers.setdefault(prefix, []).insert(0, klass)
68
 
 
69
 
 
70
 
def register_lazy_transport(scheme, module, classname):
71
 
    """Register lazy-loaded transport class.
72
 
 
73
 
    When opening a URL with the given scheme, load the module and then
74
 
    instantiate the particular class.  
75
 
 
76
 
    If the module raises DependencyNotPresent when it's imported, it is
77
 
    skipped and another implementation of the protocol is tried.  This is
78
 
    intended to be used when the implementation depends on an external
79
 
    implementation that may not be present.  If any other error is raised, it
80
 
    propagates up and the attempt to open the url fails.
81
 
    """
82
 
    # TODO: If no implementation of a protocol is available because of missing
83
 
    # dependencies, we should perhaps show the message about what dependency
84
 
    # was missing.
85
 
    def _loader(base):
86
 
        mod = __import__(module, globals(), locals(), [classname])
87
 
        klass = getattr(mod, classname)
88
 
        return klass(base)
89
 
    _loader.module = module
90
 
    register_transport(scheme, _loader)
 
40
from bzrlib import (
 
41
    errors,
 
42
    osutils,
 
43
    symbol_versioning,
 
44
    urlutils,
 
45
    )
 
46
""")
 
47
 
 
48
from bzrlib.symbol_versioning import (
 
49
        deprecated_method,
 
50
        deprecated_function,
 
51
        DEPRECATED_PARAMETER,
 
52
        one_four,
 
53
        )
 
54
from bzrlib.trace import (
 
55
    mutter,
 
56
    )
 
57
from bzrlib import registry
 
58
 
 
59
 
 
60
# a dictionary of open file streams. Keys are absolute paths, values are
 
61
# transport defined.
 
62
_file_streams = {}
91
63
 
92
64
 
93
65
def _get_protocol_handlers():
94
66
    """Return a dictionary of {urlprefix: [factory]}"""
95
 
    return _protocol_handlers
 
67
    return transport_list_registry
96
68
 
97
69
 
98
70
def _set_protocol_handlers(new_handlers):
100
72
 
101
73
    WARNING this will remove all build in protocols. Use with care.
102
74
    """
103
 
    global _protocol_handlers
104
 
    _protocol_handlers = new_handlers
 
75
    global transport_list_registry
 
76
    transport_list_registry = new_handlers
105
77
 
106
78
 
107
79
def _clear_protocol_handlers():
108
 
    global _protocol_handlers
109
 
    _protocol_handlers = {}
 
80
    global transport_list_registry
 
81
    transport_list_registry = TransportListRegistry()
110
82
 
111
83
 
112
84
def _get_transport_modules():
113
85
    """Return a list of the modules providing transports."""
114
86
    modules = set()
115
 
    for prefix, factory_list in _protocol_handlers.items():
 
87
    for prefix, factory_list in transport_list_registry.iteritems():
116
88
        for factory in factory_list:
117
 
            if factory.__module__ == "bzrlib.transport":
118
 
                # this is a lazy load transport, because no real ones
119
 
                # are directlry in bzrlib.transport
120
 
                modules.add(factory.module)
 
89
            if hasattr(factory, "_module_name"):
 
90
                modules.add(factory._module_name)
121
91
            else:
122
 
                modules.add(factory.__module__)
 
92
                modules.add(factory._obj.__module__)
 
93
    # Add chroot directly, because there is no handler registered for it.
 
94
    modules.add('bzrlib.transport.chroot')
123
95
    result = list(modules)
124
96
    result.sort()
125
97
    return result
126
98
 
127
99
 
 
100
class TransportListRegistry(registry.Registry):
 
101
    """A registry which simplifies tracking available Transports.
 
102
 
 
103
    A registration of a new protocol requires two step:
 
104
    1) register the prefix with the function register_transport( )
 
105
    2) register the protocol provider with the function
 
106
    register_transport_provider( ) ( and the "lazy" variant )
 
107
 
 
108
    This is needed because:
 
109
    a) a single provider can support multple protcol ( like the ftp
 
110
    provider which supports both the ftp:// and the aftp:// protocols )
 
111
    b) a single protocol can have multiple providers ( like the http://
 
112
    protocol which is supported by both the urllib and pycurl provider )
 
113
    """
 
114
 
 
115
    def register_transport_provider(self, key, obj):
 
116
        self.get(key).insert(0, registry._ObjectGetter(obj))
 
117
 
 
118
    def register_lazy_transport_provider(self, key, module_name, member_name):
 
119
        self.get(key).insert(0,
 
120
                registry._LazyObjectGetter(module_name, member_name))
 
121
 
 
122
    def register_transport(self, key, help=None):
 
123
        self.register(key, [], help)
 
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
 
 
130
transport_list_registry = TransportListRegistry()
 
131
 
 
132
 
 
133
def register_transport_proto(prefix, help=None, info=None,
 
134
                             register_netloc=False):
 
135
    transport_list_registry.register_transport(prefix, help)
 
136
    if register_netloc:
 
137
        if not prefix.endswith('://'):
 
138
            raise ValueError(prefix)
 
139
        register_urlparse_netloc_protocol(prefix[:-3])
 
140
 
 
141
 
 
142
def register_lazy_transport(prefix, module, classname):
 
143
    if not prefix in transport_list_registry:
 
144
        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):
 
149
    if not prefix in transport_list_registry:
 
150
        register_transport_proto(prefix)
 
151
    transport_list_registry.register_transport_provider(prefix, klass)
 
152
 
 
153
 
128
154
def register_urlparse_netloc_protocol(protocol):
129
155
    """Ensure that protocol is setup to be used with urlparse netloc parsing."""
130
156
    if protocol not in urlparse.uses_netloc:
131
157
        urlparse.uses_netloc.append(protocol)
132
158
 
133
159
 
134
 
def split_url(url):
135
 
    # TODO: jam 20060606 urls should only be ascii, or they should raise InvalidURL
136
 
    if isinstance(url, unicode):
137
 
        url = url.encode('utf-8')
138
 
    (scheme, netloc, path, params,
139
 
     query, fragment) = urlparse.urlparse(url, allow_fragments=False)
140
 
    username = password = host = port = None
141
 
    if '@' in netloc:
142
 
        username, host = netloc.split('@', 1)
143
 
        if ':' in username:
144
 
            username, password = username.split(':', 1)
145
 
            password = urllib.unquote(password)
146
 
        username = urllib.unquote(username)
147
 
    else:
148
 
        host = netloc
149
 
 
150
 
    if ':' in host:
151
 
        host, port = host.rsplit(':', 1)
152
 
        try:
153
 
            port = int(port)
154
 
        except ValueError:
155
 
            # TODO: Should this be ConnectionError?
156
 
            raise errors.TransportError('%s: invalid port number' % port)
157
 
    host = urllib.unquote(host)
158
 
 
159
 
    path = urllib.unquote(path)
160
 
 
161
 
    return (scheme, username, password, host, port, path)
 
160
def _unregister_urlparse_netloc_protocol(protocol):
 
161
    """Remove protocol from urlparse netloc parsing.
 
162
 
 
163
    Except for tests, you should never use that function. Using it with 'http',
 
164
    for example, will break all http transports.
 
165
    """
 
166
    if protocol in urlparse.uses_netloc:
 
167
        urlparse.uses_netloc.remove(protocol)
 
168
 
 
169
 
 
170
def unregister_transport(scheme, factory):
 
171
    """Unregister a transport."""
 
172
    l = transport_list_registry.get(scheme)
 
173
    for i in l:
 
174
        o = i.get_obj( )
 
175
        if o == factory:
 
176
            transport_list_registry.get(scheme).remove(i)
 
177
            break
 
178
    if len(l) == 0:
 
179
        transport_list_registry.remove(scheme)
 
180
 
 
181
 
 
182
class _CoalescedOffset(object):
 
183
    """A data container for keeping track of coalesced offsets."""
 
184
 
 
185
    __slots__ = ['start', 'length', 'ranges']
 
186
 
 
187
    def __init__(self, start, length, ranges):
 
188
        self.start = start
 
189
        self.length = length
 
190
        self.ranges = ranges
 
191
 
 
192
    def __cmp__(self, other):
 
193
        return cmp((self.start, self.length, self.ranges),
 
194
                   (other.start, other.length, other.ranges))
 
195
 
 
196
    def __repr__(self):
 
197
        return '%s(%r, %r, %r)' % (self.__class__.__name__,
 
198
            self.start, self.length, self.ranges)
 
199
 
 
200
 
 
201
class LateReadError(object):
 
202
    """A helper for transports which pretends to be a readable file.
 
203
 
 
204
    When read() is called, errors.ReadError is raised.
 
205
    """
 
206
 
 
207
    def __init__(self, path):
 
208
        self._path = path
 
209
 
 
210
    def close(self):
 
211
        """a no-op - do nothing."""
 
212
 
 
213
    def _fail(self):
 
214
        """Raise ReadError."""
 
215
        raise errors.ReadError(self._path)
 
216
 
 
217
    def __iter__(self):
 
218
        self._fail()
 
219
 
 
220
    def read(self, count=-1):
 
221
        self._fail()
 
222
 
 
223
    def readlines(self):
 
224
        self._fail()
 
225
 
 
226
 
 
227
class FileStream(object):
 
228
    """Base class for FileStreams."""
 
229
 
 
230
    def __init__(self, transport, relpath):
 
231
        """Create a FileStream for relpath on transport."""
 
232
        self.transport = transport
 
233
        self.relpath = relpath
 
234
 
 
235
    def _close(self):
 
236
        """A hook point for subclasses that need to take action on close."""
 
237
 
 
238
    def close(self):
 
239
        self._close()
 
240
        del _file_streams[self.transport.abspath(self.relpath)]
 
241
 
 
242
 
 
243
class FileFileStream(FileStream):
 
244
    """A file stream object returned by open_write_stream.
 
245
    
 
246
    This version uses a file like object to perform writes.
 
247
    """
 
248
 
 
249
    def __init__(self, transport, relpath, file_handle):
 
250
        FileStream.__init__(self, transport, relpath)
 
251
        self.file_handle = file_handle
 
252
 
 
253
    def _close(self):
 
254
        self.file_handle.close()
 
255
 
 
256
    def write(self, bytes):
 
257
        self.file_handle.write(bytes)
 
258
 
 
259
 
 
260
class AppendBasedFileStream(FileStream):
 
261
    """A file stream object returned by open_write_stream.
 
262
    
 
263
    This version uses append on a transport to perform writes.
 
264
    """
 
265
 
 
266
    def write(self, bytes):
 
267
        self.transport.append_bytes(self.relpath, bytes)
162
268
 
163
269
 
164
270
class Transport(object):
171
277
    implementations can do pipelining.
172
278
    In general implementations should support having a generator or a list
173
279
    as an argument (ie always iterate, never index)
 
280
 
 
281
    :ivar base: Base URL for the transport; should always end in a slash.
174
282
    """
175
283
 
 
284
    # implementations can override this if it is more efficient
 
285
    # for them to combine larger read chunks together
 
286
    _max_readv_combine = 50
 
287
    # It is better to read this much more data in order, rather
 
288
    # than doing another seek. Even for the local filesystem,
 
289
    # there is a benefit in just reading.
 
290
    # TODO: jam 20060714 Do some real benchmarking to figure out
 
291
    #       where the biggest benefit between combining reads and
 
292
    #       and seeking is. Consider a runtime auto-tune.
 
293
    _bytes_to_read_before_seek = 0
 
294
 
176
295
    def __init__(self, base):
177
 
        super(Transport, self).__init__()
 
296
        super(Transport, self).__init__(base=base)
178
297
        self.base = base
179
298
 
180
299
    def _translate_error(self, e, path, raise_generic=True):
182
301
 
183
302
        This handles things like ENOENT, ENOTDIR, EEXIST, and EACCESS
184
303
        """
185
 
        if hasattr(e, 'errno'):
186
 
            if e.errno in (errno.ENOENT, errno.ENOTDIR):
 
304
        if getattr(e, 'errno', None) is not None:
 
305
            if e.errno in (errno.ENOENT, errno.ENOTDIR, errno.EINVAL):
187
306
                raise errors.NoSuchFile(path, extra=e)
188
307
            # I would rather use errno.EFOO, but there doesn't seem to be
189
308
            # any matching for 267
209
328
        """
210
329
        raise NotImplementedError(self.clone)
211
330
 
212
 
    def should_cache(self):
213
 
        """Return True if the data pulled across should be cached locally.
214
 
        """
215
 
        return False
 
331
    def ensure_base(self):
 
332
        """Ensure that the directory this transport references exists.
 
333
 
 
334
        This will create a directory if it doesn't exist.
 
335
        :return: True if the directory was created, False otherwise.
 
336
        """
 
337
        # The default implementation just uses "Easier to ask for forgiveness
 
338
        # than permission". We attempt to create the directory, and just
 
339
        # suppress FileExists and PermissionDenied (for Windows) exceptions.
 
340
        try:
 
341
            self.mkdir('.')
 
342
        except (errors.FileExists, errors.PermissionDenied):
 
343
            return False
 
344
        else:
 
345
            return True
 
346
 
 
347
    def external_url(self):
 
348
        """Return a URL for self that can be given to an external process.
 
349
 
 
350
        There is no guarantee that the URL can be accessed from a different
 
351
        machine - e.g. file:/// urls are only usable on the local machine,
 
352
        sftp:/// urls when the server is only bound to localhost are only
 
353
        usable from localhost etc.
 
354
 
 
355
        NOTE: This method may remove security wrappers (e.g. on chroot
 
356
        transports) and thus should *only* be used when the result will not
 
357
        be used to obtain a new transport within bzrlib. Ideally chroot
 
358
        transports would know enough to cause the external url to be the exact
 
359
        one used that caused the chrooting in the first place, but that is not
 
360
        currently the case.
 
361
 
 
362
        :return: A URL that can be given to another process.
 
363
        :raises InProcessTransport: If the transport is one that cannot be
 
364
            accessed out of the current process (e.g. a MemoryTransport)
 
365
            then InProcessTransport is raised.
 
366
        """
 
367
        raise NotImplementedError(self.external_url)
216
368
 
217
369
    def _pump(self, from_file, to_file):
218
370
        """Most children will need to copy from one file-like 
219
371
        object or string to another one.
220
372
        This just gives them something easy to call.
221
373
        """
222
 
        if isinstance(from_file, basestring):
223
 
            to_file.write(from_file)
224
 
        else:
225
 
            pumpfile(from_file, to_file)
 
374
        return osutils.pumpfile(from_file, to_file)
226
375
 
227
376
    def _get_total(self, multi):
228
377
        """Try to figure out how many entries are in multi,
267
416
 
268
417
    def abspath(self, relpath):
269
418
        """Return the full url to the given relative path.
270
 
        This can be supplied with a string or a list
271
419
 
272
 
        XXX: Robert Collins 20051016 - is this really needed in the public
273
 
             interface ?
 
420
        :param relpath: a string of a relative path
274
421
        """
 
422
 
 
423
        # XXX: Robert Collins 20051016 - is this really needed in the public
 
424
        # interface ?
275
425
        raise NotImplementedError(self.abspath)
276
426
 
 
427
    def _combine_paths(self, base_path, relpath):
 
428
        """Transform a Transport-relative path to a remote absolute path.
 
429
 
 
430
        This does not handle substitution of ~ but does handle '..' and '.'
 
431
        components.
 
432
 
 
433
        Examples::
 
434
 
 
435
            t._combine_paths('/home/sarah', 'project/foo')
 
436
                => '/home/sarah/project/foo'
 
437
            t._combine_paths('/home/sarah', '../../etc')
 
438
                => '/etc'
 
439
            t._combine_paths('/home/sarah', '/etc')
 
440
                => '/etc'
 
441
 
 
442
        :param base_path: urlencoded path for the transport root; typically a 
 
443
             URL but need not contain scheme/host/etc.
 
444
        :param relpath: relative url string for relative part of remote path.
 
445
        :return: urlencoded string for final path.
 
446
        """
 
447
        if not isinstance(relpath, str):
 
448
            raise errors.InvalidURL(relpath)
 
449
        if relpath.startswith('/'):
 
450
            base_parts = []
 
451
        else:
 
452
            base_parts = base_path.split('/')
 
453
        if len(base_parts) > 0 and base_parts[-1] == '':
 
454
            base_parts = base_parts[:-1]
 
455
        for p in relpath.split('/'):
 
456
            if p == '..':
 
457
                if len(base_parts) == 0:
 
458
                    # In most filesystems, a request for the parent
 
459
                    # of root, just returns root.
 
460
                    continue
 
461
                base_parts.pop()
 
462
            elif p == '.':
 
463
                continue # No-op
 
464
            elif p != '':
 
465
                base_parts.append(p)
 
466
        path = '/'.join(base_parts)
 
467
        if not path.startswith('/'):
 
468
            path = '/' + path
 
469
        return path
 
470
 
 
471
    def recommended_page_size(self):
 
472
        """Return the recommended page size for this transport.
 
473
 
 
474
        This is potentially different for every path in a given namespace.
 
475
        For example, local transports might use an operating system call to 
 
476
        get the block size for a given path, which can vary due to mount
 
477
        points.
 
478
 
 
479
        :return: The page size in bytes.
 
480
        """
 
481
        return 4 * 1024
 
482
 
277
483
    def relpath(self, abspath):
278
484
        """Return the local path portion from a given absolute path.
279
485
 
295
501
        This function will only be defined for Transports which have a
296
502
        physical local filesystem representation.
297
503
        """
298
 
        # TODO: jam 20060426 Should this raise NotLocalUrl instead?
299
 
        raise errors.TransportNotPossible('This is not a LocalTransport,'
300
 
            ' so there is no local representation for a path')
 
504
        raise errors.NotLocalUrl(self.abspath(relpath))
 
505
 
301
506
 
302
507
    def has(self, relpath):
303
508
        """Does the file relpath exist?
304
509
        
305
510
        Note that some transports MAY allow querying on directories, but this
306
511
        is not part of the protocol.  In other words, the results of 
307
 
        t.has("a_directory_name") are undefined."
 
512
        t.has("a_directory_name") are undefined.
 
513
 
 
514
        :rtype: bool
308
515
        """
309
516
        raise NotImplementedError(self.has)
310
517
 
330
537
        *NOTE*: This only lists *files*, not subdirectories!
331
538
        
332
539
        As with other listing functions, only some transports implement this,.
333
 
        you may check via is_listable to determine if it will.
 
540
        you may check via listable() to determine if it will.
334
541
        """
335
542
        raise errors.TransportNotPossible("This transport has not "
336
543
                                          "implemented iter_files_recursive "
340
547
    def get(self, relpath):
341
548
        """Get the file at the given relative path.
342
549
 
 
550
        This may fail in a number of ways:
 
551
         - HTTP servers may return content for a directory. (unexpected
 
552
           content failure)
 
553
         - FTP servers may indicate NoSuchFile for a directory.
 
554
         - SFTP servers may give a file handle for a directory that will
 
555
           fail on read().
 
556
 
 
557
        For correct use of the interface, be sure to catch errors.PathError
 
558
        when calling it and catch errors.ReadError when reading from the
 
559
        returned object.
 
560
 
343
561
        :param relpath: The relative path to the file
 
562
        :rtype: File-like object.
344
563
        """
345
564
        raise NotImplementedError(self.get)
346
565
 
347
 
    def readv(self, relpath, offsets):
348
 
        """Get parts of the file at the given relative path.
349
 
 
350
 
        :offsets: A list of (offset, size) tuples.
351
 
        :return: A list or generator of (offset, data) tuples
352
 
        """
353
 
        def do_combined_read(combined_offsets):
354
 
            total_size = 0
355
 
            for offset, size in combined_offsets:
356
 
                total_size += size
357
 
            mutter('readv coalesced %d reads.', len(combined_offsets))
358
 
            offset = combined_offsets[0][0]
359
 
            fp.seek(offset)
360
 
            data = fp.read(total_size)
361
 
            pos = 0
362
 
            for offset, size in combined_offsets:
363
 
                yield offset, data[pos:pos + size]
364
 
                pos += size
365
 
 
366
 
        if not len(offsets):
 
566
    def get_bytes(self, relpath):
 
567
        """Get a raw string of the bytes for a file at the given location.
 
568
 
 
569
        :param relpath: The relative path to the file
 
570
        """
 
571
        return self.get(relpath).read()
 
572
 
 
573
    @deprecated_method(one_four)
 
574
    def get_smart_client(self):
 
575
        """Return a smart client for this transport if possible.
 
576
 
 
577
        A smart client doesn't imply the presence of a smart server: it implies
 
578
        that the smart protocol can be tunnelled via this transport.
 
579
 
 
580
        :raises NoSmartServer: if no smart server client is available.
 
581
        """
 
582
        raise errors.NoSmartServer(self.base)
 
583
 
 
584
    def get_smart_medium(self):
 
585
        """Return a smart client medium for this transport if possible.
 
586
 
 
587
        A smart medium doesn't imply the presence of a smart server: it implies
 
588
        that the smart protocol can be tunnelled via this transport.
 
589
 
 
590
        :raises NoSmartMedium: if no smart server medium is available.
 
591
        """
 
592
        raise errors.NoSmartMedium(self)
 
593
 
 
594
    @deprecated_method(one_four)
 
595
    def get_shared_medium(self):
 
596
        """Return a smart client shared medium for this transport if possible.
 
597
 
 
598
        A smart medium doesn't imply the presence of a smart server: it implies
 
599
        that the smart protocol can be tunnelled via this transport.
 
600
 
 
601
        :raises NoSmartMedium: if no smart server medium is available.
 
602
        """
 
603
        raise errors.NoSmartMedium(self)
 
604
 
 
605
    def readv(self, relpath, offsets, adjust_for_latency=False,
 
606
        upper_limit=None):
 
607
        """Get parts of the file at the given relative path.
 
608
 
 
609
        :param relpath: The path to read data from.
 
610
        :param offsets: A list of (offset, size) tuples.
 
611
        :param adjust_for_latency: Adjust the requested offsets to accomodate
 
612
            transport latency. This may re-order the offsets, expand them to
 
613
            grab adjacent data when there is likely a high cost to requesting
 
614
            data relative to delivering it.
 
615
        :param upper_limit: When adjust_for_latency is True setting upper_limit
 
616
            allows the caller to tell the transport about the length of the
 
617
            file, so that requests are not issued for ranges beyond the end of
 
618
            the file. This matters because some servers and/or transports error
 
619
            in such a case rather than just satisfying the available ranges.
 
620
            upper_limit should always be provided when adjust_for_latency is
 
621
            True, and should be the size of the file in bytes.
 
622
        :return: A list or generator of (offset, data) tuples
 
623
        """
 
624
        if adjust_for_latency:
 
625
            # Design note: We may wish to have different algorithms for the
 
626
            # expansion of the offsets per-transport. E.g. for local disk to
 
627
            # use page-aligned expansion. If that is the case consider the
 
628
            # following structure:
 
629
            #  - a test that transport.readv uses self._offset_expander or some
 
630
            #    similar attribute, to do the expansion
 
631
            #  - a test for each transport that it has some known-good offset
 
632
            #    expander
 
633
            #  - unit tests for each offset expander
 
634
            #  - a set of tests for the offset expander interface, giving
 
635
            #    baseline behaviour (which the current transport
 
636
            #    adjust_for_latency tests could be repurposed to).
 
637
            offsets = self._sort_expand_and_combine(offsets, upper_limit)
 
638
        return self._readv(relpath, offsets)
 
639
 
 
640
    def _readv(self, relpath, offsets):
 
641
        """Get parts of the file at the given relative path.
 
642
 
 
643
        :param relpath: The path to read.
 
644
        :param offsets: A list of (offset, size) tuples.
 
645
        :return: A list or generator of (offset, data) tuples
 
646
        """
 
647
        if not offsets:
367
648
            return
 
649
 
368
650
        fp = self.get(relpath)
369
 
        pending_offsets = deque(offsets)
370
 
        combined_offsets = []
371
 
        while len(pending_offsets):
372
 
            offset, size = pending_offsets.popleft()
373
 
            if not combined_offsets:
374
 
                combined_offsets = [[offset, size]]
 
651
        return self._seek_and_read(fp, offsets, relpath)
 
652
 
 
653
    def _seek_and_read(self, fp, offsets, relpath='<unknown>'):
 
654
        """An implementation of readv that uses fp.seek and fp.read.
 
655
 
 
656
        This uses _coalesce_offsets to issue larger reads and fewer seeks.
 
657
 
 
658
        :param fp: A file-like object that supports seek() and read(size)
 
659
        :param offsets: A list of offsets to be read from the given file.
 
660
        :return: yield (pos, data) tuples for each request
 
661
        """
 
662
        # We are going to iterate multiple times, we need a list
 
663
        offsets = list(offsets)
 
664
        sorted_offsets = sorted(offsets)
 
665
 
 
666
        # turn the list of offsets into a stack
 
667
        offset_stack = iter(offsets)
 
668
        cur_offset_and_size = offset_stack.next()
 
669
        coalesced = self._coalesce_offsets(sorted_offsets,
 
670
                               limit=self._max_readv_combine,
 
671
                               fudge_factor=self._bytes_to_read_before_seek)
 
672
 
 
673
        # Cache the results, but only until they have been fulfilled
 
674
        data_map = {}
 
675
        for c_offset in coalesced:
 
676
            # TODO: jam 20060724 it might be faster to not issue seek if 
 
677
            #       we are already at the right location. This should be
 
678
            #       benchmarked.
 
679
            fp.seek(c_offset.start)
 
680
            data = fp.read(c_offset.length)
 
681
            if len(data) < c_offset.length:
 
682
                raise errors.ShortReadvError(relpath, c_offset.start,
 
683
                            c_offset.length, actual=len(data))
 
684
            for suboffset, subsize in c_offset.ranges:
 
685
                key = (c_offset.start+suboffset, subsize)
 
686
                data_map[key] = data[suboffset:suboffset+subsize]
 
687
 
 
688
            # Now that we've read some data, see if we can yield anything back
 
689
            while cur_offset_and_size in data_map:
 
690
                this_data = data_map.pop(cur_offset_and_size)
 
691
                yield cur_offset_and_size[0], this_data
 
692
                cur_offset_and_size = offset_stack.next()
 
693
 
 
694
    def _sort_expand_and_combine(self, offsets, upper_limit):
 
695
        """Helper for readv.
 
696
 
 
697
        :param offsets: A readv vector - (offset, length) tuples.
 
698
        :param upper_limit: The highest byte offset that may be requested.
 
699
        :return: A readv vector that will read all the regions requested by
 
700
            offsets, in start-to-end order, with no duplicated regions,
 
701
            expanded by the transports recommended page size.
 
702
        """
 
703
        offsets = sorted(offsets)
 
704
        # short circuit empty requests
 
705
        if len(offsets) == 0:
 
706
            def empty_yielder():
 
707
                # Quick thunk to stop this function becoming a generator
 
708
                # itself, rather we return a generator that has nothing to
 
709
                # yield.
 
710
                if False:
 
711
                    yield None
 
712
            return empty_yielder()
 
713
        # expand by page size at either end
 
714
        maximum_expansion = self.recommended_page_size()
 
715
        new_offsets = []
 
716
        for offset, length in offsets:
 
717
            expansion = maximum_expansion - length
 
718
            if expansion < 0:
 
719
                # we're asking for more than the minimum read anyway.
 
720
                expansion = 0
 
721
            reduction = expansion / 2
 
722
            new_offset = offset - reduction
 
723
            new_length = length + expansion
 
724
            if new_offset < 0:
 
725
                # don't ask for anything < 0
 
726
                new_offset = 0
 
727
            if (upper_limit is not None and
 
728
                new_offset + new_length > upper_limit):
 
729
                new_length = upper_limit - new_offset
 
730
            new_offsets.append((new_offset, new_length))
 
731
        # combine the expanded offsets
 
732
        offsets = []
 
733
        current_offset, current_length = new_offsets[0]
 
734
        current_finish = current_length + current_offset
 
735
        for offset, length in new_offsets[1:]:
 
736
            finish = offset + length
 
737
            if offset > current_finish:
 
738
                # there is a gap, output the current accumulator and start
 
739
                # a new one for the region we're examining.
 
740
                offsets.append((current_offset, current_length))
 
741
                current_offset = offset
 
742
                current_length = length
 
743
                current_finish = finish
 
744
                continue
 
745
            if finish > current_finish:
 
746
                # extend the current accumulator to the end of the region
 
747
                # we're examining.
 
748
                current_finish = finish
 
749
                current_length = finish - current_offset
 
750
        offsets.append((current_offset, current_length))
 
751
        return offsets
 
752
 
 
753
    @staticmethod
 
754
    def _coalesce_offsets(offsets, limit=0, fudge_factor=0, max_size=0):
 
755
        """Yield coalesced offsets.
 
756
 
 
757
        With a long list of neighboring requests, combine them
 
758
        into a single large request, while retaining the original
 
759
        offsets.
 
760
        Turns  [(15, 10), (25, 10)] => [(15, 20, [(0, 10), (10, 10)])]
 
761
 
 
762
        :param offsets: A list of (start, length) pairs
 
763
 
 
764
        :param limit: Only combine a maximum of this many pairs Some transports
 
765
                penalize multiple reads more than others, and sometimes it is
 
766
                better to return early.
 
767
                0 means no limit
 
768
 
 
769
        :param fudge_factor: All transports have some level of 'it is
 
770
                better to read some more data and throw it away rather 
 
771
                than seek', so collapse if we are 'close enough'
 
772
 
 
773
        :param max_size: Create coalesced offsets no bigger than this size.
 
774
                When a single offset is bigger than 'max_size', it will keep
 
775
                its size and be alone in the coalesced offset.
 
776
                0 means no maximum size.
 
777
 
 
778
        :return: yield _CoalescedOffset objects, which have members for where
 
779
                to start, how much to read, and how to split those 
 
780
                chunks back up
 
781
        """
 
782
        last_end = None
 
783
        cur = _CoalescedOffset(None, None, [])
 
784
 
 
785
        for start, size in offsets:
 
786
            end = start + size
 
787
            if (last_end is not None
 
788
                and start <= last_end + fudge_factor
 
789
                and start >= cur.start
 
790
                and (limit <= 0 or len(cur.ranges) < limit)
 
791
                and (max_size <= 0 or end - cur.start <= max_size)):
 
792
                cur.length = end - cur.start
 
793
                cur.ranges.append((start-cur.start, size))
375
794
            else:
376
 
                if (len (combined_offsets) < 50 and
377
 
                    combined_offsets[-1][0] + combined_offsets[-1][1] == offset):
378
 
                    # combatible offset:
379
 
                    combined_offsets.append([offset, size])
380
 
                else:
381
 
                    # incompatible, or over the threshold issue a read and yield
382
 
                    pending_offsets.appendleft((offset, size))
383
 
                    for result in do_combined_read(combined_offsets):
384
 
                        yield result
385
 
                    combined_offsets = []
386
 
        # whatever is left is a single coalesced request
387
 
        if len(combined_offsets):
388
 
            for result in do_combined_read(combined_offsets):
389
 
                yield result
 
795
                if cur.start is not None:
 
796
                    yield cur
 
797
                cur = _CoalescedOffset(start, size, [(0, size)])
 
798
            last_end = end
 
799
 
 
800
        if cur.start is not None:
 
801
            yield cur
 
802
 
 
803
        return
390
804
 
391
805
    def get_multi(self, relpaths, pb=None):
392
806
        """Get a list of file-like objects, one for each entry in relpaths.
405
819
            yield self.get(relpath)
406
820
            count += 1
407
821
 
408
 
    def put(self, relpath, f, mode=None):
409
 
        """Copy the file-like or string object into the location.
 
822
    def put_bytes(self, relpath, bytes, mode=None):
 
823
        """Atomically put the supplied bytes into the given location.
 
824
 
 
825
        :param relpath: The location to put the contents, relative to the
 
826
            transport base.
 
827
        :param bytes: A bytestring of data.
 
828
        :param mode: Create the file with the given mode.
 
829
        :return: None
 
830
        """
 
831
        if not isinstance(bytes, str):
 
832
            raise AssertionError(
 
833
                'bytes must be a plain string, not %s' % type(bytes))
 
834
        return self.put_file(relpath, StringIO(bytes), mode=mode)
 
835
 
 
836
    def put_bytes_non_atomic(self, relpath, bytes, mode=None,
 
837
                             create_parent_dir=False,
 
838
                             dir_mode=None):
 
839
        """Copy the string into the target location.
 
840
 
 
841
        This function is not strictly safe to use. See 
 
842
        Transport.put_bytes_non_atomic for more information.
 
843
 
 
844
        :param relpath: The remote location to put the contents.
 
845
        :param bytes:   A string object containing the raw bytes to write into
 
846
                        the target file.
 
847
        :param mode:    Possible access permissions for new file.
 
848
                        None means do not set remote permissions.
 
849
        :param create_parent_dir: If we cannot create the target file because
 
850
                        the parent directory does not exist, go ahead and
 
851
                        create it, and then try again.
 
852
        :param dir_mode: Possible access permissions for new directories.
 
853
        """
 
854
        if not isinstance(bytes, str):
 
855
            raise AssertionError(
 
856
                'bytes must be a plain string, not %s' % type(bytes))
 
857
        self.put_file_non_atomic(relpath, StringIO(bytes), mode=mode,
 
858
                                 create_parent_dir=create_parent_dir,
 
859
                                 dir_mode=dir_mode)
 
860
 
 
861
    def put_file(self, relpath, f, mode=None):
 
862
        """Copy the file-like object into the location.
410
863
 
411
864
        :param relpath: Location to put the contents, relative to base.
412
 
        :param f:       File-like or string object.
413
 
        :param mode: The mode for the newly created file, 
414
 
                     None means just use the default
415
 
        """
416
 
        raise NotImplementedError(self.put)
417
 
 
418
 
    def put_multi(self, files, mode=None, pb=None):
419
 
        """Put a set of files into the location.
420
 
 
421
 
        :param files: A list of tuples of relpath, file object [(path1, file1), (path2, file2),...]
422
 
        :param pb:  An optional ProgressBar for indicating percent done.
423
 
        :param mode: The mode for the newly created files
424
 
        :return: The number of files copied.
425
 
        """
426
 
        def put(path, f):
427
 
            self.put(path, f, mode=mode)
428
 
        return len(self._iterate_over(files, put, pb, 'put', expand=True))
 
865
        :param f:       File-like object.
 
866
        :param mode: The mode for the newly created file,
 
867
                     None means just use the default.
 
868
        :return: The length of the file that was written.
 
869
        """
 
870
        # We would like to mark this as NotImplemented, but most likely
 
871
        # transports have defined it in terms of the old api.
 
872
        symbol_versioning.warn('Transport %s should implement put_file,'
 
873
                               ' rather than implementing put() as of'
 
874
                               ' version 0.11.'
 
875
                               % (self.__class__.__name__,),
 
876
                               DeprecationWarning)
 
877
        return self.put(relpath, f, mode=mode)
 
878
        #raise NotImplementedError(self.put_file)
 
879
 
 
880
    def put_file_non_atomic(self, relpath, f, mode=None,
 
881
                            create_parent_dir=False,
 
882
                            dir_mode=None):
 
883
        """Copy the file-like object into the target location.
 
884
 
 
885
        This function is not strictly safe to use. It is only meant to
 
886
        be used when you already know that the target does not exist.
 
887
        It is not safe, because it will open and truncate the remote
 
888
        file. So there may be a time when the file has invalid contents.
 
889
 
 
890
        :param relpath: The remote location to put the contents.
 
891
        :param f:       File-like object.
 
892
        :param mode:    Possible access permissions for new file.
 
893
                        None means do not set remote permissions.
 
894
        :param create_parent_dir: If we cannot create the target file because
 
895
                        the parent directory does not exist, go ahead and
 
896
                        create it, and then try again.
 
897
        :param dir_mode: Possible access permissions for new directories.
 
898
        """
 
899
        # Default implementation just does an atomic put.
 
900
        try:
 
901
            return self.put_file(relpath, f, mode=mode)
 
902
        except errors.NoSuchFile:
 
903
            if not create_parent_dir:
 
904
                raise
 
905
            parent_dir = osutils.dirname(relpath)
 
906
            if parent_dir:
 
907
                self.mkdir(parent_dir, mode=dir_mode)
 
908
                return self.put_file(relpath, f, mode=mode)
429
909
 
430
910
    def mkdir(self, relpath, mode=None):
431
911
        """Create a directory at the given path."""
437
917
            self.mkdir(path, mode=mode)
438
918
        return len(self._iterate_over(relpaths, mkdir, pb, 'mkdir', expand=False))
439
919
 
440
 
    def append(self, relpath, f):
441
 
        """Append the text in the file-like or string object to 
442
 
        the supplied location.
443
 
 
444
 
        returns the length of f before the content was written to it.
445
 
        """
446
 
        raise NotImplementedError(self.append)
 
920
    def open_write_stream(self, relpath, mode=None):
 
921
        """Open a writable file stream at relpath.
 
922
 
 
923
        A file stream is a file like object with a write() method that accepts
 
924
        bytes to write.. Buffering may occur internally until the stream is
 
925
        closed with stream.close().  Calls to readv or the get_* methods will
 
926
        be synchronised with any internal buffering that may be present.
 
927
 
 
928
        :param relpath: The relative path to the file.
 
929
        :param mode: The mode for the newly created file, 
 
930
                     None means just use the default
 
931
        :return: A FileStream. FileStream objects have two methods, write() and
 
932
            close(). There is no guarantee that data is committed to the file
 
933
            if close() has not been called (even if get() is called on the same
 
934
            path).
 
935
        """
 
936
        raise NotImplementedError(self.open_write_stream)
 
937
 
 
938
    def append_file(self, relpath, f, mode=None):
 
939
        """Append bytes from a file-like object to a file at relpath.
 
940
 
 
941
        The file is created if it does not already exist.
 
942
 
 
943
        :param f: a file-like object of the bytes to append.
 
944
        :param mode: Unix mode for newly created files.  This is not used for
 
945
            existing files.
 
946
 
 
947
        :returns: the length of relpath before the content was written to it.
 
948
        """
 
949
        symbol_versioning.warn('Transport %s should implement append_file,'
 
950
                               ' rather than implementing append() as of'
 
951
                               ' version 0.11.'
 
952
                               % (self.__class__.__name__,),
 
953
                               DeprecationWarning)
 
954
        return self.append(relpath, f, mode=mode)
 
955
 
 
956
    def append_bytes(self, relpath, bytes, mode=None):
 
957
        """Append bytes to a file at relpath.
 
958
 
 
959
        The file is created if it does not already exist.
 
960
 
 
961
        :type f: str
 
962
        :param f: a string of the bytes to append.
 
963
        :param mode: Unix mode for newly created files.  This is not used for
 
964
            existing files.
 
965
 
 
966
        :returns: the length of relpath before the content was written to it.
 
967
        """
 
968
        if not isinstance(bytes, str):
 
969
            raise TypeError(
 
970
                'bytes must be a plain string, not %s' % type(bytes))
 
971
        return self.append_file(relpath, StringIO(bytes), mode=mode)
447
972
 
448
973
    def append_multi(self, files, pb=None):
449
974
        """Append the text in each file-like or string object to
452
977
        :param files: A set of (path, f) entries
453
978
        :param pb:  An optional ProgressBar for indicating percent done.
454
979
        """
455
 
        return self._iterate_over(files, self.append, pb, 'append', expand=True)
 
980
        return self._iterate_over(files, self.append_file, pb, 'append', expand=True)
456
981
 
457
982
    def copy(self, rel_from, rel_to):
458
983
        """Copy the item at rel_from to the location at rel_to.
460
985
        Override this for efficiency if a specific transport can do it 
461
986
        faster than this default implementation.
462
987
        """
463
 
        self.put(rel_to, self.get(rel_from))
 
988
        self.put_file(rel_to, self.get(rel_from))
464
989
 
465
990
    def copy_multi(self, relpaths, pb=None):
466
991
        """Copy a bunch of entries.
481
1006
        """
482
1007
        # The dummy implementation just does a simple get + put
483
1008
        def copy_entry(path):
484
 
            other.put(path, self.get(path), mode=mode)
 
1009
            other.put_file(path, self.get(path), mode=mode)
485
1010
 
486
1011
        return len(self._iterate_over(relpaths, copy_entry, pb, 'copy_to', expand=False))
487
1012
 
635
1160
        WARNING: many transports do not support this, so trying avoid using
636
1161
        it if at all possible.
637
1162
        """
638
 
        raise errors.TransportNotPossible("This transport has not "
 
1163
        raise errors.TransportNotPossible("Transport %r has not "
639
1164
                                          "implemented list_dir "
640
1165
                                          "(but must claim to be listable "
641
 
                                          "to trigger this error).")
 
1166
                                          "to trigger this error)."
 
1167
                                          % (self))
642
1168
 
643
1169
    def lock_read(self, relpath):
644
1170
        """Lock the given file for shared (read) access.
645
 
        WARNING: many transports do not support this, so trying avoid using it
 
1171
 
 
1172
        WARNING: many transports do not support this, so trying avoid using it.
 
1173
        These methods may be removed in the future.
 
1174
 
 
1175
        Transports may raise TransportNotPossible if OS-level locks cannot be
 
1176
        taken over this transport.  
646
1177
 
647
1178
        :return: A lock object, which should contain an unlock() function.
648
1179
        """
649
 
        raise NotImplementedError(self.lock_read)
 
1180
        raise errors.TransportNotPossible("transport locks not supported on %s" % self)
650
1181
 
651
1182
    def lock_write(self, relpath):
652
1183
        """Lock the given file for exclusive (write) access.
653
 
        WARNING: many transports do not support this, so trying avoid using it
 
1184
 
 
1185
        WARNING: many transports do not support this, so trying avoid using it.
 
1186
        These methods may be removed in the future.
 
1187
 
 
1188
        Transports may raise TransportNotPossible if OS-level locks cannot be
 
1189
        taken over this transport.
654
1190
 
655
1191
        :return: A lock object, which should contain an unlock() function.
656
1192
        """
657
 
        raise NotImplementedError(self.lock_write)
 
1193
        raise errors.TransportNotPossible("transport locks not supported on %s" % self)
658
1194
 
659
1195
    def is_readonly(self):
660
1196
        """Return true if this connection cannot be written to."""
676
1212
        # several questions about the transport.
677
1213
        return False
678
1214
 
679
 
 
680
 
# jam 20060426 For compatibility we copy the functions here
681
 
# TODO: The should be marked as deprecated
682
 
urlescape = urlutils.escape
683
 
urlunescape = urlutils.unescape
684
 
_urlRE = re.compile(r'^(?P<proto>[^:/\\]+)://(?P<path>.*)$')
685
 
 
686
 
 
687
 
def get_transport(base):
 
1215
    def _reuse_for(self, other_base):
 
1216
        # This is really needed for ConnectedTransport only, but it's easier to
 
1217
        # have Transport refuses to be reused than testing that the reuse
 
1218
        # should be asked to ConnectedTransport only.
 
1219
        return None
 
1220
 
 
1221
 
 
1222
class _SharedConnection(object):
 
1223
    """A connection shared between several transports."""
 
1224
 
 
1225
    def __init__(self, connection=None, credentials=None, base=None):
 
1226
        """Constructor.
 
1227
 
 
1228
        :param connection: An opaque object specific to each transport.
 
1229
 
 
1230
        :param credentials: An opaque object containing the credentials used to
 
1231
            create the connection.
 
1232
        """
 
1233
        self.connection = connection
 
1234
        self.credentials = credentials
 
1235
        self.base = base
 
1236
 
 
1237
 
 
1238
class ConnectedTransport(Transport):
 
1239
    """A transport connected to a remote server.
 
1240
 
 
1241
    This class provide the basis to implement transports that need to connect
 
1242
    to a remote server.
 
1243
 
 
1244
    Host and credentials are available as private attributes, cloning preserves
 
1245
    them and share the underlying, protocol specific, connection.
 
1246
    """
 
1247
 
 
1248
    def __init__(self, base, _from_transport=None):
 
1249
        """Constructor.
 
1250
 
 
1251
        The caller should ensure that _from_transport points at the same host
 
1252
        as the new base.
 
1253
 
 
1254
        :param base: transport root URL
 
1255
 
 
1256
        :param _from_transport: optional transport to build from. The built
 
1257
            transport will share the connection with this transport.
 
1258
        """
 
1259
        if not base.endswith('/'):
 
1260
            base += '/'
 
1261
        (self._scheme,
 
1262
         self._user, self._password,
 
1263
         self._host, self._port,
 
1264
         self._path) = self._split_url(base)
 
1265
        if _from_transport is not None:
 
1266
            # Copy the password as it does not appear in base and will be lost
 
1267
            # otherwise. It can appear in the _split_url above if the user
 
1268
            # provided it on the command line. Otherwise, daughter classes will
 
1269
            # prompt the user for one when appropriate.
 
1270
            self._password = _from_transport._password
 
1271
 
 
1272
        base = self._unsplit_url(self._scheme,
 
1273
                                 self._user, self._password,
 
1274
                                 self._host, self._port,
 
1275
                                 self._path)
 
1276
 
 
1277
        super(ConnectedTransport, self).__init__(base)
 
1278
        if _from_transport is None:
 
1279
            self._shared_connection = _SharedConnection()
 
1280
        else:
 
1281
            self._shared_connection = _from_transport._shared_connection
 
1282
 
 
1283
    def clone(self, offset=None):
 
1284
        """Return a new transport with root at self.base + offset
 
1285
 
 
1286
        We leave the daughter classes take advantage of the hint
 
1287
        that it's a cloning not a raw creation.
 
1288
        """
 
1289
        if offset is None:
 
1290
            return self.__class__(self.base, _from_transport=self)
 
1291
        else:
 
1292
            return self.__class__(self.abspath(offset), _from_transport=self)
 
1293
 
 
1294
    @staticmethod
 
1295
    def _split_url(url):
 
1296
        """
 
1297
        Extract the server address, the credentials and the path from the url.
 
1298
 
 
1299
        user, password, host and path should be quoted if they contain reserved
 
1300
        chars.
 
1301
 
 
1302
        :param url: an quoted url
 
1303
 
 
1304
        :return: (scheme, user, password, host, port, path) tuple, all fields
 
1305
            are unquoted.
 
1306
        """
 
1307
        if isinstance(url, unicode):
 
1308
            raise errors.InvalidURL('should be ascii:\n%r' % url)
 
1309
        url = url.encode('utf-8')
 
1310
        (scheme, netloc, path, params,
 
1311
         query, fragment) = urlparse.urlparse(url, allow_fragments=False)
 
1312
        user = password = host = port = None
 
1313
        if '@' in netloc:
 
1314
            user, host = netloc.rsplit('@', 1)
 
1315
            if ':' in user:
 
1316
                user, password = user.split(':', 1)
 
1317
                password = urllib.unquote(password)
 
1318
            user = urllib.unquote(user)
 
1319
        else:
 
1320
            host = netloc
 
1321
 
 
1322
        if ':' in host:
 
1323
            host, port = host.rsplit(':', 1)
 
1324
            try:
 
1325
                port = int(port)
 
1326
            except ValueError:
 
1327
                raise errors.InvalidURL('invalid port number %s in url:\n%s' %
 
1328
                                        (port, url))
 
1329
        if host == '':
 
1330
            raise errors.InvalidURL('Host empty in: %s' % url)
 
1331
 
 
1332
        host = urllib.unquote(host)
 
1333
        path = urllib.unquote(path)
 
1334
 
 
1335
        return (scheme, user, password, host, port, path)
 
1336
 
 
1337
    @staticmethod
 
1338
    def _unsplit_url(scheme, user, password, host, port, path):
 
1339
        """
 
1340
        Build the full URL for the given already URL encoded path.
 
1341
 
 
1342
        user, password, host and path will be quoted if they contain reserved
 
1343
        chars.
 
1344
 
 
1345
        :param scheme: protocol
 
1346
 
 
1347
        :param user: login
 
1348
 
 
1349
        :param password: associated password
 
1350
 
 
1351
        :param host: the server address
 
1352
 
 
1353
        :param port: the associated port
 
1354
 
 
1355
        :param path: the absolute path on the server
 
1356
 
 
1357
        :return: The corresponding URL.
 
1358
        """
 
1359
        netloc = urllib.quote(host)
 
1360
        if user is not None:
 
1361
            # Note that we don't put the password back even if we
 
1362
            # have one so that it doesn't get accidentally
 
1363
            # exposed.
 
1364
            netloc = '%s@%s' % (urllib.quote(user), netloc)
 
1365
        if port is not None:
 
1366
            netloc = '%s:%d' % (netloc, port)
 
1367
        path = urllib.quote(path)
 
1368
        return urlparse.urlunparse((scheme, netloc, path, None, None, None))
 
1369
 
 
1370
    def relpath(self, abspath):
 
1371
        """Return the local path portion from a given absolute path"""
 
1372
        scheme, user, password, host, port, path = self._split_url(abspath)
 
1373
        error = []
 
1374
        if (scheme != self._scheme):
 
1375
            error.append('scheme mismatch')
 
1376
        if (user != self._user):
 
1377
            error.append('user name mismatch')
 
1378
        if (host != self._host):
 
1379
            error.append('host mismatch')
 
1380
        if (port != self._port):
 
1381
            error.append('port mismatch')
 
1382
        if not (path == self._path[:-1] or path.startswith(self._path)):
 
1383
            error.append('path mismatch')
 
1384
        if error:
 
1385
            extra = ', '.join(error)
 
1386
            raise errors.PathNotChild(abspath, self.base, extra=extra)
 
1387
        pl = len(self._path)
 
1388
        return path[pl:].strip('/')
 
1389
 
 
1390
    def abspath(self, relpath):
 
1391
        """Return the full url to the given relative path.
 
1392
        
 
1393
        :param relpath: the relative path urlencoded
 
1394
 
 
1395
        :returns: the Unicode version of the absolute path for relpath.
 
1396
        """
 
1397
        relative = urlutils.unescape(relpath).encode('utf-8')
 
1398
        path = self._combine_paths(self._path, relative)
 
1399
        return self._unsplit_url(self._scheme, self._user, self._password,
 
1400
                                 self._host, self._port,
 
1401
                                 path)
 
1402
 
 
1403
    def _remote_path(self, relpath):
 
1404
        """Return the absolute path part of the url to the given relative path.
 
1405
 
 
1406
        This is the path that the remote server expect to receive in the
 
1407
        requests, daughter classes should redefine this method if needed and
 
1408
        use the result to build their requests.
 
1409
 
 
1410
        :param relpath: the path relative to the transport base urlencoded.
 
1411
 
 
1412
        :return: the absolute Unicode path on the server,
 
1413
        """
 
1414
        relative = urlutils.unescape(relpath).encode('utf-8')
 
1415
        remote_path = self._combine_paths(self._path, relative)
 
1416
        return remote_path
 
1417
 
 
1418
    def _get_shared_connection(self):
 
1419
        """Get the object shared amongst cloned transports.
 
1420
 
 
1421
        This should be used only by classes that needs to extend the sharing
 
1422
        with objects other than transports.
 
1423
 
 
1424
        Use _get_connection to get the connection itself.
 
1425
        """
 
1426
        return self._shared_connection
 
1427
 
 
1428
    def _set_connection(self, connection, credentials=None):
 
1429
        """Record a newly created connection with its associated credentials.
 
1430
 
 
1431
        Note: To ensure that connection is still shared after a temporary
 
1432
        failure and a new one needs to be created, daughter classes should
 
1433
        always call this method to set the connection and do so each time a new
 
1434
        connection is created.
 
1435
 
 
1436
        :param connection: An opaque object representing the connection used by
 
1437
            the daughter class.
 
1438
 
 
1439
        :param credentials: An opaque object representing the credentials
 
1440
            needed to create the connection.
 
1441
        """
 
1442
        self._shared_connection.connection = connection
 
1443
        self._shared_connection.credentials = credentials
 
1444
 
 
1445
    def _get_connection(self):
 
1446
        """Returns the transport specific connection object."""
 
1447
        return self._shared_connection.connection
 
1448
 
 
1449
    def _get_credentials(self):
 
1450
        """Returns the credentials used to establish the connection."""
 
1451
        return self._shared_connection.credentials
 
1452
 
 
1453
    def _update_credentials(self, credentials):
 
1454
        """Update the credentials of the current connection.
 
1455
 
 
1456
        Some protocols can renegociate the credentials within a connection,
 
1457
        this method allows daughter classes to share updated credentials.
 
1458
        
 
1459
        :param credentials: the updated credentials.
 
1460
        """
 
1461
        # We don't want to call _set_connection here as we are only updating
 
1462
        # the credentials not creating a new connection.
 
1463
        self._shared_connection.credentials = credentials
 
1464
 
 
1465
    def _reuse_for(self, other_base):
 
1466
        """Returns a transport sharing the same connection if possible.
 
1467
 
 
1468
        Note: we share the connection if the expected credentials are the
 
1469
        same: (host, port, user). Some protocols may disagree and redefine the
 
1470
        criteria in daughter classes.
 
1471
 
 
1472
        Note: we don't compare the passwords here because other_base may have
 
1473
        been obtained from an existing transport.base which do not mention the
 
1474
        password.
 
1475
 
 
1476
        :param other_base: the URL we want to share the connection with.
 
1477
 
 
1478
        :return: A new transport or None if the connection cannot be shared.
 
1479
        """
 
1480
        try:
 
1481
            (scheme, user, password,
 
1482
             host, port, path) = self._split_url(other_base)
 
1483
        except errors.InvalidURL:
 
1484
            # No hope in trying to reuse an existing transport for an invalid
 
1485
            # URL
 
1486
            return None
 
1487
 
 
1488
        transport = None
 
1489
        # Don't compare passwords, they may be absent from other_base or from
 
1490
        # self and they don't carry more information than user anyway.
 
1491
        if (scheme == self._scheme
 
1492
            and user == self._user
 
1493
            and host == self._host
 
1494
            and port == self._port):
 
1495
            if not path.endswith('/'):
 
1496
                # This normally occurs at __init__ time, but it's easier to do
 
1497
                # it now to avoid creating two transports for the same base.
 
1498
                path += '/'
 
1499
            if self._path  == path:
 
1500
                # shortcut, it's really the same transport
 
1501
                return self
 
1502
            # We don't call clone here because the intent is different: we
 
1503
            # build a new transport on a different base (which may be totally
 
1504
            # unrelated) but we share the connection.
 
1505
            transport = self.__class__(other_base, _from_transport=self)
 
1506
        return transport
 
1507
 
 
1508
 
 
1509
# We try to recognize an url lazily (ignoring user, password, etc)
 
1510
_urlRE = re.compile(r'^(?P<proto>[^:/\\]+)://(?P<rest>.*)$')
 
1511
 
 
1512
def get_transport(base, possible_transports=None):
688
1513
    """Open a transport to access a URL or directory.
689
1514
 
690
 
    base is either a URL or a directory name.  
 
1515
    :param base: either a URL or a directory name.
 
1516
 
 
1517
    :param transports: optional reusable transports list. If not None, created
 
1518
        transports will be added to the list.
 
1519
 
 
1520
    :return: A new transport optionally sharing its connection with one of
 
1521
        possible_transports.
691
1522
    """
692
 
    # TODO: give a better error if base looks like a url but there's no
693
 
    # handler for the scheme?
694
 
    global _protocol_handlers
695
1523
    if base is None:
696
1524
        base = '.'
 
1525
    last_err = None
 
1526
    from bzrlib.directory_service import directories
 
1527
    base = directories.dereference(base)
697
1528
 
698
1529
    def convert_path_to_url(base, error_str):
699
1530
        m = _urlRE.match(base)
700
1531
        if m:
701
1532
            # This looks like a URL, but we weren't able to 
702
1533
            # instantiate it as such raise an appropriate error
703
 
            raise errors.InvalidURL(base, error_str % m.group('proto'))
 
1534
            # FIXME: we have a 'error_str' unused and we use last_err below
 
1535
            raise errors.UnsupportedProtocol(base, last_err)
704
1536
        # This doesn't look like a protocol, consider it a local path
705
1537
        new_base = urlutils.local_path_to_url(base)
706
 
        mutter('converting os path %r => url %s' , base, new_base)
 
1538
        # mutter('converting os path %r => url %s', base, new_base)
707
1539
        return new_base
708
1540
 
709
1541
    # Catch any URLs which are passing Unicode rather than ASCII
713
1545
        # Only local paths can be Unicode
714
1546
        base = convert_path_to_url(base,
715
1547
            'URLs must be properly escaped (protocol: %s)')
716
 
    
717
 
    for proto, factory_list in _protocol_handlers.iteritems():
 
1548
 
 
1549
    transport = None
 
1550
    if possible_transports is not None:
 
1551
        for t in possible_transports:
 
1552
            t_same_connection = t._reuse_for(base)
 
1553
            if t_same_connection is not None:
 
1554
                # Add only new transports
 
1555
                if t_same_connection not in possible_transports:
 
1556
                    possible_transports.append(t_same_connection)
 
1557
                return t_same_connection
 
1558
 
 
1559
    for proto, factory_list in transport_list_registry.iteritems():
718
1560
        if proto is not None and base.startswith(proto):
719
 
            t = _try_transport_factories(base, factory_list)
720
 
            if t:
721
 
                return t
 
1561
            transport, last_err = _try_transport_factories(base, factory_list)
 
1562
            if transport:
 
1563
                if possible_transports is not None:
 
1564
                    if transport in possible_transports:
 
1565
                        raise AssertionError()
 
1566
                    possible_transports.append(transport)
 
1567
                return transport
722
1568
 
723
1569
    # We tried all the different protocols, now try one last time
724
1570
    # as a local protocol
725
1571
    base = convert_path_to_url(base, 'Unsupported protocol: %s')
726
1572
 
727
1573
    # The default handler is the filesystem handler, stored as protocol None
728
 
    return _try_transport_factories(base, _protocol_handlers[None])
 
1574
    factory_list = transport_list_registry.get(None)
 
1575
    transport, last_err = _try_transport_factories(base, factory_list)
 
1576
 
 
1577
    return transport
729
1578
 
730
1579
 
731
1580
def _try_transport_factories(base, factory_list):
 
1581
    last_err = None
732
1582
    for factory in factory_list:
733
1583
        try:
734
 
            return factory(base)
735
 
        except DependencyNotPresent, e:
 
1584
            return factory.get_obj()(base), None
 
1585
        except errors.DependencyNotPresent, e:
736
1586
            mutter("failed to instantiate transport %r for %r: %r" %
737
1587
                    (factory, base, e))
 
1588
            last_err = e
738
1589
            continue
739
 
    return None
 
1590
    return None, last_err
 
1591
 
 
1592
 
 
1593
def do_catching_redirections(action, transport, redirected):
 
1594
    """Execute an action with given transport catching redirections.
 
1595
 
 
1596
    This is a facility provided for callers needing to follow redirections
 
1597
    silently. The silence is relative: it is the caller responsability to
 
1598
    inform the user about each redirection or only inform the user of a user
 
1599
    via the exception parameter.
 
1600
 
 
1601
    :param action: A callable, what the caller want to do while catching
 
1602
                  redirections.
 
1603
    :param transport: The initial transport used.
 
1604
    :param redirected: A callable receiving the redirected transport and the 
 
1605
                  RedirectRequested exception.
 
1606
 
 
1607
    :return: Whatever 'action' returns
 
1608
    """
 
1609
    MAX_REDIRECTIONS = 8
 
1610
 
 
1611
    # If a loop occurs, there is little we can do. So we don't try to detect
 
1612
    # them, just getting out if too much redirections occurs. The solution
 
1613
    # is outside: where the loop is defined.
 
1614
    for redirections in range(MAX_REDIRECTIONS):
 
1615
        try:
 
1616
            return action(transport)
 
1617
        except errors.RedirectRequested, e:
 
1618
            redirection_notice = '%s is%s redirected to %s' % (
 
1619
                e.source, e.permanently, e.target)
 
1620
            transport = redirected(transport, e, redirection_notice)
 
1621
    else:
 
1622
        # Loop exited without resolving redirect ? Either the
 
1623
        # user has kept a very very very old reference or a loop
 
1624
        # occurred in the redirections.  Nothing we can cure here:
 
1625
        # tell the user. Note that as the user has been informed
 
1626
        # about each redirection (it is the caller responsibility
 
1627
        # to do that in redirected via the provided
 
1628
        # redirection_notice). The caller may provide more
 
1629
        # information if needed (like what file or directory we
 
1630
        # were trying to act upon when the redirection loop
 
1631
        # occurred).
 
1632
        raise errors.TooManyRedirections
740
1633
 
741
1634
 
742
1635
class Server(object):
771
1664
        raise NotImplementedError
772
1665
 
773
1666
    def get_bogus_url(self):
774
 
        """Return a url for this protocol, that will fail to connect."""
 
1667
        """Return a url for this protocol, that will fail to connect.
 
1668
        
 
1669
        This may raise NotImplementedError to indicate that this server cannot
 
1670
        provide bogus urls.
 
1671
        """
775
1672
        raise NotImplementedError
776
1673
 
777
1674
 
778
 
class TransportTestProviderAdapter(object):
779
 
    """A tool to generate a suite testing all transports for a single test.
780
 
 
781
 
    This is done by copying the test once for each transport and injecting
782
 
    the transport_class and transport_server classes into each copy. Each copy
783
 
    is also given a new id() to make it easy to identify.
784
 
    """
785
 
 
786
 
    def adapt(self, test):
787
 
        result = TestSuite()
788
 
        for klass, server_factory in self._test_permutations():
789
 
            new_test = deepcopy(test)
790
 
            new_test.transport_class = klass
791
 
            new_test.transport_server = server_factory
792
 
            def make_new_test_id():
793
 
                new_id = "%s(%s)" % (new_test.id(), server_factory.__name__)
794
 
                return lambda: new_id
795
 
            new_test.id = make_new_test_id()
796
 
            result.addTest(new_test)
797
 
        return result
798
 
 
799
 
    def get_transport_test_permutations(self, module):
800
 
        """Get the permutations module wants to have tested."""
801
 
        if not hasattr(module, 'get_test_permutations'):
802
 
            warning("transport module %s doesn't provide get_test_permutations()"
803
 
                    % module.__name__)
804
 
            return []
805
 
        return module.get_test_permutations()
806
 
 
807
 
    def _test_permutations(self):
808
 
        """Return a list of the klass, server_factory pairs to test."""
809
 
        result = []
810
 
        for module in _get_transport_modules():
811
 
            try:
812
 
                result.extend(self.get_transport_test_permutations(reduce(getattr, 
813
 
                    (module).split('.')[1:],
814
 
                     __import__(module))))
815
 
            except errors.DependencyNotPresent, e:
816
 
                # Continue even if a dependency prevents us 
817
 
                # from running this test
818
 
                pass
819
 
        return result
820
 
 
821
 
 
822
 
class TransportLogger(object):
823
 
    """Adapt a transport to get clear logging data on api calls.
824
 
    
825
 
    Feel free to extend to log whatever calls are of interest.
826
 
    """
827
 
 
828
 
    def __init__(self, adapted):
829
 
        self._adapted = adapted
830
 
        self._calls = []
831
 
 
832
 
    def get(self, name):
833
 
        self._calls.append((name,))
834
 
        return self._adapted.get(name)
835
 
 
836
 
    def __getattr__(self, name):
837
 
        """Thunk all undefined access through to self._adapted."""
838
 
        # raise AttributeError, name 
839
 
        return getattr(self._adapted, name)
840
 
 
841
 
    def readv(self, name, offsets):
842
 
        self._calls.append((name, offsets))
843
 
        return self._adapted.readv(name, offsets)
844
 
        
845
 
 
846
1675
# None is the default transport, for things with no url scheme
847
 
register_lazy_transport(None, 'bzrlib.transport.local', 'LocalTransport')
 
1676
register_transport_proto('file://',
 
1677
            help="Access using the standard filesystem (default)")
848
1678
register_lazy_transport('file://', 'bzrlib.transport.local', 'LocalTransport')
 
1679
transport_list_registry.set_default_transport("file://")
 
1680
 
 
1681
register_transport_proto('sftp://',
 
1682
            help="Access using SFTP (most SSH servers provide SFTP).",
 
1683
            register_netloc=True)
849
1684
register_lazy_transport('sftp://', 'bzrlib.transport.sftp', 'SFTPTransport')
 
1685
# Decorated http transport
 
1686
register_transport_proto('http+urllib://',
 
1687
#                help="Read-only access of branches exported on the web."
 
1688
                         register_netloc=True)
850
1689
register_lazy_transport('http+urllib://', 'bzrlib.transport.http._urllib',
851
1690
                        'HttpTransport_urllib')
 
1691
register_transport_proto('https+urllib://',
 
1692
#                help="Read-only access of branches exported on the web using SSL."
 
1693
                         register_netloc=True)
852
1694
register_lazy_transport('https+urllib://', 'bzrlib.transport.http._urllib',
853
1695
                        'HttpTransport_urllib')
 
1696
register_transport_proto('http+pycurl://',
 
1697
#                help="Read-only access of branches exported on the web."
 
1698
                         register_netloc=True)
854
1699
register_lazy_transport('http+pycurl://', 'bzrlib.transport.http._pycurl',
855
1700
                        'PyCurlTransport')
 
1701
register_transport_proto('https+pycurl://',
 
1702
#                help="Read-only access of branches exported on the web using SSL."
 
1703
                         register_netloc=True)
856
1704
register_lazy_transport('https+pycurl://', 'bzrlib.transport.http._pycurl',
857
1705
                        'PyCurlTransport')
 
1706
# Default http transports (last declared wins (if it can be imported))
 
1707
register_transport_proto('http://',
 
1708
                 help="Read-only access of branches exported on the web.")
 
1709
register_transport_proto('https://',
 
1710
            help="Read-only access of branches exported on the web using SSL.")
858
1711
register_lazy_transport('http://', 'bzrlib.transport.http._urllib',
859
1712
                        'HttpTransport_urllib')
860
1713
register_lazy_transport('https://', 'bzrlib.transport.http._urllib',
861
1714
                        'HttpTransport_urllib')
862
 
register_lazy_transport('http://', 'bzrlib.transport.http._pycurl', 'PyCurlTransport')
863
 
register_lazy_transport('https://', 'bzrlib.transport.http._pycurl', 'PyCurlTransport')
 
1715
register_lazy_transport('http://', 'bzrlib.transport.http._pycurl',
 
1716
                        'PyCurlTransport')
 
1717
register_lazy_transport('https://', 'bzrlib.transport.http._pycurl',
 
1718
                        'PyCurlTransport')
 
1719
 
 
1720
register_transport_proto('ftp://', help="Access using passive FTP.")
864
1721
register_lazy_transport('ftp://', 'bzrlib.transport.ftp', 'FtpTransport')
 
1722
register_transport_proto('aftp://', help="Access using active FTP.")
865
1723
register_lazy_transport('aftp://', 'bzrlib.transport.ftp', 'FtpTransport')
866
 
register_lazy_transport('memory://', 'bzrlib.transport.memory', 'MemoryTransport')
867
 
register_lazy_transport('readonly+', 'bzrlib.transport.readonly', 'ReadonlyTransportDecorator')
868
 
register_lazy_transport('fakenfs+', 'bzrlib.transport.fakenfs', 'FakeNFSTransportDecorator')
869
 
register_lazy_transport('vfat+', 
 
1724
 
 
1725
register_transport_proto('memory://')
 
1726
register_lazy_transport('memory://', 'bzrlib.transport.memory',
 
1727
                        'MemoryTransport')
 
1728
 
 
1729
# chroots cannot be implicitly accessed, they must be explicitly created:
 
1730
register_transport_proto('chroot+')
 
1731
 
 
1732
register_transport_proto('readonly+',
 
1733
#              help="This modifier converts any transport to be readonly."
 
1734
            )
 
1735
register_lazy_transport('readonly+', 'bzrlib.transport.readonly',
 
1736
                        'ReadonlyTransportDecorator')
 
1737
 
 
1738
register_transport_proto('fakenfs+')
 
1739
register_lazy_transport('fakenfs+', 'bzrlib.transport.fakenfs',
 
1740
                        'FakeNFSTransportDecorator')
 
1741
 
 
1742
register_transport_proto('trace+')
 
1743
register_lazy_transport('trace+', 'bzrlib.transport.trace',
 
1744
                        'TransportTraceDecorator')
 
1745
 
 
1746
register_transport_proto('unlistable+')
 
1747
register_lazy_transport('unlistable+', 'bzrlib.transport.unlistable',
 
1748
                        'UnlistableTransportDecorator')
 
1749
 
 
1750
register_transport_proto('brokenrename+')
 
1751
register_lazy_transport('brokenrename+', 'bzrlib.transport.brokenrename',
 
1752
                        'BrokenRenameTransportDecorator')
 
1753
 
 
1754
register_transport_proto('vfat+')
 
1755
register_lazy_transport('vfat+',
870
1756
                        'bzrlib.transport.fakevfat',
871
1757
                        'FakeVFATTransportDecorator')
 
1758
 
 
1759
register_transport_proto('nosmart+')
 
1760
register_lazy_transport('nosmart+', 'bzrlib.transport.nosmart',
 
1761
                        'NoSmartTransportDecorator')
 
1762
 
 
1763
# These two schemes were registered, but don't seem to have an actual transport
 
1764
# protocol registered
 
1765
for scheme in ['ssh', 'bzr+loopback']:
 
1766
    register_urlparse_netloc_protocol(scheme)
 
1767
del scheme
 
1768
 
 
1769
register_transport_proto('bzr://',
 
1770
            help="Fast access using the Bazaar smart server.",
 
1771
                         register_netloc=True)
 
1772
 
 
1773
register_lazy_transport('bzr://', 'bzrlib.transport.remote',
 
1774
                        'RemoteTCPTransport')
 
1775
register_transport_proto('bzr-v2://', register_netloc=True)
 
1776
 
 
1777
register_lazy_transport('bzr-v2://', 'bzrlib.transport.remote',
 
1778
                        'RemoteTCPTransportV2Only')
 
1779
register_transport_proto('bzr+http://',
 
1780
#                help="Fast access using the Bazaar smart server over HTTP."
 
1781
                         register_netloc=True)
 
1782
register_lazy_transport('bzr+http://', 'bzrlib.transport.remote',
 
1783
                        'RemoteHTTPTransport')
 
1784
register_transport_proto('bzr+https://',
 
1785
#                help="Fast access using the Bazaar smart server over HTTPS."
 
1786
                         register_netloc=True)
 
1787
register_lazy_transport('bzr+https://',
 
1788
                        'bzrlib.transport.remote',
 
1789
                        'RemoteHTTPTransport')
 
1790
register_transport_proto('bzr+ssh://',
 
1791
            help="Fast access using the Bazaar smart server over SSH.",
 
1792
            register_netloc=True)
 
1793
register_lazy_transport('bzr+ssh://', 'bzrlib.transport.remote',
 
1794
                        'RemoteSSHTransport')