~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/transport/http.py

  • Committer: Martin Pool
  • Date: 2005-11-04 01:46:31 UTC
  • mto: (1185.33.49 bzr.dev)
  • mto: This revision was merged to the branch mainline in revision 1512.
  • Revision ID: mbp@sourcefrog.net-20051104014631-750e0ad4172c952c
Make biobench directly executable

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2005-2010 Canonical Ltd
2
 
#
 
1
# Copyright (C) 2005 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
 
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16
 
 
17
 
"""Base implementation of Transport over http.
18
 
 
19
 
There are separate implementation modules for each http client implementation.
 
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 
16
"""Implementation of Transport over http.
20
17
"""
21
18
 
22
 
from __future__ import absolute_import
23
 
 
24
 
import os
25
 
import re
 
19
from bzrlib.transport import Transport, register_transport
 
20
from bzrlib.errors import (TransportNotPossible, NoSuchFile, 
 
21
                           NonRelativePath, TransportError)
 
22
import os, errno
 
23
from cStringIO import StringIO
 
24
import urllib2
26
25
import urlparse
27
 
import sys
28
 
import weakref
29
26
 
30
 
from bzrlib import (
31
 
    debug,
32
 
    errors,
33
 
    transport,
34
 
    ui,
35
 
    urlutils,
36
 
    )
37
 
from bzrlib.smart import medium
 
27
from bzrlib.errors import BzrError, BzrCheckError
 
28
from bzrlib.branch import Branch
38
29
from bzrlib.trace import mutter
39
 
from bzrlib.transport import (
40
 
    ConnectedTransport,
41
 
    )
42
 
 
43
 
 
44
 
class HttpTransportBase(ConnectedTransport):
45
 
    """Base class for http implementations.
46
 
 
47
 
    Does URL parsing, etc, but not any network IO.
48
 
 
49
 
    The protocol can be given as e.g. http+urllib://host/ to use a particular
50
 
    implementation.
 
30
 
 
31
 
 
32
def get_url(url):
 
33
    import urllib2
 
34
    mutter("get_url %s" % url)
 
35
    url_f = urllib2.urlopen(url)
 
36
    return url_f
 
37
 
 
38
class HttpTransportError(TransportError):
 
39
    pass
 
40
 
 
41
class HttpTransport(Transport):
 
42
    """This is the transport agent for http:// access.
 
43
    
 
44
    TODO: Implement pipelined versions of all of the *_multi() functions.
51
45
    """
52
46
 
53
 
    # _unqualified_scheme: "http" or "https"
54
 
    # _scheme: may have "+pycurl", etc
55
 
 
56
 
    def __init__(self, base, _impl_name, _from_transport=None):
 
47
    def __init__(self, base):
57
48
        """Set the base path where files will be stored."""
58
 
        proto_match = re.match(r'^(https?)(\+\w+)?://', base)
59
 
        if not proto_match:
60
 
            raise AssertionError("not a http url: %r" % base)
61
 
        self._unqualified_scheme = proto_match.group(1)
62
 
        self._impl_name = _impl_name
63
 
        super(HttpTransportBase, self).__init__(base,
64
 
                                                _from_transport=_from_transport)
65
 
        self._medium = None
66
 
        # range hint is handled dynamically throughout the life
67
 
        # of the transport object. We start by trying multi-range
68
 
        # requests and if the server returns bogus results, we
69
 
        # retry with single range requests and, finally, we
70
 
        # forget about range if the server really can't
71
 
        # understand. Once acquired, this piece of info is
72
 
        # propagated to clones.
73
 
        if _from_transport is not None:
74
 
            self._range_hint = _from_transport._range_hint
75
 
        else:
76
 
            self._range_hint = 'multi'
 
49
        assert base.startswith('http://') or base.startswith('https://')
 
50
        super(HttpTransport, self).__init__(base)
 
51
        # In the future we might actually connect to the remote host
 
52
        # rather than using get_url
 
53
        # self._connection = None
 
54
        (self._proto, self._host,
 
55
            self._path, self._parameters,
 
56
            self._query, self._fragment) = urlparse.urlparse(self.base)
 
57
 
 
58
    def should_cache(self):
 
59
        """Return True if the data pulled across should be cached locally.
 
60
        """
 
61
        return True
 
62
 
 
63
    def clone(self, offset=None):
 
64
        """Return a new HttpTransport with root at self.base + offset
 
65
        For now HttpTransport does not actually connect, so just return
 
66
        a new HttpTransport object.
 
67
        """
 
68
        if offset is None:
 
69
            return HttpTransport(self.base)
 
70
        else:
 
71
            return HttpTransport(self.abspath(offset))
 
72
 
 
73
    def abspath(self, relpath):
 
74
        """Return the full url to the given relative path.
 
75
        This can be supplied with a string or a list
 
76
        """
 
77
        assert isinstance(relpath, basestring)
 
78
        if isinstance(relpath, basestring):
 
79
            relpath_parts = relpath.split('/')
 
80
        else:
 
81
            # TODO: Don't call this with an array - no magic interfaces
 
82
            relpath_parts = relpath[:]
 
83
        if len(relpath_parts) > 1:
 
84
            if relpath_parts[0] == '':
 
85
                raise ValueError("path %r within branch %r seems to be absolute"
 
86
                                 % (relpath, self._path))
 
87
            if relpath_parts[-1] == '':
 
88
                raise ValueError("path %r within branch %r seems to be a directory"
 
89
                                 % (relpath, self._path))
 
90
        basepath = self._path.split('/')
 
91
        if len(basepath) > 0 and basepath[-1] == '':
 
92
            basepath = basepath[:-1]
 
93
        for p in relpath_parts:
 
94
            if p == '..':
 
95
                if len(basepath) == 0:
 
96
                    # In most filesystems, a request for the parent
 
97
                    # of root, just returns root.
 
98
                    continue
 
99
                basepath.pop()
 
100
            elif p == '.' or p == '':
 
101
                continue # No-op
 
102
            else:
 
103
                basepath.append(p)
 
104
        # Possibly, we could use urlparse.urljoin() here, but
 
105
        # I'm concerned about when it chooses to strip the last
 
106
        # portion of the path, and when it doesn't.
 
107
        path = '/'.join(basepath)
 
108
        return urlparse.urlunparse((self._proto,
 
109
                self._host, path, '', '', ''))
77
110
 
78
111
    def has(self, relpath):
79
 
        raise NotImplementedError("has() is abstract on %r" % self)
80
 
 
81
 
    def get(self, relpath):
 
112
        """Does the target location exist?
 
113
 
 
114
        TODO: HttpTransport.has() should use a HEAD request,
 
115
        not a full GET request.
 
116
 
 
117
        TODO: This should be changed so that we don't use
 
118
        urllib2 and get an exception, the code path would be
 
119
        cleaner if we just do an http HEAD request, and parse
 
120
        the return code.
 
121
        """
 
122
        try:
 
123
            f = get_url(self.abspath(relpath))
 
124
            # Without the read and then close()
 
125
            # we tend to have busy sockets.
 
126
            f.read()
 
127
            f.close()
 
128
            return True
 
129
        except urllib2.URLError, e:
 
130
            if e.code == 404:
 
131
                return False
 
132
            raise
 
133
        except IOError, e:
 
134
            if e.errno == errno.ENOENT:
 
135
                return False
 
136
            raise HttpTransportError(orig_error=e)
 
137
 
 
138
    def get(self, relpath, decode=False):
82
139
        """Get the file at the given relative path.
83
140
 
84
141
        :param relpath: The relative path to the file
85
142
        """
86
 
        code, response_file = self._get(relpath, None)
87
 
        return response_file
88
 
 
89
 
    def _get(self, relpath, ranges, tail_amount=0):
90
 
        """Get a file, or part of a file.
91
 
 
92
 
        :param relpath: Path relative to transport base URL
93
 
        :param ranges: None to get the whole file;
94
 
            or  a list of _CoalescedOffset to fetch parts of a file.
95
 
        :param tail_amount: The amount to get from the end of the file.
96
 
 
97
 
        :returns: (http_code, result_file)
98
 
        """
99
 
        raise NotImplementedError(self._get)
100
 
 
101
 
    def _remote_path(self, relpath):
102
 
        """See ConnectedTransport._remote_path.
103
 
 
104
 
        user and passwords are not embedded in the path provided to the server.
105
 
        """
106
 
        url = self._parsed_url.clone(relpath)
107
 
        url.user = url.quoted_user = None
108
 
        url.password = url.quoted_password = None
109
 
        url.scheme = self._unqualified_scheme
110
 
        return str(url)
111
 
 
112
 
    def _create_auth(self):
113
 
        """Returns a dict containing the credentials provided at build time."""
114
 
        auth = dict(host=self._parsed_url.host, port=self._parsed_url.port,
115
 
                    user=self._parsed_url.user, password=self._parsed_url.password,
116
 
                    protocol=self._unqualified_scheme,
117
 
                    path=self._parsed_url.path)
118
 
        return auth
119
 
 
120
 
    def get_smart_medium(self):
121
 
        """See Transport.get_smart_medium."""
122
 
        if self._medium is None:
123
 
            # Since medium holds some state (smart server probing at least), we
124
 
            # need to keep it around. Note that this is needed because medium
125
 
            # has the same 'base' attribute as the transport so it can't be
126
 
            # shared between transports having different bases.
127
 
            self._medium = SmartClientHTTPMedium(self)
128
 
        return self._medium
129
 
 
130
 
    def _degrade_range_hint(self, relpath, ranges, exc_info):
131
 
        if self._range_hint == 'multi':
132
 
            self._range_hint = 'single'
133
 
            mutter('Retry "%s" with single range request' % relpath)
134
 
        elif self._range_hint == 'single':
135
 
            self._range_hint = None
136
 
            mutter('Retry "%s" without ranges' % relpath)
137
 
        else:
138
 
            # We tried all the tricks, but nothing worked. We re-raise the
139
 
            # original exception; the 'mutter' calls above will indicate that
140
 
            # further tries were unsuccessful
141
 
            raise exc_info[0], exc_info[1], exc_info[2]
142
 
 
143
 
    # _coalesce_offsets is a helper for readv, it try to combine ranges without
144
 
    # degrading readv performances. _bytes_to_read_before_seek is the value
145
 
    # used for the limit parameter and has been tuned for other transports. For
146
 
    # HTTP, the name is inappropriate but the parameter is still useful and
147
 
    # helps reduce the number of chunks in the response. The overhead for a
148
 
    # chunk (headers, length, footer around the data itself is variable but
149
 
    # around 50 bytes. We use 128 to reduce the range specifiers that appear in
150
 
    # the header, some servers (notably Apache) enforce a maximum length for a
151
 
    # header and issue a '400: Bad request' error when too much ranges are
152
 
    # specified.
153
 
    _bytes_to_read_before_seek = 128
154
 
    # No limit on the offset number that get combined into one, we are trying
155
 
    # to avoid downloading the whole file.
156
 
    _max_readv_combine = 0
157
 
    # By default Apache has a limit of ~400 ranges before replying with a 400
158
 
    # Bad Request. So we go underneath that amount to be safe.
159
 
    _max_get_ranges = 200
160
 
    # We impose no limit on the range size. But see _pycurl.py for a different
161
 
    # use.
162
 
    _get_max_size = 0
163
 
 
164
 
    def _readv(self, relpath, offsets):
165
 
        """Get parts of the file at the given relative path.
166
 
 
167
 
        :param offsets: A list of (offset, size) tuples.
168
 
        :param return: A list or generator of (offset, data) tuples
169
 
        """
170
 
        # offsets may be a generator, we will iterate it several times, so
171
 
        # build a list
172
 
        offsets = list(offsets)
173
 
 
174
 
        try_again = True
175
 
        retried_offset = None
176
 
        while try_again:
177
 
            try_again = False
178
 
 
179
 
            # Coalesce the offsets to minimize the GET requests issued
180
 
            sorted_offsets = sorted(offsets)
181
 
            coalesced = self._coalesce_offsets(
182
 
                sorted_offsets, limit=self._max_readv_combine,
183
 
                fudge_factor=self._bytes_to_read_before_seek,
184
 
                max_size=self._get_max_size)
185
 
 
186
 
            # Turn it into a list, we will iterate it several times
187
 
            coalesced = list(coalesced)
188
 
            if 'http' in debug.debug_flags:
189
 
                mutter('http readv of %s  offsets => %s collapsed %s',
190
 
                    relpath, len(offsets), len(coalesced))
191
 
 
192
 
            # Cache the data read, but only until it's been used
193
 
            data_map = {}
194
 
            # We will iterate on the data received from the GET requests and
195
 
            # serve the corresponding offsets respecting the initial order. We
196
 
            # need an offset iterator for that.
197
 
            iter_offsets = iter(offsets)
198
 
            cur_offset_and_size = iter_offsets.next()
199
 
 
200
 
            try:
201
 
                for cur_coal, rfile in self._coalesce_readv(relpath, coalesced):
202
 
                    # Split the received chunk
203
 
                    for offset, size in cur_coal.ranges:
204
 
                        start = cur_coal.start + offset
205
 
                        rfile.seek(start, os.SEEK_SET)
206
 
                        data = rfile.read(size)
207
 
                        data_len = len(data)
208
 
                        if data_len != size:
209
 
                            raise errors.ShortReadvError(relpath, start, size,
210
 
                                                         actual=data_len)
211
 
                        if (start, size) == cur_offset_and_size:
212
 
                            # The offset requested are sorted as the coalesced
213
 
                            # ones, no need to cache. Win !
214
 
                            yield cur_offset_and_size[0], data
215
 
                            cur_offset_and_size = iter_offsets.next()
216
 
                        else:
217
 
                            # Different sorting. We need to cache.
218
 
                            data_map[(start, size)] = data
219
 
 
220
 
                    # Yield everything we can
221
 
                    while cur_offset_and_size in data_map:
222
 
                        # Clean the cached data since we use it
223
 
                        # XXX: will break if offsets contains duplicates --
224
 
                        # vila20071129
225
 
                        this_data = data_map.pop(cur_offset_and_size)
226
 
                        yield cur_offset_and_size[0], this_data
227
 
                        cur_offset_and_size = iter_offsets.next()
228
 
 
229
 
            except (errors.ShortReadvError, errors.InvalidRange,
230
 
                    errors.InvalidHttpRange, errors.HttpBoundaryMissing), e:
231
 
                mutter('Exception %r: %s during http._readv',e, e)
232
 
                if (not isinstance(e, errors.ShortReadvError)
233
 
                    or retried_offset == cur_offset_and_size):
234
 
                    # We don't degrade the range hint for ShortReadvError since
235
 
                    # they do not indicate a problem with the server ability to
236
 
                    # handle ranges. Except when we fail to get back a required
237
 
                    # offset twice in a row. In that case, falling back to
238
 
                    # single range or whole file should help or end up in a
239
 
                    # fatal exception.
240
 
                    self._degrade_range_hint(relpath, coalesced, sys.exc_info())
241
 
                # Some offsets may have been already processed, so we retry
242
 
                # only the unsuccessful ones.
243
 
                offsets = [cur_offset_and_size] + [o for o in iter_offsets]
244
 
                retried_offset = cur_offset_and_size
245
 
                try_again = True
246
 
 
247
 
    def _coalesce_readv(self, relpath, coalesced):
248
 
        """Issue several GET requests to satisfy the coalesced offsets"""
249
 
 
250
 
        def get_and_yield(relpath, coalesced):
251
 
            if coalesced:
252
 
                # Note that the _get below may raise
253
 
                # errors.InvalidHttpRange. It's the caller's responsibility to
254
 
                # decide how to retry since it may provide different coalesced
255
 
                # offsets.
256
 
                code, rfile = self._get(relpath, coalesced)
257
 
                for coal in coalesced:
258
 
                    yield coal, rfile
259
 
 
260
 
        if self._range_hint is None:
261
 
            # Download whole file
262
 
            for c, rfile in get_and_yield(relpath, coalesced):
263
 
                yield c, rfile
264
 
        else:
265
 
            total = len(coalesced)
266
 
            if self._range_hint == 'multi':
267
 
                max_ranges = self._max_get_ranges
268
 
            elif self._range_hint == 'single':
269
 
                max_ranges = total
270
 
            else:
271
 
                raise AssertionError("Unknown _range_hint %r"
272
 
                                     % (self._range_hint,))
273
 
            # TODO: Some web servers may ignore the range requests and return
274
 
            # the whole file, we may want to detect that and avoid further
275
 
            # requests.
276
 
            # Hint: test_readv_multiple_get_requests will fail once we do that
277
 
            cumul = 0
278
 
            ranges = []
279
 
            for coal in coalesced:
280
 
                if ((self._get_max_size > 0
281
 
                     and cumul + coal.length > self._get_max_size)
282
 
                    or len(ranges) >= max_ranges):
283
 
                    # Get that much and yield
284
 
                    for c, rfile in get_and_yield(relpath, ranges):
285
 
                        yield c, rfile
286
 
                    # Restart with the current offset
287
 
                    ranges = [coal]
288
 
                    cumul = coal.length
289
 
                else:
290
 
                    ranges.append(coal)
291
 
                    cumul += coal.length
292
 
            # Get the rest and yield
293
 
            for c, rfile in get_and_yield(relpath, ranges):
294
 
                yield c, rfile
295
 
 
296
 
    def recommended_page_size(self):
297
 
        """See Transport.recommended_page_size().
298
 
 
299
 
        For HTTP we suggest a large page size to reduce the overhead
300
 
        introduced by latency.
301
 
        """
302
 
        return 64 * 1024
303
 
 
304
 
    def _post(self, body_bytes):
305
 
        """POST body_bytes to .bzr/smart on this transport.
306
 
 
307
 
        :returns: (response code, response body file-like object).
308
 
        """
309
 
        # TODO: Requiring all the body_bytes to be available at the beginning of
310
 
        # the POST may require large client buffers.  It would be nice to have
311
 
        # an interface that allows streaming via POST when possible (and
312
 
        # degrades to a local buffer when not).
313
 
        raise NotImplementedError(self._post)
314
 
 
315
 
    def put_file(self, relpath, f, mode=None):
316
 
        """Copy the file-like object into the location.
 
143
        try:
 
144
            return get_url(self.abspath(relpath))
 
145
        except urllib2.URLError, e:
 
146
            if e.code == 404:
 
147
                raise NoSuchFile(msg = "Error retrieving %s: %s" 
 
148
                                 % (self.abspath(relpath), str(e)),
 
149
                                 orig_error=e)
 
150
            raise
 
151
        except (BzrError, IOError), e:
 
152
            raise NoSuchFile(msg = "Error retrieving %s: %s" 
 
153
                             % (self.abspath(relpath), str(e)),
 
154
                             orig_error=e)
 
155
 
 
156
    def put(self, relpath, f):
 
157
        """Copy the file-like or string object into the location.
317
158
 
318
159
        :param relpath: Location to put the contents, relative to base.
319
 
        :param f:       File-like object.
 
160
        :param f:       File-like or string object.
320
161
        """
321
 
        raise errors.TransportNotPossible('http PUT not supported')
 
162
        raise TransportNotPossible('http PUT not supported')
322
163
 
323
 
    def mkdir(self, relpath, mode=None):
 
164
    def mkdir(self, relpath):
324
165
        """Create a directory at the given path."""
325
 
        raise errors.TransportNotPossible('http does not support mkdir()')
326
 
 
327
 
    def rmdir(self, relpath):
328
 
        """See Transport.rmdir."""
329
 
        raise errors.TransportNotPossible('http does not support rmdir()')
330
 
 
331
 
    def append_file(self, relpath, f, mode=None):
 
166
        raise TransportNotPossible('http does not support mkdir()')
 
167
 
 
168
    def append(self, relpath, f):
332
169
        """Append the text in the file-like object into the final
333
170
        location.
334
171
        """
335
 
        raise errors.TransportNotPossible('http does not support append()')
 
172
        raise TransportNotPossible('http does not support append()')
336
173
 
337
174
    def copy(self, rel_from, rel_to):
338
175
        """Copy the item at rel_from to the location at rel_to"""
339
 
        raise errors.TransportNotPossible('http does not support copy()')
 
176
        raise TransportNotPossible('http does not support copy()')
340
177
 
341
 
    def copy_to(self, relpaths, other, mode=None, pb=None):
 
178
    def copy_to(self, relpaths, other, pb=None):
342
179
        """Copy a set of entries from self into another Transport.
343
180
 
344
181
        :param relpaths: A list/generator of entries to be copied.
349
186
        # At this point HttpTransport might be able to check and see if
350
187
        # the remote location is the same, and rather than download, and
351
188
        # then upload, it could just issue a remote copy_this command.
352
 
        if isinstance(other, HttpTransportBase):
353
 
            raise errors.TransportNotPossible(
354
 
                'http cannot be the target of copy_to()')
 
189
        if isinstance(other, HttpTransport):
 
190
            raise TransportNotPossible('http cannot be the target of copy_to()')
355
191
        else:
356
 
            return super(HttpTransportBase, self).\
357
 
                    copy_to(relpaths, other, mode=mode, pb=pb)
 
192
            return super(HttpTransport, self).copy_to(relpaths, other, pb=pb)
358
193
 
359
194
    def move(self, rel_from, rel_to):
360
195
        """Move the item at rel_from to the location at rel_to"""
361
 
        raise errors.TransportNotPossible('http does not support move()')
 
196
        raise TransportNotPossible('http does not support move()')
362
197
 
363
198
    def delete(self, relpath):
364
199
        """Delete the item at relpath"""
365
 
        raise errors.TransportNotPossible('http does not support delete()')
366
 
 
367
 
    def external_url(self):
368
 
        """See bzrlib.transport.Transport.external_url."""
369
 
        # HTTP URL's are externally usable as long as they don't mention their
370
 
        # implementation qualifier
371
 
        url = self._parsed_url.clone()
372
 
        url.scheme = self._unqualified_scheme
373
 
        return str(url)
374
 
 
375
 
    def is_readonly(self):
376
 
        """See Transport.is_readonly."""
377
 
        return True
 
200
        raise TransportNotPossible('http does not support delete()')
378
201
 
379
202
    def listable(self):
380
203
        """See Transport.listable."""
383
206
    def stat(self, relpath):
384
207
        """Return the stat information for a file.
385
208
        """
386
 
        raise errors.TransportNotPossible('http does not support stat()')
 
209
        raise TransportNotPossible('http does not support stat()')
387
210
 
388
211
    def lock_read(self, relpath):
389
212
        """Lock the given file for shared (read) access.
404
227
 
405
228
        :return: A lock object, which should be passed to Transport.unlock()
406
229
        """
407
 
        raise errors.TransportNotPossible('http does not support lock_write()')
408
 
 
409
 
    def _attempted_range_header(self, offsets, tail_amount):
410
 
        """Prepare a HTTP Range header at a level the server should accept.
411
 
 
412
 
        :return: the range header representing offsets/tail_amount or None if
413
 
            no header can be built.
414
 
        """
415
 
 
416
 
        if self._range_hint == 'multi':
417
 
            # Generate the header describing all offsets
418
 
            return self._range_header(offsets, tail_amount)
419
 
        elif self._range_hint == 'single':
420
 
            # Combine all the requested ranges into a single
421
 
            # encompassing one
422
 
            if len(offsets) > 0:
423
 
                if tail_amount not in (0, None):
424
 
                    # Nothing we can do here to combine ranges with tail_amount
425
 
                    # in a single range, just returns None. The whole file
426
 
                    # should be downloaded.
427
 
                    return None
428
 
                else:
429
 
                    start = offsets[0].start
430
 
                    last = offsets[-1]
431
 
                    end = last.start + last.length - 1
432
 
                    whole = self._coalesce_offsets([(start, end - start + 1)],
433
 
                                                   limit=0, fudge_factor=0)
434
 
                    return self._range_header(list(whole), 0)
435
 
            else:
436
 
                # Only tail_amount, requested, leave range_header
437
 
                # do its work
438
 
                return self._range_header(offsets, tail_amount)
439
 
        else:
440
 
            return None
441
 
 
442
 
    @staticmethod
443
 
    def _range_header(ranges, tail_amount):
444
 
        """Turn a list of bytes ranges into a HTTP Range header value.
445
 
 
446
 
        :param ranges: A list of _CoalescedOffset
447
 
        :param tail_amount: The amount to get from the end of the file.
448
 
 
449
 
        :return: HTTP range header string.
450
 
 
451
 
        At least a non-empty ranges *or* a tail_amount must be
452
 
        provided.
453
 
        """
454
 
        strings = []
455
 
        for offset in ranges:
456
 
            strings.append('%d-%d' % (offset.start,
457
 
                                      offset.start + offset.length - 1))
458
 
 
459
 
        if tail_amount:
460
 
            strings.append('-%d' % tail_amount)
461
 
 
462
 
        return ','.join(strings)
463
 
 
464
 
    def _redirected_to(self, source, target):
465
 
        """Returns a transport suitable to re-issue a redirected request.
466
 
 
467
 
        :param source: The source url as returned by the server.
468
 
        :param target: The target url as returned by the server.
469
 
 
470
 
        The redirection can be handled only if the relpath involved is not
471
 
        renamed by the redirection.
472
 
 
473
 
        :returns: A transport or None.
474
 
        """
475
 
        parsed_source = self._split_url(source)
476
 
        parsed_target = self._split_url(target)
477
 
        pl = len(self._parsed_url.path)
478
 
        # determine the excess tail - the relative path that was in
479
 
        # the original request but not part of this transports' URL.
480
 
        excess_tail = parsed_source.path[pl:].strip("/")
481
 
        if not target.endswith(excess_tail):
482
 
            # The final part of the url has been renamed, we can't handle the
483
 
            # redirection.
484
 
            return None
485
 
 
486
 
        target_path = parsed_target.path
487
 
        if excess_tail:
488
 
            # Drop the tail that was in the redirect but not part of
489
 
            # the path of this transport.
490
 
            target_path = target_path[:-len(excess_tail)]
491
 
 
492
 
        if parsed_target.scheme in ('http', 'https'):
493
 
            # Same protocol family (i.e. http[s]), we will preserve the same
494
 
            # http client implementation when a redirection occurs from one to
495
 
            # the other (otherwise users may be surprised that bzr switches
496
 
            # from one implementation to the other, and devs may suffer
497
 
            # debugging it).
498
 
            if (parsed_target.scheme == self._unqualified_scheme
499
 
                and parsed_target.host == self._parsed_url.host
500
 
                and parsed_target.port == self._parsed_url.port
501
 
                and (parsed_target.user is None or
502
 
                     parsed_target.user == self._parsed_url.user)):
503
 
                # If a user is specified, it should match, we don't care about
504
 
                # passwords, wrong passwords will be rejected anyway.
505
 
                return self.clone(target_path)
506
 
            else:
507
 
                # Rebuild the url preserving the scheme qualification and the
508
 
                # credentials (if they don't apply, the redirected to server
509
 
                # will tell us, but if they do apply, we avoid prompting the
510
 
                # user)
511
 
                redir_scheme = parsed_target.scheme + '+' + self._impl_name
512
 
                new_url = self._unsplit_url(redir_scheme,
513
 
                    self._parsed_url.user,
514
 
                    self._parsed_url.password,
515
 
                    parsed_target.host, parsed_target.port,
516
 
                    target_path)
517
 
                return transport.get_transport_from_url(new_url)
518
 
        else:
519
 
            # Redirected to a different protocol
520
 
            new_url = self._unsplit_url(parsed_target.scheme,
521
 
                    parsed_target.user,
522
 
                    parsed_target.password,
523
 
                    parsed_target.host, parsed_target.port,
524
 
                    target_path)
525
 
            return transport.get_transport_from_url(new_url)
526
 
 
527
 
 
528
 
# TODO: May be better located in smart/medium.py with the other
529
 
# SmartMedium classes
530
 
class SmartClientHTTPMedium(medium.SmartClientMedium):
531
 
 
532
 
    def __init__(self, http_transport):
533
 
        super(SmartClientHTTPMedium, self).__init__(http_transport.base)
534
 
        # We don't want to create a circular reference between the http
535
 
        # transport and its associated medium. Since the transport will live
536
 
        # longer than the medium, the medium keep only a weak reference to its
537
 
        # transport.
538
 
        self._http_transport_ref = weakref.ref(http_transport)
539
 
 
540
 
    def get_request(self):
541
 
        return SmartClientHTTPMediumRequest(self)
542
 
 
543
 
    def should_probe(self):
544
 
        return True
545
 
 
546
 
    def remote_path_from_transport(self, transport):
547
 
        # Strip the optional 'bzr+' prefix from transport so it will have the
548
 
        # same scheme as self.
549
 
        transport_base = transport.base
550
 
        if transport_base.startswith('bzr+'):
551
 
            transport_base = transport_base[4:]
552
 
        rel_url = urlutils.relative_url(self.base, transport_base)
553
 
        return urlutils.unquote(rel_url)
554
 
 
555
 
    def send_http_smart_request(self, bytes):
556
 
        try:
557
 
            # Get back the http_transport hold by the weak reference
558
 
            t = self._http_transport_ref()
559
 
            code, body_filelike = t._post(bytes)
560
 
            if code != 200:
561
 
                raise errors.InvalidHttpResponse(
562
 
                    t._remote_path('.bzr/smart'),
563
 
                    'Expected 200 response code, got %r' % (code,))
564
 
        except (errors.InvalidHttpResponse, errors.ConnectionReset), e:
565
 
            raise errors.SmartProtocolError(str(e))
566
 
        return body_filelike
567
 
 
568
 
    def _report_activity(self, bytes, direction):
569
 
        """See SmartMedium._report_activity.
570
 
 
571
 
        Does nothing; the underlying plain HTTP transport will report the
572
 
        activity that this medium would report.
573
 
        """
574
 
        pass
575
 
 
576
 
    def disconnect(self):
577
 
        """See SmartClientMedium.disconnect()."""
578
 
        t = self._http_transport_ref()
579
 
        t.disconnect()
580
 
 
581
 
 
582
 
# TODO: May be better located in smart/medium.py with the other
583
 
# SmartMediumRequest classes
584
 
class SmartClientHTTPMediumRequest(medium.SmartClientMediumRequest):
585
 
    """A SmartClientMediumRequest that works with an HTTP medium."""
586
 
 
587
 
    def __init__(self, client_medium):
588
 
        medium.SmartClientMediumRequest.__init__(self, client_medium)
589
 
        self._buffer = ''
590
 
 
591
 
    def _accept_bytes(self, bytes):
592
 
        self._buffer += bytes
593
 
 
594
 
    def _finished_writing(self):
595
 
        data = self._medium.send_http_smart_request(self._buffer)
596
 
        self._response_body = data
597
 
 
598
 
    def _read_bytes(self, count):
599
 
        """See SmartClientMediumRequest._read_bytes."""
600
 
        return self._response_body.read(count)
601
 
 
602
 
    def _read_line(self):
603
 
        line, excess = medium._get_line(self._response_body.read)
604
 
        if excess != '':
605
 
            raise AssertionError(
606
 
                '_get_line returned excess bytes, but this mediumrequest '
607
 
                'cannot handle excess. (%r)' % (excess,))
608
 
        return line
609
 
 
610
 
    def _finished_reading(self):
611
 
        """See SmartClientMediumRequest._finished_reading."""
612
 
        pass
613
 
 
614
 
 
615
 
def unhtml_roughly(maybe_html, length_limit=1000):
616
 
    """Very approximate html->text translation, for presenting error bodies.
617
 
 
618
 
    :param length_limit: Truncate the result to this many characters.
619
 
 
620
 
    >>> unhtml_roughly("<b>bad</b> things happened\\n")
621
 
    ' bad  things happened '
622
 
    """
623
 
    return re.subn(r"(<[^>]*>|\n|&nbsp;)", " ", maybe_html)[0][:length_limit]
 
230
        raise TransportNotPossible('http does not support lock_write()')