/brz/remove-bazaar

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

« back to all changes in this revision

Viewing changes to bzrlib/transport/http/response.py

  • Committer: Marius Kruger
  • Date: 2010-07-10 21:28:56 UTC
  • mto: (5384.1.1 integration)
  • mto: This revision was merged to the branch mainline in revision 5385.
  • Revision ID: marius.kruger@enerweb.co.za-20100710212856-uq4ji3go0u5se7hx
* Update documentation
* add NEWS

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2006-2011 Canonical Ltd
 
1
# Copyright (C) 2006, 2007 Canonical Ltd
2
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
21
21
responses.
22
22
"""
23
23
 
24
 
from __future__ import absolute_import
25
 
 
26
 
import cgi
27
 
import os
28
 
try:
29
 
    import http.client as http_client
30
 
except ImportError:  # python < 3
31
 
    import httplib as http_client
32
 
try:
33
 
    import email.utils as email_utils
34
 
except ImportError:  # python < 3
35
 
    import rfc822 as email_utils
36
 
 
37
 
from ... import (
 
24
 
 
25
import httplib
 
26
from cStringIO import StringIO
 
27
import rfc822
 
28
 
 
29
from bzrlib import (
38
30
    errors,
 
31
    trace,
39
32
    osutils,
40
33
    )
41
 
from ...sixish import (
42
 
    BytesIO,
43
 
    PY3,
44
 
    )
45
 
 
46
 
 
47
 
class ResponseFile(object):
48
 
    """A wrapper around the http socket containing the result of a GET request.
49
 
 
50
 
    Only read() and seek() (forward) are supported.
51
 
 
52
 
    """
53
 
 
54
 
    def __init__(self, path, infile):
55
 
        """Constructor.
56
 
 
57
 
        :param path: File url, for error reports.
58
 
 
59
 
        :param infile: File-like socket set at body start.
60
 
        """
61
 
        self._path = path
62
 
        self._file = infile
63
 
        self._pos = 0
64
 
 
65
 
    def close(self):
66
 
        """Close this file.
67
 
 
68
 
        Dummy implementation for consistency with the 'file' API.
69
 
        """
70
 
 
71
 
    def __enter__(self):
72
 
        return self
73
 
 
74
 
    def __exit__(self, exc_type, exc_val, exc_tb):
75
 
        return False  # propogate exceptions.
76
 
 
77
 
    def read(self, size=None):
78
 
        """Read size bytes from the current position in the file.
79
 
 
80
 
        :param size:  The number of bytes to read.  Leave unspecified or pass
81
 
            -1 to read to EOF.
82
 
        """
83
 
        if size is None and not PY3:
84
 
            size = -1
85
 
        data = self._file.read(size)
86
 
        self._pos += len(data)
87
 
        return data
88
 
 
89
 
    def readline(self):
90
 
        data = self._file.readline()
91
 
        self._pos += len(data)
92
 
        return data
93
 
 
94
 
    def __iter__(self):
95
 
        while True:
96
 
            line = self.readline()
97
 
            if not line:
98
 
                return
99
 
            yield line
100
 
 
101
 
    def tell(self):
102
 
        return self._pos
103
 
 
104
 
    def seek(self, offset, whence=os.SEEK_SET):
105
 
        if whence == os.SEEK_SET:
106
 
            if offset < self._pos:
107
 
                raise AssertionError(
108
 
                    "Can't seek backwards, pos: %s, offset: %s"
109
 
                    % (self._pos, offset))
110
 
            to_discard = offset - self._pos
111
 
        elif whence == os.SEEK_CUR:
112
 
            to_discard = offset
113
 
        else:
114
 
            raise AssertionError("Can't seek backwards")
115
 
        if to_discard:
116
 
            # Just discard the unwanted bytes
117
 
            self.read(to_discard)
 
34
 
118
35
 
119
36
# A RangeFile expects the following grammar (simplified to outline the
120
37
# assumptions we rely upon).
121
38
 
122
 
# file: single_range
 
39
# file: whole_file
 
40
#     | single_range
123
41
#     | multiple_range
124
42
 
 
43
# whole_file: [content_length_header] data
 
44
 
125
45
# single_range: content_range_header data
126
46
 
127
47
# multiple_range: boundary_header boundary (content_range_header data boundary)+
128
48
 
129
 
 
130
 
class RangeFile(ResponseFile):
 
49
class RangeFile(object):
131
50
    """File-like object that allow access to partial available data.
132
51
 
133
52
    All accesses should happen sequentially since the acquisition occurs during
141
60
 
142
61
    # in _checked_read() below, we may have to discard several MB in the worst
143
62
    # case. To avoid buffering that much, we read and discard by chunks
144
 
    # instead. The underlying file is either a socket or a BytesIO, so reading
 
63
    # instead. The underlying file is either a socket or a StringIO, so reading
145
64
    # 8k chunks should be fine.
146
65
    _discarded_buf_size = 8192
147
66
 
152
71
        """Constructor.
153
72
 
154
73
        :param path: File url, for error reports.
155
 
 
156
74
        :param infile: File-like socket set at body start.
157
75
        """
158
 
        super(RangeFile, self).__init__(path, infile)
 
76
        self._path = path
 
77
        self._file = infile
159
78
        self._boundary = None
160
79
        # When using multi parts response, this will be set with the headers
161
80
        # associated with the range currently read.
176
95
        The file should be at the beginning of the body, the first range
177
96
        definition is read and taken into account.
178
97
        """
179
 
        if not isinstance(boundary, bytes):
180
 
            raise TypeError(boundary)
181
98
        self._boundary = boundary
182
99
        # Decode the headers and setup the first range
183
100
        self.read_boundary()
185
102
 
186
103
    def read_boundary(self):
187
104
        """Read the boundary headers defining a new range"""
188
 
        boundary_line = b'\r\n'
189
 
        while boundary_line == b'\r\n':
 
105
        boundary_line = '\r\n'
 
106
        while boundary_line == '\r\n':
190
107
            # RFC2616 19.2 Additional CRLFs may precede the first boundary
191
108
            # string entity.
192
109
            # To be on the safe side we allow it before any boundary line
193
110
            boundary_line = self._file.readline()
194
111
 
195
 
        if boundary_line == b'':
196
 
            # A timeout in the proxy server caused the response to end early.
197
 
            # See launchpad bug 198646.
198
 
            raise errors.HttpBoundaryMissing(
199
 
                self._path,
200
 
                self._boundary)
201
 
 
202
 
        if boundary_line != b'--' + self._boundary + b'\r\n':
203
 
            # email_utils.unquote() incorrectly unquotes strings enclosed in <>
 
112
        if boundary_line != '--' + self._boundary + '\r\n':
 
113
            # rfc822.unquote() incorrectly unquotes strings enclosed in <>
204
114
            # IIS 6 and 7 incorrectly wrap boundary strings in <>
205
115
            # together they make a beautiful bug, which we will be gracious
206
116
            # about here
207
117
            if (self._unquote_boundary(boundary_line) !=
208
 
                    b'--' + self._boundary + b'\r\n'):
 
118
                '--' + self._boundary + '\r\n'):
209
119
                raise errors.InvalidHttpResponse(
210
120
                    self._path,
211
121
                    "Expected a boundary (%s) line, got '%s'"
212
122
                    % (self._boundary, boundary_line))
213
123
 
214
124
    def _unquote_boundary(self, b):
215
 
        return b[:2] + email_utils.unquote(b[2:-2].decode('ascii')).encode('ascii') + b[-2:]
 
125
        return b[:2] + rfc822.unquote(b[2:-2]) + b[-2:]
216
126
 
217
127
    def read_range_definition(self):
218
128
        """Read a new range definition in a multi parts message.
220
130
        Parse the headers including the empty line following them so that we
221
131
        are ready to read the data itself.
222
132
        """
223
 
        if PY3:
224
 
            self._headers = http_client.parse_headers(self._file)
225
 
        else:
226
 
            self._headers = http_client.HTTPMessage(self._file, seekable=0)
 
133
        self._headers = httplib.HTTPMessage(self._file, seekable=0)
227
134
        # Extract the range definition
228
 
        content_range = self._headers.get('content-range', None)
 
135
        content_range = self._headers.getheader('content-range', None)
229
136
        if content_range is None:
230
137
            raise errors.InvalidHttpResponse(
231
138
                self._path,
296
203
            -1 to read to EOF.
297
204
        """
298
205
        if (self._size > 0
299
 
                and self._pos == self._start + self._size):
 
206
            and self._pos == self._start + self._size):
300
207
            if size == 0:
301
 
                return b''
 
208
                return ''
302
209
            else:
303
210
                self._seek_to_next_range()
304
211
        elif self._pos < self._start:
314
221
                    % (size, self._start, self._size))
315
222
 
316
223
        # read data from file
317
 
        buf = BytesIO()
 
224
        buffer = StringIO()
318
225
        limited = size
319
226
        if self._size > 0:
320
227
            # Don't read past the range definition
321
228
            limited = self._start + self._size - self._pos
322
229
            if size >= 0:
323
230
                limited = min(limited, size)
324
 
        osutils.pumpfile(self._file, buf, limited, self._max_read_size)
325
 
        data = buf.getvalue()
 
231
        osutils.pumpfile(self._file, buffer, limited, self._max_read_size)
 
232
        data = buffer.getvalue()
326
233
 
327
234
        # Update _pos respecting the data effectively read
328
235
        self._pos += len(data)
336
243
            final_pos = start_pos + offset
337
244
        elif whence == 2:
338
245
            if self._size > 0:
339
 
                final_pos = self._start + self._size + offset  # offset < 0
 
246
                final_pos = self._start + self._size + offset # offset < 0
340
247
            else:
341
248
                raise errors.InvalidRange(
342
249
                    self._path, self._pos,
362
269
                cur_limit = self._start + self._size
363
270
 
364
271
        size = final_pos - self._pos
365
 
        if size > 0:  # size can be < 0 if we crossed a range boundary
 
272
        if size > 0: # size can be < 0 if we crossed a range boundary
366
273
            # We don't need the data, just read it and throw it away
367
274
            self._checked_read(size)
368
275
 
370
277
        return self._pos
371
278
 
372
279
 
373
 
def handle_response(url, code, getheader, data):
 
280
def handle_response(url, code, msg, data):
374
281
    """Interpret the code & headers and wrap the provided data in a RangeFile.
375
282
 
376
283
    This is a factory method which returns an appropriate RangeFile based on
378
285
 
379
286
    :param url: The url being processed. Mostly for error reporting
380
287
    :param code: The integer HTTP response code
381
 
    :param getheader: Function for retrieving header
 
288
    :param msg: An HTTPMessage containing the headers for the response
382
289
    :param data: A file-like object that can be read() to get the
383
290
                 requested data
384
291
    :return: A file-like object that can seek()+read() the
385
292
             ranges indicated by the headers.
386
293
    """
 
294
    rfile = RangeFile(url, data)
387
295
    if code == 200:
388
296
        # A whole file
389
 
        rfile = ResponseFile(url, data)
 
297
        size = msg.getheader('content-length', None)
 
298
        if size is None:
 
299
            size = -1
 
300
        else:
 
301
            size = int(size)
 
302
        rfile.set_range(0, size)
390
303
    elif code == 206:
391
 
        rfile = RangeFile(url, data)
392
 
        # When there is no content-type header we treat the response as
393
 
        # being of type 'application/octet-stream' as per RFC2616 section
394
 
        # 7.2.1.
395
 
        # Therefore it is obviously not multipart
396
 
        content_type = getheader('content-type', 'application/octet-stream')
397
 
        mimetype, options = cgi.parse_header(content_type)
398
 
        if mimetype == 'multipart/byteranges':
399
 
            rfile.set_boundary(options['boundary'].encode('ascii'))
 
304
        content_type = msg.getheader('content-type', None)
 
305
        if content_type is None:
 
306
            # When there is no content-type header we treat the response as
 
307
            # being of type 'application/octet-stream' as per RFC2616 section
 
308
            # 7.2.1.
 
309
            # Therefore it is obviously not multipart
 
310
            content_type = 'application/octet-stream'
 
311
            is_multipart = False
 
312
        else:
 
313
            is_multipart = (msg.getmaintype() == 'multipart'
 
314
                            and msg.getsubtype() == 'byteranges')
 
315
 
 
316
        if is_multipart:
 
317
            # Full fledged multipart response
 
318
            rfile.set_boundary(msg.getparam('boundary'))
400
319
        else:
401
320
            # A response to a range request, but not multipart
402
 
            content_range = getheader('content-range', None)
 
321
            content_range = msg.getheader('content-range', None)
403
322
            if content_range is None:
404
 
                raise errors.InvalidHttpResponse(
405
 
                    url, 'Missing the Content-Range header in a 206 range response')
 
323
                raise errors.InvalidHttpResponse(url,
 
324
                    'Missing the Content-Range header in a 206 range response')
406
325
            rfile.set_range_from_header(content_range)
407
326
    else:
408
327
        raise errors.InvalidHttpResponse(url,
409
328
                                         'Unknown response code %s' % code)
410
329
 
411
330
    return rfile
 
331