~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/transport/http.py

[merge] fix \t in commit messages

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