~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/transport/http.py

  • Committer: Martin Pool
  • Date: 2005-09-30 05:56:05 UTC
  • mto: (1185.14.2)
  • mto: This revision was merged to the branch mainline in revision 1396.
  • Revision ID: mbp@sourcefrog.net-20050930055605-a2c534529b392a7d
- fix upgrade for transport changes

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 errors, ui
 
27
from bzrlib.errors import BzrError, BzrCheckError
 
28
from bzrlib.branch import Branch
30
29
from bzrlib.trace import mutter
31
 
from bzrlib.transport import (
32
 
    smart,
33
 
    Transport,
34
 
    )
35
 
 
36
 
 
37
 
# TODO: This is not used anymore by HttpTransport_urllib
38
 
# (extracting the auth info and prompting the user for a password
39
 
# have been split), only the tests still use it. It should be
40
 
# deleted and the tests rewritten ASAP to stay in sync.
41
 
def extract_auth(url, password_manager):
42
 
    """Extract auth parameters from am HTTP/HTTPS url and add them to the given
43
 
    password manager.  Return the url, minus those auth parameters (which
44
 
    confuse urllib2).
45
 
    """
46
 
    assert re.match(r'^(https?)(\+\w+)?://', url), \
47
 
            'invalid absolute url %r' % url
48
 
    scheme, netloc, path, query, fragment = urlparse.urlsplit(url)
49
 
 
50
 
    if '@' in netloc:
51
 
        auth, netloc = netloc.split('@', 1)
52
 
        if ':' in auth:
53
 
            username, password = auth.split(':', 1)
54
 
        else:
55
 
            username, password = auth, None
56
 
        if ':' in netloc:
57
 
            host = netloc.split(':', 1)[0]
58
 
        else:
59
 
            host = netloc
60
 
        username = urllib.unquote(username)
61
 
        if password is not None:
62
 
            password = urllib.unquote(password)
63
 
        else:
64
 
            password = ui.ui_factory.get_password(
65
 
                prompt='HTTP %(user)s@%(host)s password',
66
 
                user=username, host=host)
67
 
        password_manager.add_password(None, host, username, password)
68
 
    url = urlparse.urlunsplit((scheme, netloc, path, query, fragment))
69
 
    return url
70
 
 
71
 
 
72
 
def _extract_headers(header_text, url):
73
 
    """Extract the mapping for an rfc2822 header
74
 
 
75
 
    This is a helper function for the test suite and for _pycurl.
76
 
    (urllib already parses the headers for us)
77
 
 
78
 
    In the case that there are multiple headers inside the file,
79
 
    the last one is returned.
80
 
 
81
 
    :param header_text: A string of header information.
82
 
        This expects that the first line of a header will always be HTTP ...
83
 
    :param url: The url we are parsing, so we can raise nice errors
84
 
    :return: mimetools.Message object, which basically acts like a case 
85
 
        insensitive dictionary.
86
 
    """
87
 
    first_header = True
88
 
    remaining = header_text
89
 
 
90
 
    if not remaining:
91
 
        raise errors.InvalidHttpResponse(url, 'Empty headers')
92
 
 
93
 
    while remaining:
94
 
        header_file = StringIO(remaining)
95
 
        first_line = header_file.readline()
96
 
        if not first_line.startswith('HTTP'):
97
 
            if first_header: # The first header *must* start with HTTP
98
 
                raise errors.InvalidHttpResponse(url,
99
 
                    'Opening header line did not start with HTTP: %s'
100
 
                    % (first_line,))
101
 
                assert False, 'Opening header line was not HTTP'
102
 
            else:
103
 
                break # We are done parsing
104
 
        first_header = False
105
 
        m = mimetools.Message(header_file)
106
 
 
107
 
        # mimetools.Message parses the first header up to a blank line
108
 
        # So while there is remaining data, it probably means there is
109
 
        # another header to be parsed.
110
 
        # Get rid of any preceeding whitespace, which if it is all whitespace
111
 
        # will get rid of everything.
112
 
        remaining = header_file.read().lstrip()
113
 
    return m
114
 
 
115
 
 
116
 
class HttpTransportBase(Transport, smart.SmartClientMedium):
117
 
    """Base class for http implementations.
118
 
 
119
 
    Does URL parsing, etc, but not any network IO.
120
 
 
121
 
    The protocol can be given as e.g. http+urllib://host/ to use a particular
122
 
    implementation.
123
 
    """
124
 
 
125
 
    # _proto: "http" or "https"
126
 
    # _qualified_proto: may have "+pycurl", etc
127
 
 
128
 
    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):
129
51
        """Set the base path where files will be stored."""
130
 
        proto_match = re.match(r'^(https?)(\+\w+)?://', base)
131
 
        if not proto_match:
132
 
            raise AssertionError("not a http url: %r" % base)
133
 
        self._proto = proto_match.group(1)
134
 
        impl_name = proto_match.group(2)
135
 
        if impl_name:
136
 
            impl_name = impl_name[1:]
137
 
        self._impl_name = impl_name
138
 
        if base[-1] != '/':
139
 
            base = base + '/'
140
 
        super(HttpTransportBase, self).__init__(base)
141
 
        (apparent_proto, self._host,
 
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,
142
58
            self._path, self._parameters,
143
59
            self._query, self._fragment) = urlparse.urlparse(self.base)
144
 
        self._qualified_proto = apparent_proto
145
 
        # range hint is handled dynamically throughout the life
146
 
        # of the object. We start by trying mulri-range requests
147
 
        # and if the server returns bougs results, we retry with
148
 
        # single range requests and, finally, we forget about
149
 
        # range if the server really can't understand. Once
150
 
        # aquired, this piece of info is propogated to clones.
151
 
        if from_transport is not None:
152
 
            self._range_hint = from_transport._range_hint
 
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)
153
73
        else:
154
 
            self._range_hint = 'multi'
 
74
            return HttpTransport(self.abspath(offset))
155
75
 
156
76
    def abspath(self, relpath):
157
77
        """Return the full url to the given relative path.
158
 
 
159
 
        This can be supplied with a string or a list.
160
 
 
161
 
        The URL returned always has the protocol scheme originally used to 
162
 
        construct the transport, even if that includes an explicit
163
 
        implementation qualifier.
 
78
        This can be supplied with a string or a list
164
79
        """
165
 
        assert isinstance(relpath, basestring)
166
 
        if isinstance(relpath, unicode):
167
 
            raise errors.InvalidURL(relpath, 'paths must not be unicode.')
168
80
        if isinstance(relpath, basestring):
169
 
            relpath_parts = relpath.split('/')
170
 
        else:
171
 
            # TODO: Don't call this with an array - no magic interfaces
172
 
            relpath_parts = relpath[:]
173
 
        if relpath.startswith('/'):
174
 
            basepath = []
175
 
        else:
176
 
            # Except for the root, no trailing slashes are allowed
177
 
            if len(relpath_parts) > 1 and relpath_parts[-1] == '':
178
 
                raise ValueError(
179
 
                    "path %r within branch %r seems to be a directory"
180
 
                    % (relpath, self._path))
181
 
            basepath = self._path.split('/')
182
 
            if len(basepath) > 0 and basepath[-1] == '':
183
 
                basepath = basepath[:-1]
 
81
            relpath = [relpath]
 
82
        basepath = self._path.split('/')
 
83
        if len(basepath) > 0 and basepath[-1] == '':
 
84
            basepath = basepath[:-1]
184
85
 
185
 
        for p in relpath_parts:
 
86
        for p in relpath:
186
87
            if p == '..':
187
 
                if len(basepath) == 0:
 
88
                if len(basepath) < 0:
188
89
                    # In most filesystems, a request for the parent
189
90
                    # of root, just returns root.
190
91
                    continue
191
92
                basepath.pop()
192
 
            elif p == '.' or p == '':
 
93
            elif p == '.':
193
94
                continue # No-op
194
95
            else:
195
96
                basepath.append(p)
 
97
 
196
98
        # Possibly, we could use urlparse.urljoin() here, but
197
99
        # I'm concerned about when it chooses to strip the last
198
100
        # portion of the path, and when it doesn't.
199
101
        path = '/'.join(basepath)
200
 
        if path == '':
201
 
            path = '/'
202
 
        result = urlparse.urlunparse((self._qualified_proto,
203
 
                                    self._host, path, '', '', ''))
204
 
        return result
 
102
        return urlparse.urlunparse((self._proto,
 
103
                self._host, path, '', '', ''))
205
104
 
206
 
    def _real_abspath(self, relpath):
207
 
        """Produce absolute path, adjusting protocol if needed"""
208
 
        abspath = self.abspath(relpath)
209
 
        qp = self._qualified_proto
210
 
        rp = self._proto
211
 
        if self._qualified_proto != self._proto:
212
 
            abspath = rp + abspath[len(qp):]
213
 
        if not isinstance(abspath, str):
214
 
            # escaping must be done at a higher level
215
 
            abspath = abspath.encode('ascii')
216
 
        return abspath
 
105
    def relpath(self, abspath):
 
106
        if not abspath.startswith(self.base):
 
107
            raise NonRelativePath('path %r is not under base URL %r'
 
108
                           % (abspath, self.base))
 
109
        pl = len(self.base)
 
110
        return abspath[pl:].lstrip('/')
217
111
 
218
112
    def has(self, relpath):
219
 
        raise NotImplementedError("has() is abstract on %r" % self)
220
 
 
221
 
    def get(self, relpath):
 
113
        """Does the target location exist?
 
114
 
 
115
        TODO: HttpTransport.has() should use a HEAD request,
 
116
        not a full GET request.
 
117
 
 
118
        TODO: This should be changed so that we don't use
 
119
        urllib2 and get an exception, the code path would be
 
120
        cleaner if we just do an http HEAD request, and parse
 
121
        the return code.
 
122
        """
 
123
        try:
 
124
            f = get_url(self.abspath(relpath))
 
125
            # Without the read and then close()
 
126
            # we tend to have busy sockets.
 
127
            f.read()
 
128
            f.close()
 
129
            return True
 
130
        except BzrError:
 
131
            return False
 
132
        except urllib2.URLError:
 
133
            return False
 
134
        except IOError, e:
 
135
            if e.errno == errno.ENOENT:
 
136
                return False
 
137
            raise HttpTransportError(orig_error=e)
 
138
 
 
139
    def get(self, relpath, decode=False):
222
140
        """Get the file at the given relative path.
223
141
 
224
142
        :param relpath: The relative path to the file
225
143
        """
226
 
        code, response_file = self._get(relpath, None)
227
 
        return response_file
228
 
 
229
 
    def _get(self, relpath, ranges):
230
 
        """Get a file, or part of a file.
231
 
 
232
 
        :param relpath: Path relative to transport base URL
233
 
        :param byte_range: None to get the whole file;
234
 
            or [(start,end)] to fetch parts of a file.
235
 
 
236
 
        :returns: (http_code, result_file)
237
 
 
238
 
        Note that the current http implementations can only fetch one range at
239
 
        a time through this call.
240
 
        """
241
 
        raise NotImplementedError(self._get)
242
 
 
243
 
    def get_request(self):
244
 
        return SmartClientHTTPMediumRequest(self)
245
 
 
246
 
    def get_smart_medium(self):
247
 
        """See Transport.get_smart_medium.
248
 
 
249
 
        HttpTransportBase directly implements the minimal interface of
250
 
        SmartMediumClient, so this returns self.
251
 
        """
252
 
        return self
253
 
 
254
 
    def _retry_get(self, relpath, ranges, exc_info):
255
 
        """A GET request have failed, let's retry with a simpler request."""
256
 
 
257
 
        try_again = False
258
 
        # The server does not gives us enough data or
259
 
        # bogus-looking result, let's try again with
260
 
        # a simpler request if possible.
261
 
        if self._range_hint == 'multi':
262
 
            self._range_hint = 'single'
263
 
            mutter('Retry %s with single range request' % relpath)
264
 
            try_again = True
265
 
        elif self._range_hint == 'single':
266
 
            self._range_hint = None
267
 
            mutter('Retry %s without ranges' % relpath)
268
 
            try_again = True
269
 
        if try_again:
270
 
            # Note that since the offsets and the ranges may not
271
 
            # be in the same order, we don't try to calculate a
272
 
            # restricted single range encompassing unprocessed
273
 
            # offsets.
274
 
            code, f = self._get(relpath, ranges)
275
 
            return try_again, code, f
276
 
        else:
277
 
            # We tried all the tricks, but nothing worked. We
278
 
            # re-raise original exception; the 'mutter' calls
279
 
            # above will indicate that further tries were
280
 
            # unsuccessful
281
 
            raise exc_info[0], exc_info[1], exc_info[2]
282
 
 
283
 
    def readv(self, relpath, offsets):
284
 
        """Get parts of the file at the given relative path.
285
 
 
286
 
        :param offsets: A list of (offset, size) tuples.
287
 
        :param return: A list or generator of (offset, data) tuples
288
 
        """
289
 
        ranges = self.offsets_to_ranges(offsets)
290
 
        mutter('http readv of %s collapsed %s offsets => %s',
291
 
                relpath, len(offsets), ranges)
292
 
 
293
 
        try_again = True
294
 
        while try_again:
295
 
            try_again = False
296
 
            try:
297
 
                code, f = self._get(relpath, ranges)
298
 
            except (errors.InvalidRange, errors.ShortReadvError), e:
299
 
                try_again, code, f = self._retry_get(relpath, ranges,
300
 
                                                     sys.exc_info())
301
 
 
302
 
        for start, size in offsets:
303
 
            try_again = True
304
 
            while try_again:
305
 
                try_again = False
306
 
                f.seek(start, (start < 0) and 2 or 0)
307
 
                start = f.tell()
308
 
                try:
309
 
                    data = f.read(size)
310
 
                    if len(data) != size:
311
 
                        raise errors.ShortReadvError(relpath, start, size,
312
 
                                                     actual=len(data))
313
 
                except (errors.InvalidRange, errors.ShortReadvError), e:
314
 
                    # Note that we replace 'f' here and that it
315
 
                    # may need cleaning one day before being
316
 
                    # thrown that way.
317
 
                    try_again, code, f = self._retry_get(relpath, ranges,
318
 
                                                         sys.exc_info())
319
 
            # After one or more tries, we get the data.
320
 
            yield start, data
321
 
 
322
 
    @staticmethod
323
 
    def offsets_to_ranges(offsets):
324
 
        """Turn a list of offsets and sizes into a list of byte ranges.
325
 
 
326
 
        :param offsets: A list of tuples of (start, size).  An empty list
327
 
            is not accepted.
328
 
        :return: a list of inclusive byte ranges (start, end) 
329
 
            Adjacent ranges will be combined.
330
 
        """
331
 
        # Make sure we process sorted offsets
332
 
        offsets = sorted(offsets)
333
 
 
334
 
        prev_end = None
335
 
        combined = []
336
 
 
337
 
        for start, size in offsets:
338
 
            end = start + size - 1
339
 
            if prev_end is None:
340
 
                combined.append([start, end])
341
 
            elif start <= prev_end + 1:
342
 
                combined[-1][1] = end
343
 
            else:
344
 
                combined.append([start, end])
345
 
            prev_end = end
346
 
 
347
 
        return combined
348
 
 
349
 
    def _post(self, body_bytes):
350
 
        """POST body_bytes to .bzr/smart on this transport.
351
 
        
352
 
        :returns: (response code, response body file-like object).
353
 
        """
354
 
        # TODO: Requiring all the body_bytes to be available at the beginning of
355
 
        # the POST may require large client buffers.  It would be nice to have
356
 
        # an interface that allows streaming via POST when possible (and
357
 
        # degrades to a local buffer when not).
358
 
        raise NotImplementedError(self._post)
359
 
 
360
 
    def put_file(self, relpath, f, mode=None):
361
 
        """Copy the file-like object into the location.
 
144
        try:
 
145
            return get_url(self.abspath(relpath))
 
146
        except (BzrError, urllib2.URLError, IOError), e:
 
147
            raise NoSuchFile(orig_error=e)
 
148
        except Exception,e:
 
149
            raise HttpTransportError(orig_error=e)
 
150
 
 
151
    def get_partial(self, relpath, start, length=None):
 
152
        """Get just part of a file.
 
153
 
 
154
        :param relpath: Path to the file, relative to base
 
155
        :param start: The starting position to read from
 
156
        :param length: The length to read. A length of None indicates
 
157
                       read to the end of the file.
 
158
        :return: A file-like object containing at least the specified bytes.
 
159
                 Some implementations may return objects which can be read
 
160
                 past this length, but this is not guaranteed.
 
161
        """
 
162
        # TODO: You can make specialized http requests for just
 
163
        # a portion of the file. Figure out how to do that.
 
164
        # For now, urllib2 returns files that cannot seek() so
 
165
        # we just read bytes off the beginning, until we
 
166
        # get to the point that we care about.
 
167
        f = self.get(relpath)
 
168
        # TODO: read in smaller chunks, in case things are
 
169
        # buffered internally.
 
170
        f.read(start)
 
171
        return f
 
172
 
 
173
    def put(self, relpath, f):
 
174
        """Copy the file-like or string object into the location.
362
175
 
363
176
        :param relpath: Location to put the contents, relative to base.
364
 
        :param f:       File-like object.
 
177
        :param f:       File-like or string object.
365
178
        """
366
 
        raise errors.TransportNotPossible('http PUT not supported')
 
179
        raise TransportNotPossible('http PUT not supported')
367
180
 
368
 
    def mkdir(self, relpath, mode=None):
 
181
    def mkdir(self, relpath):
369
182
        """Create a directory at the given path."""
370
 
        raise errors.TransportNotPossible('http does not support mkdir()')
371
 
 
372
 
    def rmdir(self, relpath):
373
 
        """See Transport.rmdir."""
374
 
        raise errors.TransportNotPossible('http does not support rmdir()')
375
 
 
376
 
    def append_file(self, relpath, f, mode=None):
 
183
        raise TransportNotPossible('http does not support mkdir()')
 
184
 
 
185
    def append(self, relpath, f):
377
186
        """Append the text in the file-like object into the final
378
187
        location.
379
188
        """
380
 
        raise errors.TransportNotPossible('http does not support append()')
 
189
        raise TransportNotPossible('http does not support append()')
381
190
 
382
191
    def copy(self, rel_from, rel_to):
383
192
        """Copy the item at rel_from to the location at rel_to"""
384
 
        raise errors.TransportNotPossible('http does not support copy()')
 
193
        raise TransportNotPossible('http does not support copy()')
385
194
 
386
 
    def copy_to(self, relpaths, other, mode=None, pb=None):
 
195
    def copy_to(self, relpaths, other, pb=None):
387
196
        """Copy a set of entries from self into another Transport.
388
197
 
389
198
        :param relpaths: A list/generator of entries to be copied.
394
203
        # At this point HttpTransport might be able to check and see if
395
204
        # the remote location is the same, and rather than download, and
396
205
        # then upload, it could just issue a remote copy_this command.
397
 
        if isinstance(other, HttpTransportBase):
398
 
            raise errors.TransportNotPossible(
399
 
                'http cannot be the target of copy_to()')
 
206
        if isinstance(other, HttpTransport):
 
207
            raise TransportNotPossible('http cannot be the target of copy_to()')
400
208
        else:
401
 
            return super(HttpTransportBase, self).\
402
 
                    copy_to(relpaths, other, mode=mode, pb=pb)
 
209
            return super(HttpTransport, self).copy_to(relpaths, other, pb=pb)
403
210
 
404
211
    def move(self, rel_from, rel_to):
405
212
        """Move the item at rel_from to the location at rel_to"""
406
 
        raise errors.TransportNotPossible('http does not support move()')
 
213
        raise TransportNotPossible('http does not support move()')
407
214
 
408
215
    def delete(self, relpath):
409
216
        """Delete the item at relpath"""
410
 
        raise errors.TransportNotPossible('http does not support delete()')
411
 
 
412
 
    def is_readonly(self):
413
 
        """See Transport.is_readonly."""
414
 
        return True
415
 
 
416
 
    def listable(self):
417
 
        """See Transport.listable."""
418
 
        return False
 
217
        raise TransportNotPossible('http does not support delete()')
 
218
 
 
219
    def list_dir(self, relpath):
 
220
        """Return a list of all files at the given location.
 
221
        WARNING: many transports do not support this, so trying avoid using
 
222
        it if at all possible.
 
223
        """
 
224
        raise TransportNotPossible('http does not support list_dir()')
419
225
 
420
226
    def stat(self, relpath):
421
227
        """Return the stat information for a file.
422
228
        """
423
 
        raise errors.TransportNotPossible('http does not support stat()')
 
229
        raise TransportNotPossible('http does not support stat()')
424
230
 
425
231
    def lock_read(self, relpath):
426
232
        """Lock the given file for shared (read) access.
441
247
 
442
248
        :return: A lock object, which should be passed to Transport.unlock()
443
249
        """
444
 
        raise errors.TransportNotPossible('http does not support lock_write()')
445
 
 
446
 
    def clone(self, offset=None):
447
 
        """Return a new HttpTransportBase with root at self.base + offset
448
 
 
449
 
        We leave the daughter classes take advantage of the hint
450
 
        that it's a cloning not a raw creation.
451
 
        """
452
 
        if offset is None:
453
 
            return self.__class__(self.base, self)
454
 
        else:
455
 
            return self.__class__(self.abspath(offset), self)
456
 
 
457
 
    def attempted_range_header(self, ranges, tail_amount):
458
 
        """Prepare a HTTP Range header at a level the server should accept"""
459
 
 
460
 
        if self._range_hint == 'multi':
461
 
            # Nothing to do here
462
 
            return self.range_header(ranges, tail_amount)
463
 
        elif self._range_hint == 'single':
464
 
            # Combine all the requested ranges into a single
465
 
            # encompassing one
466
 
            if len(ranges) > 0:
467
 
                start, ignored = ranges[0]
468
 
                ignored, end = ranges[-1]
469
 
                if tail_amount not in (0, None):
470
 
                    # Nothing we can do here to combine ranges
471
 
                    # with tail_amount, just returns None. The
472
 
                    # whole file should be downloaded.
473
 
                    return None
474
 
                else:
475
 
                    return self.range_header([(start, end)], 0)
476
 
            else:
477
 
                # Only tail_amount, requested, leave range_header
478
 
                # do its work
479
 
                return self.range_header(ranges, tail_amount)
480
 
        else:
481
 
            return None
482
 
 
483
 
    @staticmethod
484
 
    def range_header(ranges, tail_amount):
485
 
        """Turn a list of bytes ranges into a HTTP Range header value.
486
 
 
487
 
        :param ranges: A list of byte ranges, (start, end).
488
 
        :param tail_amount: The amount to get from the end of the file.
489
 
 
490
 
        :return: HTTP range header string.
491
 
 
492
 
        At least a non-empty ranges *or* a tail_amount must be
493
 
        provided.
494
 
        """
495
 
        strings = []
496
 
        for start, end in ranges:
497
 
            strings.append('%d-%d' % (start, end))
498
 
 
499
 
        if tail_amount:
500
 
            strings.append('-%d' % tail_amount)
501
 
 
502
 
        return ','.join(strings)
503
 
 
504
 
    def send_http_smart_request(self, bytes):
505
 
        code, body_filelike = self._post(bytes)
506
 
        assert code == 200, 'unexpected HTTP response code %r' % (code,)
507
 
        return body_filelike
508
 
 
509
 
 
510
 
class SmartClientHTTPMediumRequest(smart.SmartClientMediumRequest):
511
 
    """A SmartClientMediumRequest that works with an HTTP medium."""
512
 
 
513
 
    def __init__(self, medium):
514
 
        smart.SmartClientMediumRequest.__init__(self, medium)
515
 
        self._buffer = ''
516
 
 
517
 
    def _accept_bytes(self, bytes):
518
 
        self._buffer += bytes
519
 
 
520
 
    def _finished_writing(self):
521
 
        data = self._medium.send_http_smart_request(self._buffer)
522
 
        self._response_body = data
523
 
 
524
 
    def _read_bytes(self, count):
525
 
        return self._response_body.read(count)
526
 
 
527
 
    def _finished_reading(self):
528
 
        """See SmartClientMediumRequest._finished_reading."""
529
 
        pass
 
250
        raise TransportNotPossible('http does not support lock_write()')
 
251
 
 
252
register_transport('http://', HttpTransport)
 
253
register_transport('https://', HttpTransport)
 
254