~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

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

  • Committer: Martin Pool
  • Date: 2006-04-20 02:37:21 UTC
  • mto: This revision was merged to the branch mainline in revision 1675.
  • Revision ID: mbp@sourcefrog.net-20060420023721-04d8a3b015987240
Add .hg to default ignore list

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2006, 2007 Canonical Ltd
2
 
#
3
 
# This program is free software; you can redistribute it and/or modify
4
 
# it under the terms of the GNU General Public License as published by
5
 
# the Free Software Foundation; either version 2 of the License, or
6
 
# (at your option) any later version.
7
 
#
8
 
# This program is distributed in the hope that it will be useful,
9
 
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10
 
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11
 
# GNU General Public License for more details.
12
 
#
13
 
# You should have received a copy of the GNU General Public License
14
 
# along with this program; if not, write to the Free Software
15
 
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
16
 
 
17
 
"""Handlers for HTTP Responses.
18
 
 
19
 
The purpose of these classes is to provide a uniform interface for clients
20
 
to standard HTTP responses, single range responses and multipart range
21
 
responses.
22
 
"""
23
 
 
24
 
 
25
 
import httplib
26
 
from cStringIO import StringIO
27
 
 
28
 
from bzrlib import (
29
 
    errors,
30
 
    trace,
31
 
    osutils,
32
 
    )
33
 
 
34
 
 
35
 
# A RangeFile expects the following grammar (simplified to outline the
36
 
# assumptions we rely upon).
37
 
 
38
 
# file: whole_file
39
 
#     | single_range
40
 
#     | multiple_range
41
 
 
42
 
# whole_file: [content_length_header] data
43
 
 
44
 
# single_range: content_range_header data
45
 
 
46
 
# multiple_range: boundary_header boundary (content_range_header data boundary)+
47
 
 
48
 
class RangeFile(object):
49
 
    """File-like object that allow access to partial available data.
50
 
 
51
 
    All accesses should happen sequentially since the acquisition occurs during
52
 
    an http response reception (as sockets can't be seeked, we simulate the
53
 
    seek by just reading and discarding the data).
54
 
 
55
 
    The access pattern is defined by a set of ranges discovered as reading
56
 
    progress. Only one range is available at a given time, so all accesses
57
 
    should happen with monotonically increasing offsets.
58
 
    """
59
 
 
60
 
    # in _checked_read() below, we may have to discard several MB in the worst
61
 
    # case. To avoid buffering that much, we read and discard by chunks
62
 
    # instead. The underlying file is either a socket or a StringIO, so reading
63
 
    # 8k chunks should be fine.
64
 
    _discarded_buf_size = 8192
65
 
 
66
 
    # maximum size of read requests -- used to avoid MemoryError issues in recv
67
 
    _max_read_size = 512 * 1024
68
 
 
69
 
    def __init__(self, path, infile):
70
 
        """Constructor.
71
 
 
72
 
        :param path: File url, for error reports.
73
 
        :param infile: File-like socket set at body start.
74
 
        """
75
 
        self._path = path
76
 
        self._file = infile
77
 
        self._boundary = None
78
 
        # When using multi parts response, this will be set with the headers
79
 
        # associated with the range currently read.
80
 
        self._headers = None
81
 
        # Default to the whole file of unspecified size
82
 
        self.set_range(0, -1)
83
 
 
84
 
    def set_range(self, start, size):
85
 
        """Change the range mapping"""
86
 
        self._start = start
87
 
        self._size = size
88
 
        # Set the new _pos since that's what we want to expose
89
 
        self._pos = self._start
90
 
 
91
 
    def set_boundary(self, boundary):
92
 
        """Define the boundary used in a multi parts message.
93
 
        
94
 
        The file should be at the beginning of the body, the first range
95
 
        definition is read and taken into account.
96
 
        """
97
 
        self._boundary = boundary
98
 
        # Decode the headers and setup the first range
99
 
        self.read_boundary()
100
 
        self.read_range_definition()
101
 
 
102
 
    def read_boundary(self):
103
 
        """Read the boundary headers defining a new range"""
104
 
        boundary_line = '\r\n'
105
 
        while boundary_line == '\r\n':
106
 
            # RFC2616 19.2 Additional CRLFs may precede the first boundary
107
 
            # string entity.
108
 
            # To be on the safe side we allow it before any boundary line
109
 
            boundary_line = self._file.readline()
110
 
        if boundary_line != '--' + self._boundary + '\r\n':
111
 
            raise errors.InvalidHttpResponse(
112
 
                self._path,
113
 
                "Expected a boundary (%s) line, got '%s'" % (self._boundary,
114
 
                                                             boundary_line))
115
 
 
116
 
    def read_range_definition(self):
117
 
        """Read a new range definition in a multi parts message.
118
 
 
119
 
        Parse the headers including the empty line following them so that we
120
 
        are ready to read the data itself.
121
 
        """
122
 
        self._headers = httplib.HTTPMessage(self._file, seekable=0)
123
 
        # Extract the range definition
124
 
        content_range = self._headers.getheader('content-range', None)
125
 
        if content_range is None:
126
 
            raise errors.InvalidHttpResponse(
127
 
                self._path,
128
 
                'Content-Range header missing in a multi-part response')
129
 
        self.set_range_from_header(content_range)
130
 
 
131
 
    def set_range_from_header(self, content_range):
132
 
        """Helper to set the new range from its description in the headers"""
133
 
        try:
134
 
            rtype, values = content_range.split()
135
 
        except ValueError:
136
 
            raise errors.InvalidHttpRange(self._path, content_range,
137
 
                                          'Malformed header')
138
 
        if rtype != 'bytes':
139
 
            raise errors.InvalidHttpRange(self._path, content_range,
140
 
                                          "Unsupported range type '%s'" % rtype)
141
 
        try:
142
 
            # We don't need total, but note that it may be either the file size
143
 
            # or '*' if the server can't or doesn't want to return the file
144
 
            # size.
145
 
            start_end, total = values.split('/')
146
 
            start, end = start_end.split('-')
147
 
            start = int(start)
148
 
            end = int(end)
149
 
        except ValueError:
150
 
            raise errors.InvalidHttpRange(self._path, content_range,
151
 
                                          'Invalid range values')
152
 
        size = end - start + 1
153
 
        if size <= 0:
154
 
            raise errors.InvalidHttpRange(self._path, content_range,
155
 
                                          'Invalid range, size <= 0')
156
 
        self.set_range(start, size)
157
 
 
158
 
    def _checked_read(self, size):
159
 
        """Read the file checking for short reads.
160
 
 
161
 
        The data read is discarded along the way.
162
 
        """
163
 
        pos = self._pos
164
 
        remaining = size
165
 
        while remaining > 0:
166
 
            data = self._file.read(min(remaining, self._discarded_buf_size))
167
 
            remaining -= len(data)
168
 
            if not data:
169
 
                raise errors.ShortReadvError(self._path, pos, size,
170
 
                                             size - remaining)
171
 
        self._pos += size
172
 
 
173
 
    def _seek_to_next_range(self):
174
 
        # We will cross range boundaries
175
 
        if self._boundary is None:
176
 
            # If we don't have a boundary, we can't find another range
177
 
            raise errors.InvalidRange(self._path, self._pos,
178
 
                                      "Range (%s, %s) exhausted"
179
 
                                      % (self._start, self._size))
180
 
        self.read_boundary()
181
 
        self.read_range_definition()
182
 
 
183
 
    def read(self, size=-1):
184
 
        """Read size bytes from the current position in the file.
185
 
 
186
 
        Reading across ranges is not supported. We rely on the underlying http
187
 
        client to clean the socket if we leave bytes unread. This may occur for
188
 
        the final boundary line of a multipart response or for any range
189
 
        request not entirely consumed by the client (due to offset coalescing)
190
 
 
191
 
        :param size:  The number of bytes to read.  Leave unspecified or pass
192
 
            -1 to read to EOF.
193
 
        """
194
 
        if (self._size > 0
195
 
            and self._pos == self._start + self._size):
196
 
            if size == 0:
197
 
                return ''
198
 
            else:
199
 
                self._seek_to_next_range()
200
 
        elif self._pos < self._start:
201
 
            raise errors.InvalidRange(
202
 
                self._path, self._pos,
203
 
                "Can't read %s bytes before range (%s, %s)"
204
 
                % (size, self._start, self._size))
205
 
        if self._size > 0:
206
 
            if size > 0 and self._pos + size > self._start + self._size:
207
 
                raise errors.InvalidRange(
208
 
                    self._path, self._pos,
209
 
                    "Can't read %s bytes across range (%s, %s)"
210
 
                    % (size, self._start, self._size))
211
 
 
212
 
        # read data from file
213
 
        buffer = StringIO()
214
 
        limited = size
215
 
        if self._size > 0:
216
 
            # Don't read past the range definition
217
 
            limited = self._start + self._size - self._pos
218
 
            if size >= 0:
219
 
                limited = min(limited, size)
220
 
        osutils.pumpfile(self._file, buffer, limited, self._max_read_size)
221
 
        data = buffer.getvalue()
222
 
 
223
 
        # Update _pos respecting the data effectively read
224
 
        self._pos += len(data)
225
 
        return data
226
 
 
227
 
    def seek(self, offset, whence=0):
228
 
        start_pos = self._pos
229
 
        if whence == 0:
230
 
            final_pos = offset
231
 
        elif whence == 1:
232
 
            final_pos = start_pos + offset
233
 
        elif whence == 2:
234
 
            if self._size > 0:
235
 
                final_pos = self._start + self._size + offset # offset < 0
236
 
            else:
237
 
                raise errors.InvalidRange(
238
 
                    self._path, self._pos,
239
 
                    "RangeFile: can't seek from end while size is unknown")
240
 
        else:
241
 
            raise ValueError("Invalid value %s for whence." % whence)
242
 
 
243
 
        if final_pos < self._pos:
244
 
            # Can't seek backwards
245
 
            raise errors.InvalidRange(
246
 
                self._path, self._pos,
247
 
                'RangeFile: trying to seek backwards to %s' % final_pos)
248
 
 
249
 
        if self._size > 0:
250
 
            cur_limit = self._start + self._size
251
 
            while final_pos > cur_limit:
252
 
                # We will cross range boundaries
253
 
                remain = cur_limit - self._pos
254
 
                if remain > 0:
255
 
                    # Finish reading the current range
256
 
                    self._checked_read(remain)
257
 
                self._seek_to_next_range()
258
 
                cur_limit = self._start + self._size
259
 
 
260
 
        size = final_pos - self._pos
261
 
        if size > 0: # size can be < 0 if we crossed a range boundary
262
 
            # We don't need the data, just read it and throw it away
263
 
            self._checked_read(size)
264
 
 
265
 
    def tell(self):
266
 
        return self._pos
267
 
 
268
 
 
269
 
def handle_response(url, code, msg, data):
270
 
    """Interpret the code & headers and wrap the provided data in a RangeFile.
271
 
 
272
 
    This is a factory method which returns an appropriate RangeFile based on
273
 
    the code & headers it's given.
274
 
 
275
 
    :param url: The url being processed. Mostly for error reporting
276
 
    :param code: The integer HTTP response code
277
 
    :param msg: An HTTPMessage containing the headers for the response
278
 
    :param data: A file-like object that can be read() to get the
279
 
                 requested data
280
 
    :return: A file-like object that can seek()+read() the 
281
 
             ranges indicated by the headers.
282
 
    """
283
 
    rfile = RangeFile(url, data)
284
 
    if code == 200:
285
 
        # A whole file
286
 
        size = msg.getheader('content-length', None)
287
 
        if size is None:
288
 
            size = -1
289
 
        else:
290
 
            size = int(size)
291
 
        rfile.set_range(0, size)
292
 
    elif code == 206:
293
 
        content_type = msg.getheader('content-type', None)
294
 
        if content_type is None:
295
 
            # When there is no content-type header we treat the response as
296
 
            # being of type 'application/octet-stream' as per RFC2616 section
297
 
            # 7.2.1.
298
 
            # Therefore it is obviously not multipart
299
 
            content_type = 'application/octet-stream'
300
 
            is_multipart = False
301
 
        else:
302
 
            is_multipart = (msg.getmaintype() == 'multipart'
303
 
                            and msg.getsubtype() == 'byteranges')
304
 
 
305
 
        if is_multipart:
306
 
            # Full fledged multipart response
307
 
            rfile.set_boundary(msg.getparam('boundary'))
308
 
        else:
309
 
            # A response to a range request, but not multipart
310
 
            content_range = msg.getheader('content-range', None)
311
 
            if content_range is None:
312
 
                raise errors.InvalidHttpResponse(url,
313
 
                    'Missing the Content-Range header in a 206 range response')
314
 
            rfile.set_range_from_header(content_range)
315
 
    else:
316
 
        raise errors.InvalidHttpResponse(url,
317
 
                                         'Unknown response code %s' % code)
318
 
 
319
 
    return rfile
320