~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/transport/http.py

- increment version

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2005, 2006 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
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
16
 
 
17
 
"""Base implementation of Transport over http.
18
 
 
19
 
There are separate implementation modules for each http client implementation.
 
16
"""Implementation of Transport over http.
20
17
"""
21
18
 
 
19
from bzrlib.transport import Transport, register_transport
 
20
from bzrlib.errors import (TransportNotPossible, NoSuchFile, 
 
21
                           NonRelativePath, TransportError)
 
22
import os, errno
22
23
from cStringIO import StringIO
23
 
import mimetools
24
 
import re
 
24
import urllib2
25
25
import urlparse
26
 
import urllib
27
 
import sys
28
26
 
29
 
from bzrlib import (
30
 
    errors,
31
 
    ui,
32
 
    urlutils,
33
 
    )
34
 
from bzrlib.smart import medium
35
 
from bzrlib.symbol_versioning import (
36
 
        deprecated_method,
37
 
        zero_seventeen,
38
 
        )
 
27
from bzrlib.errors import BzrError, BzrCheckError
 
28
from bzrlib.branch import Branch
39
29
from bzrlib.trace import mutter
40
 
from bzrlib.transport import (
41
 
    ConnectedTransport,
42
 
    _CoalescedOffset,
43
 
    Transport,
44
 
    )
45
 
 
46
 
# FIXME: There are two known cases where we open a new connection during the
47
 
# lifetime of the transport:
48
 
# - when an error is received from the server,
49
 
# - when the keep-alive header reach zero.
50
 
# This should be taken into account for the connection sharing by either
51
 
# failing without reconnecting or inform the clones that a new connection have
52
 
# been established.
53
 
 
54
 
# TODO: This is not used anymore by HttpTransport_urllib
55
 
# (extracting the auth info and prompting the user for a password
56
 
# have been split), only the tests still use it. It should be
57
 
# deleted and the tests rewritten ASAP to stay in sync.
58
 
def extract_auth(url, password_manager):
59
 
    """Extract auth parameters from am HTTP/HTTPS url and add them to the given
60
 
    password manager.  Return the url, minus those auth parameters (which
61
 
    confuse urllib2).
62
 
    """
63
 
    assert re.match(r'^(https?)(\+\w+)?://', url), \
64
 
            'invalid absolute url %r' % url
65
 
    scheme, netloc, path, query, fragment = urlparse.urlsplit(url)
66
 
 
67
 
    if '@' in netloc:
68
 
        auth, netloc = netloc.split('@', 1)
69
 
        if ':' in auth:
70
 
            username, password = auth.split(':', 1)
71
 
        else:
72
 
            username, password = auth, None
73
 
        if ':' in netloc:
74
 
            host = netloc.split(':', 1)[0]
75
 
        else:
76
 
            host = netloc
77
 
        username = urllib.unquote(username)
78
 
        if password is not None:
79
 
            password = urllib.unquote(password)
80
 
        else:
81
 
            password = ui.ui_factory.get_password(
82
 
                prompt='HTTP %(user)s@%(host)s password',
83
 
                user=username, host=host)
84
 
        password_manager.add_password(None, host, username, password)
85
 
    url = urlparse.urlunsplit((scheme, netloc, path, query, fragment))
86
 
    return url
87
 
 
88
 
 
89
 
def _extract_headers(header_text, url):
90
 
    """Extract the mapping for an rfc2822 header
91
 
 
92
 
    This is a helper function for the test suite and for _pycurl.
93
 
    (urllib already parses the headers for us)
94
 
 
95
 
    In the case that there are multiple headers inside the file,
96
 
    the last one is returned.
97
 
 
98
 
    :param header_text: A string of header information.
99
 
        This expects that the first line of a header will always be HTTP ...
100
 
    :param url: The url we are parsing, so we can raise nice errors
101
 
    :return: mimetools.Message object, which basically acts like a case 
102
 
        insensitive dictionary.
103
 
    """
104
 
    first_header = True
105
 
    remaining = header_text
106
 
 
107
 
    if not remaining:
108
 
        raise errors.InvalidHttpResponse(url, 'Empty headers')
109
 
 
110
 
    while remaining:
111
 
        header_file = StringIO(remaining)
112
 
        first_line = header_file.readline()
113
 
        if not first_line.startswith('HTTP'):
114
 
            if first_header: # The first header *must* start with HTTP
115
 
                raise errors.InvalidHttpResponse(url,
116
 
                    'Opening header line did not start with HTTP: %s'
117
 
                    % (first_line,))
118
 
            else:
119
 
                break # We are done parsing
120
 
        first_header = False
121
 
        m = mimetools.Message(header_file)
122
 
 
123
 
        # mimetools.Message parses the first header up to a blank line
124
 
        # So while there is remaining data, it probably means there is
125
 
        # another header to be parsed.
126
 
        # Get rid of any preceeding whitespace, which if it is all whitespace
127
 
        # will get rid of everything.
128
 
        remaining = header_file.read().lstrip()
129
 
    return m
130
 
 
131
 
 
132
 
class HttpTransportBase(ConnectedTransport, medium.SmartClientMedium):
133
 
    """Base class for http implementations.
134
 
 
135
 
    Does URL parsing, etc, but not any network IO.
136
 
 
137
 
    The protocol can be given as e.g. http+urllib://host/ to use a particular
138
 
    implementation.
139
 
    """
140
 
 
141
 
    # _unqualified_scheme: "http" or "https"
142
 
    # _scheme: may have "+pycurl", etc
143
 
 
144
 
    def __init__(self, base, from_transport=None):
 
30
 
 
31
# velocitynet.com.au transparently proxies connections and thereby
 
32
# breaks keep-alive -- sucks!
 
33
 
 
34
 
 
35
def get_url(url):
 
36
    import urllib2
 
37
    mutter("get_url %s" % url)
 
38
    url_f = urllib2.urlopen(url)
 
39
    return url_f
 
40
 
 
41
class HttpTransportError(TransportError):
 
42
    pass
 
43
 
 
44
class HttpTransport(Transport):
 
45
    """This is the transport agent for http:// access.
 
46
    
 
47
    TODO: Implement pipelined versions of all of the *_multi() functions.
 
48
    """
 
49
 
 
50
    def __init__(self, base):
145
51
        """Set the base path where files will be stored."""
146
 
        proto_match = re.match(r'^(https?)(\+\w+)?://', base)
147
 
        if not proto_match:
148
 
            raise AssertionError("not a http url: %r" % base)
149
 
        self._unqualified_scheme = proto_match.group(1)
150
 
        impl_name = proto_match.group(2)
151
 
        if impl_name:
152
 
            impl_name = impl_name[1:]
153
 
        self._impl_name = impl_name
154
 
        super(HttpTransportBase, self).__init__(base, from_transport)
155
 
        # range hint is handled dynamically throughout the life
156
 
        # of the transport object. We start by trying multi-range
157
 
        # requests and if the server returns bogus results, we
158
 
        # retry with single range requests and, finally, we
159
 
        # forget about range if the server really can't
160
 
        # understand. Once acquired, this piece of info is
161
 
        # propagated to clones.
162
 
        if from_transport is not None:
163
 
            self._range_hint = from_transport._range_hint
 
52
        assert base.startswith('http://') or base.startswith('https://')
 
53
        super(HttpTransport, self).__init__(base)
 
54
        # In the future we might actually connect to the remote host
 
55
        # rather than using get_url
 
56
        # self._connection = None
 
57
        (self._proto, self._host,
 
58
            self._path, self._parameters,
 
59
            self._query, self._fragment) = urlparse.urlparse(self.base)
 
60
 
 
61
    def should_cache(self):
 
62
        """Return True if the data pulled across should be cached locally.
 
63
        """
 
64
        return True
 
65
 
 
66
    def clone(self, offset=None):
 
67
        """Return a new HttpTransport with root at self.base + offset
 
68
        For now HttpTransport does not actually connect, so just return
 
69
        a new HttpTransport object.
 
70
        """
 
71
        if offset is None:
 
72
            return HttpTransport(self.base)
164
73
        else:
165
 
            self._range_hint = 'multi'
166
 
 
167
 
    def _remote_path(self, relpath):
168
 
        """Produce absolute path, adjusting protocol."""
169
 
        relative = urlutils.unescape(relpath).encode('utf-8')
170
 
        path = self._combine_paths(self._path, relative)
171
 
        return self._unsplit_url(self._unqualified_scheme,
172
 
                                 self._user, self._password,
173
 
                                 self._host, self._port,
174
 
                                 path)
 
74
            return HttpTransport(self.abspath(offset))
 
75
 
 
76
    def abspath(self, relpath):
 
77
        """Return the full url to the given relative path.
 
78
        This can be supplied with a string or a list
 
79
        """
 
80
        if isinstance(relpath, basestring):
 
81
            relpath = [relpath]
 
82
        basepath = self._path.split('/')
 
83
        if len(basepath) > 0 and basepath[-1] == '':
 
84
            basepath = basepath[:-1]
 
85
 
 
86
        for p in relpath:
 
87
            if p == '..':
 
88
                if len(basepath) < 0:
 
89
                    # In most filesystems, a request for the parent
 
90
                    # of root, just returns root.
 
91
                    continue
 
92
                if len(basepath) > 0:
 
93
                    basepath.pop()
 
94
            elif p == '.':
 
95
                continue # No-op
 
96
            else:
 
97
                basepath.append(p)
 
98
 
 
99
        # Possibly, we could use urlparse.urljoin() here, but
 
100
        # I'm concerned about when it chooses to strip the last
 
101
        # portion of the path, and when it doesn't.
 
102
        path = '/'.join(basepath)
 
103
        return urlparse.urlunparse((self._proto,
 
104
                self._host, path, '', '', ''))
 
105
 
 
106
    def relpath(self, abspath):
 
107
        if not abspath.startswith(self.base):
 
108
            raise NonRelativePath('path %r is not under base URL %r'
 
109
                           % (abspath, self.base))
 
110
        pl = len(self.base)
 
111
        return abspath[pl:].lstrip('/')
175
112
 
176
113
    def has(self, relpath):
177
 
        raise NotImplementedError("has() is abstract on %r" % self)
178
 
 
179
 
    def get(self, relpath):
 
114
        """Does the target location exist?
 
115
 
 
116
        TODO: HttpTransport.has() should use a HEAD request,
 
117
        not a full GET request.
 
118
 
 
119
        TODO: This should be changed so that we don't use
 
120
        urllib2 and get an exception, the code path would be
 
121
        cleaner if we just do an http HEAD request, and parse
 
122
        the return code.
 
123
        """
 
124
        try:
 
125
            f = get_url(self.abspath(relpath))
 
126
            # Without the read and then close()
 
127
            # we tend to have busy sockets.
 
128
            f.read()
 
129
            f.close()
 
130
            return True
 
131
        except BzrError:
 
132
            return False
 
133
        except urllib2.URLError:
 
134
            return False
 
135
        except IOError, e:
 
136
            if e.errno == errno.ENOENT:
 
137
                return False
 
138
            raise HttpTransportError(orig_error=e)
 
139
 
 
140
    def get(self, relpath, decode=False):
180
141
        """Get the file at the given relative path.
181
142
 
182
143
        :param relpath: The relative path to the file
183
144
        """
184
 
        code, response_file = self._get(relpath, None)
185
 
        return response_file
186
 
 
187
 
    def _get(self, relpath, ranges, tail_amount=0):
188
 
        """Get a file, or part of a file.
189
 
 
190
 
        :param relpath: Path relative to transport base URL
191
 
        :param ranges: None to get the whole file;
192
 
            or  a list of _CoalescedOffset to fetch parts of a file.
193
 
        :param tail_amount: The amount to get from the end of the file.
194
 
 
195
 
        :returns: (http_code, result_file)
196
 
        """
197
 
        raise NotImplementedError(self._get)
198
 
 
199
 
    def get_request(self):
200
 
        return SmartClientHTTPMediumRequest(self)
201
 
 
202
 
    def get_smart_medium(self):
203
 
        """See Transport.get_smart_medium.
204
 
 
205
 
        HttpTransportBase directly implements the minimal interface of
206
 
        SmartMediumClient, so this returns self.
207
 
        """
208
 
        return self
209
 
 
210
 
    def _degrade_range_hint(self, relpath, ranges, exc_info):
211
 
        if self._range_hint == 'multi':
212
 
            self._range_hint = 'single'
213
 
            mutter('Retry "%s" with single range request' % relpath)
214
 
        elif self._range_hint == 'single':
215
 
            self._range_hint = None
216
 
            mutter('Retry "%s" without ranges' % relpath)
217
 
        else:
218
 
            # We tried all the tricks, but nothing worked. We re-raise original
219
 
            # exception; the 'mutter' calls above will indicate that further
220
 
            # tries were unsuccessful
221
 
            raise exc_info[0], exc_info[1], exc_info[2]
222
 
 
223
 
    def _get_ranges_hinted(self, relpath, ranges):
224
 
        """Issue a ranged GET request taking server capabilities into account.
225
 
 
226
 
        Depending of the errors returned by the server, we try several GET
227
 
        requests, trying to minimize the data transferred.
228
 
 
229
 
        :param relpath: Path relative to transport base URL
230
 
        :param ranges: None to get the whole file;
231
 
            or  a list of _CoalescedOffset to fetch parts of a file.
232
 
        :returns: A file handle containing at least the requested ranges.
233
 
        """
234
 
        exc_info = None
235
 
        try_again = True
236
 
        while try_again:
237
 
            try_again = False
238
 
            try:
239
 
                code, f = self._get(relpath, ranges)
240
 
            except errors.InvalidRange, e:
241
 
                if exc_info is None:
242
 
                    exc_info = sys.exc_info()
243
 
                self._degrade_range_hint(relpath, ranges, exc_info)
244
 
                try_again = True
 
145
        try:
 
146
            return get_url(self.abspath(relpath))
 
147
        except (BzrError, urllib2.URLError, IOError), e:
 
148
            raise NoSuchFile(msg = "Error retrieving %s: %s" 
 
149
                             % (self.abspath(relpath), str(e)),
 
150
                             orig_error=e)
 
151
 
 
152
    def get_partial(self, relpath, start, length=None):
 
153
        """Get just part of a file.
 
154
 
 
155
        :param relpath: Path to the file, relative to base
 
156
        :param start: The starting position to read from
 
157
        :param length: The length to read. A length of None indicates
 
158
                       read to the end of the file.
 
159
        :return: A file-like object containing at least the specified bytes.
 
160
                 Some implementations may return objects which can be read
 
161
                 past this length, but this is not guaranteed.
 
162
        """
 
163
        # TODO: You can make specialized http requests for just
 
164
        # a portion of the file. Figure out how to do that.
 
165
        # For now, urllib2 returns files that cannot seek() so
 
166
        # we just read bytes off the beginning, until we
 
167
        # get to the point that we care about.
 
168
        f = self.get(relpath)
 
169
        # TODO: read in smaller chunks, in case things are
 
170
        # buffered internally.
 
171
        f.read(start)
245
172
        return f
246
173
 
247
 
    # _coalesce_offsets is a helper for readv, it try to combine ranges without
248
 
    # degrading readv performances. _bytes_to_read_before_seek is the value
249
 
    # used for the limit parameter and has been tuned for other transports. For
250
 
    # HTTP, the name is inappropriate but the parameter is still useful and
251
 
    # helps reduce the number of chunks in the response. The overhead for a
252
 
    # chunk (headers, length, footer around the data itself is variable but
253
 
    # around 50 bytes. We use 128 to reduce the range specifiers that appear in
254
 
    # the header, some servers (notably Apache) enforce a maximum length for a
255
 
    # header and issue a '400: Bad request' error when too much ranges are
256
 
    # specified.
257
 
    _bytes_to_read_before_seek = 128
258
 
    # No limit on the offset number that get combined into one, we are trying
259
 
    # to avoid downloading the whole file.
260
 
    _max_readv_combined = 0
261
 
 
262
 
    def readv(self, relpath, offsets):
263
 
        """Get parts of the file at the given relative path.
264
 
 
265
 
        :param offsets: A list of (offset, size) tuples.
266
 
        :param return: A list or generator of (offset, data) tuples
267
 
        """
268
 
        sorted_offsets = sorted(list(offsets))
269
 
        fudge = self._bytes_to_read_before_seek
270
 
        coalesced = self._coalesce_offsets(sorted_offsets,
271
 
                                           limit=self._max_readv_combine,
272
 
                                           fudge_factor=fudge)
273
 
        coalesced = list(coalesced)
274
 
        mutter('http readv of %s  offsets => %s collapsed %s',
275
 
                relpath, len(offsets), len(coalesced))
276
 
 
277
 
        f = self._get_ranges_hinted(relpath, coalesced)
278
 
        for start, size in offsets:
279
 
            try_again = True
280
 
            while try_again:
281
 
                try_again = False
282
 
                f.seek(start, ((start < 0) and 2) or 0)
283
 
                start = f.tell()
284
 
                try:
285
 
                    data = f.read(size)
286
 
                    if len(data) != size:
287
 
                        raise errors.ShortReadvError(relpath, start, size,
288
 
                                                     actual=len(data))
289
 
                except errors.ShortReadvError, e:
290
 
                    self._degrade_range_hint(relpath, coalesced, sys.exc_info())
291
 
 
292
 
                    # Since the offsets and the ranges may not be in the same
293
 
                    # order, we don't try to calculate a restricted single
294
 
                    # range encompassing unprocessed offsets.
295
 
 
296
 
                    # Note: we replace 'f' here, it may need cleaning one day
297
 
                    # before being thrown that way.
298
 
                    f = self._get_ranges_hinted(relpath, coalesced)
299
 
                    try_again = True
300
 
 
301
 
            # After one or more tries, we get the data.
302
 
            yield start, data
303
 
 
304
 
    @staticmethod
305
 
    @deprecated_method(zero_seventeen)
306
 
    def offsets_to_ranges(offsets):
307
 
        """Turn a list of offsets and sizes into a list of byte ranges.
308
 
 
309
 
        :param offsets: A list of tuples of (start, size).  An empty list
310
 
            is not accepted.
311
 
        :return: a list of inclusive byte ranges (start, end) 
312
 
            Adjacent ranges will be combined.
313
 
        """
314
 
        # Make sure we process sorted offsets
315
 
        offsets = sorted(offsets)
316
 
 
317
 
        prev_end = None
318
 
        combined = []
319
 
 
320
 
        for start, size in offsets:
321
 
            end = start + size - 1
322
 
            if prev_end is None:
323
 
                combined.append([start, end])
324
 
            elif start <= prev_end + 1:
325
 
                combined[-1][1] = end
326
 
            else:
327
 
                combined.append([start, end])
328
 
            prev_end = end
329
 
 
330
 
        return combined
331
 
 
332
 
    def _post(self, body_bytes):
333
 
        """POST body_bytes to .bzr/smart on this transport.
334
 
        
335
 
        :returns: (response code, response body file-like object).
336
 
        """
337
 
        # TODO: Requiring all the body_bytes to be available at the beginning of
338
 
        # the POST may require large client buffers.  It would be nice to have
339
 
        # an interface that allows streaming via POST when possible (and
340
 
        # degrades to a local buffer when not).
341
 
        raise NotImplementedError(self._post)
342
 
 
343
 
    def put_file(self, relpath, f, mode=None):
344
 
        """Copy the file-like object into the location.
 
174
    def put(self, relpath, f):
 
175
        """Copy the file-like or string object into the location.
345
176
 
346
177
        :param relpath: Location to put the contents, relative to base.
347
 
        :param f:       File-like object.
 
178
        :param f:       File-like or string object.
348
179
        """
349
 
        raise errors.TransportNotPossible('http PUT not supported')
 
180
        raise TransportNotPossible('http PUT not supported')
350
181
 
351
 
    def mkdir(self, relpath, mode=None):
 
182
    def mkdir(self, relpath):
352
183
        """Create a directory at the given path."""
353
 
        raise errors.TransportNotPossible('http does not support mkdir()')
354
 
 
355
 
    def rmdir(self, relpath):
356
 
        """See Transport.rmdir."""
357
 
        raise errors.TransportNotPossible('http does not support rmdir()')
358
 
 
359
 
    def append_file(self, relpath, f, mode=None):
 
184
        raise TransportNotPossible('http does not support mkdir()')
 
185
 
 
186
    def append(self, relpath, f):
360
187
        """Append the text in the file-like object into the final
361
188
        location.
362
189
        """
363
 
        raise errors.TransportNotPossible('http does not support append()')
 
190
        raise TransportNotPossible('http does not support append()')
364
191
 
365
192
    def copy(self, rel_from, rel_to):
366
193
        """Copy the item at rel_from to the location at rel_to"""
367
 
        raise errors.TransportNotPossible('http does not support copy()')
 
194
        raise TransportNotPossible('http does not support copy()')
368
195
 
369
 
    def copy_to(self, relpaths, other, mode=None, pb=None):
 
196
    def copy_to(self, relpaths, other, pb=None):
370
197
        """Copy a set of entries from self into another Transport.
371
198
 
372
199
        :param relpaths: A list/generator of entries to be copied.
377
204
        # At this point HttpTransport might be able to check and see if
378
205
        # the remote location is the same, and rather than download, and
379
206
        # then upload, it could just issue a remote copy_this command.
380
 
        if isinstance(other, HttpTransportBase):
381
 
            raise errors.TransportNotPossible(
382
 
                'http cannot be the target of copy_to()')
 
207
        if isinstance(other, HttpTransport):
 
208
            raise TransportNotPossible('http cannot be the target of copy_to()')
383
209
        else:
384
 
            return super(HttpTransportBase, self).\
385
 
                    copy_to(relpaths, other, mode=mode, pb=pb)
 
210
            return super(HttpTransport, self).copy_to(relpaths, other, pb=pb)
386
211
 
387
212
    def move(self, rel_from, rel_to):
388
213
        """Move the item at rel_from to the location at rel_to"""
389
 
        raise errors.TransportNotPossible('http does not support move()')
 
214
        raise TransportNotPossible('http does not support move()')
390
215
 
391
216
    def delete(self, relpath):
392
217
        """Delete the item at relpath"""
393
 
        raise errors.TransportNotPossible('http does not support delete()')
394
 
 
395
 
    def is_readonly(self):
396
 
        """See Transport.is_readonly."""
397
 
        return True
 
218
        raise TransportNotPossible('http does not support delete()')
398
219
 
399
220
    def listable(self):
400
221
        """See Transport.listable."""
403
224
    def stat(self, relpath):
404
225
        """Return the stat information for a file.
405
226
        """
406
 
        raise errors.TransportNotPossible('http does not support stat()')
 
227
        raise TransportNotPossible('http does not support stat()')
407
228
 
408
229
    def lock_read(self, relpath):
409
230
        """Lock the given file for shared (read) access.
424
245
 
425
246
        :return: A lock object, which should be passed to Transport.unlock()
426
247
        """
427
 
        raise errors.TransportNotPossible('http does not support lock_write()')
428
 
 
429
 
    def clone(self, offset=None):
430
 
        """Return a new HttpTransportBase with root at self.base + offset
431
 
 
432
 
        We leave the daughter classes take advantage of the hint
433
 
        that it's a cloning not a raw creation.
434
 
        """
435
 
        if offset is None:
436
 
            return self.__class__(self.base, self)
437
 
        else:
438
 
            return self.__class__(self.abspath(offset), self)
439
 
 
440
 
    def _attempted_range_header(self, offsets, tail_amount):
441
 
        """Prepare a HTTP Range header at a level the server should accept"""
442
 
 
443
 
        if self._range_hint == 'multi':
444
 
            # Nothing to do here
445
 
            return self._range_header(offsets, tail_amount)
446
 
        elif self._range_hint == 'single':
447
 
            # Combine all the requested ranges into a single
448
 
            # encompassing one
449
 
            if len(offsets) > 0:
450
 
                if tail_amount not in (0, None):
451
 
                    # Nothing we can do here to combine ranges with tail_amount
452
 
                    # in a single range, just returns None. The whole file
453
 
                    # should be downloaded.
454
 
                    return None
455
 
                else:
456
 
                    start = offsets[0].start
457
 
                    last = offsets[-1]
458
 
                    end = last.start + last.length - 1
459
 
                    whole = self._coalesce_offsets([(start, end - start + 1)],
460
 
                                                   limit=0, fudge_factor=0)
461
 
                    return self._range_header(list(whole), 0)
462
 
            else:
463
 
                # Only tail_amount, requested, leave range_header
464
 
                # do its work
465
 
                return self._range_header(offsets, tail_amount)
466
 
        else:
467
 
            return None
468
 
 
469
 
    @staticmethod
470
 
    def _range_header(ranges, tail_amount):
471
 
        """Turn a list of bytes ranges into a HTTP Range header value.
472
 
 
473
 
        :param ranges: A list of _CoalescedOffset
474
 
        :param tail_amount: The amount to get from the end of the file.
475
 
 
476
 
        :return: HTTP range header string.
477
 
 
478
 
        At least a non-empty ranges *or* a tail_amount must be
479
 
        provided.
480
 
        """
481
 
        strings = []
482
 
        for offset in ranges:
483
 
            strings.append('%d-%d' % (offset.start,
484
 
                                      offset.start + offset.length - 1))
485
 
 
486
 
        if tail_amount:
487
 
            strings.append('-%d' % tail_amount)
488
 
 
489
 
        return ','.join(strings)
490
 
 
491
 
    def send_http_smart_request(self, bytes):
492
 
        code, body_filelike = self._post(bytes)
493
 
        assert code == 200, 'unexpected HTTP response code %r' % (code,)
494
 
        return body_filelike
495
 
 
496
 
 
497
 
class SmartClientHTTPMediumRequest(medium.SmartClientMediumRequest):
498
 
    """A SmartClientMediumRequest that works with an HTTP medium."""
499
 
 
500
 
    def __init__(self, client_medium):
501
 
        medium.SmartClientMediumRequest.__init__(self, client_medium)
502
 
        self._buffer = ''
503
 
 
504
 
    def _accept_bytes(self, bytes):
505
 
        self._buffer += bytes
506
 
 
507
 
    def _finished_writing(self):
508
 
        data = self._medium.send_http_smart_request(self._buffer)
509
 
        self._response_body = data
510
 
 
511
 
    def _read_bytes(self, count):
512
 
        return self._response_body.read(count)
513
 
 
514
 
    def _finished_reading(self):
515
 
        """See SmartClientMediumRequest._finished_reading."""
516
 
        pass
 
248
        raise TransportNotPossible('http does not support lock_write()')
 
249
 
 
250
register_transport('http://', HttpTransport)
 
251
register_transport('https://', HttpTransport)