~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/transport/http.py

Got the bzrdir api straightened out, plenty of refactoring to use it pending, but the api is up and running.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2005-2010 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
 
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16
 
 
17
 
"""Base implementation of Transport over http.
18
 
 
19
 
There are separate implementation modules for each http client implementation.
 
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 
16
"""Implementation of Transport over http.
20
17
"""
21
18
 
22
 
from __future__ import absolute_import
23
 
 
24
 
import os
25
 
import re
 
19
import os, errno
 
20
from cStringIO import StringIO
 
21
import urllib, urllib2
26
22
import urlparse
27
 
import sys
28
 
import weakref
 
23
from warnings import warn
29
24
 
30
 
from bzrlib import (
31
 
    debug,
32
 
    errors,
33
 
    transport,
34
 
    ui,
35
 
    urlutils,
36
 
    )
37
 
from bzrlib.smart import medium
 
25
from bzrlib.transport import Transport, Server
 
26
from bzrlib.errors import (TransportNotPossible, NoSuchFile, 
 
27
                           TransportError, ConnectionError)
 
28
from bzrlib.errors import BzrError, BzrCheckError
 
29
from bzrlib.branch import Branch
38
30
from bzrlib.trace import mutter
39
 
from bzrlib.transport import (
40
 
    ConnectedTransport,
41
 
    )
42
 
 
43
 
 
44
 
class HttpTransportBase(ConnectedTransport):
45
 
    """Base class for http implementations.
46
 
 
47
 
    Does URL parsing, etc, but not any network IO.
48
 
 
49
 
    The protocol can be given as e.g. http+urllib://host/ to use a particular
50
 
    implementation.
51
 
    """
52
 
 
53
 
    # _unqualified_scheme: "http" or "https"
54
 
    # _scheme: may have "+pycurl", etc
55
 
 
56
 
    def __init__(self, base, _impl_name, _from_transport=None):
 
31
 
 
32
 
 
33
def extract_auth(url, password_manager):
 
34
    """
 
35
    Extract auth parameters from am HTTP/HTTPS url and add them to the given
 
36
    password manager.  Return the url, minus those auth parameters (which
 
37
    confuse urllib2).
 
38
    """
 
39
    assert url.startswith('http://') or url.startswith('https://')
 
40
    scheme, host = url.split('//', 1)
 
41
    if '/' in host:
 
42
        host, path = host.split('/', 1)
 
43
        path = '/' + path
 
44
    else:
 
45
        path = ''
 
46
    port = ''
 
47
    if '@' in host:
 
48
        auth, host = host.split('@', 1)
 
49
        if ':' in auth:
 
50
            username, password = auth.split(':', 1)
 
51
        else:
 
52
            username, password = auth, None
 
53
        if ':' in host:
 
54
            host, port = host.split(':', 1)
 
55
            port = ':' + port
 
56
        # FIXME: if password isn't given, should we ask for it?
 
57
        if password is not None:
 
58
            username = urllib.unquote(username)
 
59
            password = urllib.unquote(password)
 
60
            password_manager.add_password(None, host, username, password)
 
61
    url = scheme + '//' + host + port + path
 
62
    return url
 
63
    
 
64
def get_url(url):
 
65
    import urllib2
 
66
    mutter("get_url %s" % url)
 
67
    manager = urllib2.HTTPPasswordMgrWithDefaultRealm()
 
68
    url = extract_auth(url, manager)
 
69
    auth_handler = urllib2.HTTPBasicAuthHandler(manager)
 
70
    opener = urllib2.build_opener(auth_handler)
 
71
    url_f = opener.open(url)
 
72
    return url_f
 
73
 
 
74
class HttpTransport(Transport):
 
75
    """This is the transport agent for http:// access.
 
76
    
 
77
    TODO: Implement pipelined versions of all of the *_multi() functions.
 
78
    """
 
79
 
 
80
    def __init__(self, base):
57
81
        """Set the base path where files will be stored."""
58
 
        proto_match = re.match(r'^(https?)(\+\w+)?://', base)
59
 
        if not proto_match:
60
 
            raise AssertionError("not a http url: %r" % base)
61
 
        self._unqualified_scheme = proto_match.group(1)
62
 
        self._impl_name = _impl_name
63
 
        super(HttpTransportBase, self).__init__(base,
64
 
                                                _from_transport=_from_transport)
65
 
        self._medium = None
66
 
        # range hint is handled dynamically throughout the life
67
 
        # of the transport object. We start by trying multi-range
68
 
        # requests and if the server returns bogus results, we
69
 
        # retry with single range requests and, finally, we
70
 
        # forget about range if the server really can't
71
 
        # understand. Once acquired, this piece of info is
72
 
        # propagated to clones.
73
 
        if _from_transport is not None:
74
 
            self._range_hint = _from_transport._range_hint
75
 
        else:
76
 
            self._range_hint = 'multi'
 
82
        assert base.startswith('http://') or base.startswith('https://')
 
83
        if base[-1] != '/':
 
84
            base = base + '/'
 
85
        super(HttpTransport, self).__init__(base)
 
86
        # In the future we might actually connect to the remote host
 
87
        # rather than using get_url
 
88
        # self._connection = None
 
89
        (self._proto, self._host,
 
90
            self._path, self._parameters,
 
91
            self._query, self._fragment) = urlparse.urlparse(self.base)
 
92
 
 
93
    def should_cache(self):
 
94
        """Return True if the data pulled across should be cached locally.
 
95
        """
 
96
        return True
 
97
 
 
98
    def clone(self, offset=None):
 
99
        """Return a new HttpTransport with root at self.base + offset
 
100
        For now HttpTransport does not actually connect, so just return
 
101
        a new HttpTransport object.
 
102
        """
 
103
        if offset is None:
 
104
            return HttpTransport(self.base)
 
105
        else:
 
106
            return HttpTransport(self.abspath(offset))
 
107
 
 
108
    def abspath(self, relpath):
 
109
        """Return the full url to the given relative path.
 
110
        This can be supplied with a string or a list
 
111
        """
 
112
        assert isinstance(relpath, basestring)
 
113
        if isinstance(relpath, basestring):
 
114
            relpath_parts = relpath.split('/')
 
115
        else:
 
116
            # TODO: Don't call this with an array - no magic interfaces
 
117
            relpath_parts = relpath[:]
 
118
        if len(relpath_parts) > 1:
 
119
            if relpath_parts[0] == '':
 
120
                raise ValueError("path %r within branch %r seems to be absolute"
 
121
                                 % (relpath, self._path))
 
122
            if relpath_parts[-1] == '':
 
123
                raise ValueError("path %r within branch %r seems to be a directory"
 
124
                                 % (relpath, self._path))
 
125
        basepath = self._path.split('/')
 
126
        if len(basepath) > 0 and basepath[-1] == '':
 
127
            basepath = basepath[:-1]
 
128
        for p in relpath_parts:
 
129
            if p == '..':
 
130
                if len(basepath) == 0:
 
131
                    # In most filesystems, a request for the parent
 
132
                    # of root, just returns root.
 
133
                    continue
 
134
                basepath.pop()
 
135
            elif p == '.' or p == '':
 
136
                continue # No-op
 
137
            else:
 
138
                basepath.append(p)
 
139
        # Possibly, we could use urlparse.urljoin() here, but
 
140
        # I'm concerned about when it chooses to strip the last
 
141
        # portion of the path, and when it doesn't.
 
142
        path = '/'.join(basepath)
 
143
        return urlparse.urlunparse((self._proto,
 
144
                self._host, path, '', '', ''))
77
145
 
78
146
    def has(self, relpath):
79
 
        raise NotImplementedError("has() is abstract on %r" % self)
80
 
 
81
 
    def get(self, relpath):
 
147
        """Does the target location exist?
 
148
 
 
149
        TODO: HttpTransport.has() should use a HEAD request,
 
150
        not a full GET request.
 
151
 
 
152
        TODO: This should be changed so that we don't use
 
153
        urllib2 and get an exception, the code path would be
 
154
        cleaner if we just do an http HEAD request, and parse
 
155
        the return code.
 
156
        """
 
157
        path = relpath
 
158
        try:
 
159
            path = self.abspath(relpath)
 
160
            f = get_url(path)
 
161
            # Without the read and then close()
 
162
            # we tend to have busy sockets.
 
163
            f.read()
 
164
            f.close()
 
165
            return True
 
166
        except urllib2.URLError, e:
 
167
            mutter('url error code: %s for has url: %r', e.code, path)
 
168
            if e.code == 404:
 
169
                return False
 
170
            raise
 
171
        except IOError, e:
 
172
            mutter('io error: %s %s for has url: %r', 
 
173
                e.errno, errno.errorcode.get(e.errno), path)
 
174
            if e.errno == errno.ENOENT:
 
175
                return False
 
176
            raise TransportError(orig_error=e)
 
177
 
 
178
    def get(self, relpath, decode=False):
82
179
        """Get the file at the given relative path.
83
180
 
84
181
        :param relpath: The relative path to the file
85
182
        """
86
 
        code, response_file = self._get(relpath, None)
87
 
        return response_file
88
 
 
89
 
    def _get(self, relpath, ranges, tail_amount=0):
90
 
        """Get a file, or part of a file.
91
 
 
92
 
        :param relpath: Path relative to transport base URL
93
 
        :param ranges: None to get the whole file;
94
 
            or  a list of _CoalescedOffset to fetch parts of a file.
95
 
        :param tail_amount: The amount to get from the end of the file.
96
 
 
97
 
        :returns: (http_code, result_file)
98
 
        """
99
 
        raise NotImplementedError(self._get)
100
 
 
101
 
    def _remote_path(self, relpath):
102
 
        """See ConnectedTransport._remote_path.
103
 
 
104
 
        user and passwords are not embedded in the path provided to the server.
105
 
        """
106
 
        url = self._parsed_url.clone(relpath)
107
 
        url.user = url.quoted_user = None
108
 
        url.password = url.quoted_password = None
109
 
        url.scheme = self._unqualified_scheme
110
 
        return str(url)
111
 
 
112
 
    def _create_auth(self):
113
 
        """Returns a dict containing the credentials provided at build time."""
114
 
        auth = dict(host=self._parsed_url.host, port=self._parsed_url.port,
115
 
                    user=self._parsed_url.user, password=self._parsed_url.password,
116
 
                    protocol=self._unqualified_scheme,
117
 
                    path=self._parsed_url.path)
118
 
        return auth
119
 
 
120
 
    def get_smart_medium(self):
121
 
        """See Transport.get_smart_medium."""
122
 
        if self._medium is None:
123
 
            # Since medium holds some state (smart server probing at least), we
124
 
            # need to keep it around. Note that this is needed because medium
125
 
            # has the same 'base' attribute as the transport so it can't be
126
 
            # shared between transports having different bases.
127
 
            self._medium = SmartClientHTTPMedium(self)
128
 
        return self._medium
129
 
 
130
 
    def _degrade_range_hint(self, relpath, ranges, exc_info):
131
 
        if self._range_hint == 'multi':
132
 
            self._range_hint = 'single'
133
 
            mutter('Retry "%s" with single range request' % relpath)
134
 
        elif self._range_hint == 'single':
135
 
            self._range_hint = None
136
 
            mutter('Retry "%s" without ranges' % relpath)
137
 
        else:
138
 
            # We tried all the tricks, but nothing worked. We re-raise the
139
 
            # original exception; the 'mutter' calls above will indicate that
140
 
            # further tries were unsuccessful
141
 
            raise exc_info[0], exc_info[1], exc_info[2]
142
 
 
143
 
    # _coalesce_offsets is a helper for readv, it try to combine ranges without
144
 
    # degrading readv performances. _bytes_to_read_before_seek is the value
145
 
    # used for the limit parameter and has been tuned for other transports. For
146
 
    # HTTP, the name is inappropriate but the parameter is still useful and
147
 
    # helps reduce the number of chunks in the response. The overhead for a
148
 
    # chunk (headers, length, footer around the data itself is variable but
149
 
    # around 50 bytes. We use 128 to reduce the range specifiers that appear in
150
 
    # the header, some servers (notably Apache) enforce a maximum length for a
151
 
    # header and issue a '400: Bad request' error when too much ranges are
152
 
    # specified.
153
 
    _bytes_to_read_before_seek = 128
154
 
    # No limit on the offset number that get combined into one, we are trying
155
 
    # to avoid downloading the whole file.
156
 
    _max_readv_combine = 0
157
 
    # By default Apache has a limit of ~400 ranges before replying with a 400
158
 
    # Bad Request. So we go underneath that amount to be safe.
159
 
    _max_get_ranges = 200
160
 
    # We impose no limit on the range size. But see _pycurl.py for a different
161
 
    # use.
162
 
    _get_max_size = 0
163
 
 
164
 
    def _readv(self, relpath, offsets):
165
 
        """Get parts of the file at the given relative path.
166
 
 
167
 
        :param offsets: A list of (offset, size) tuples.
168
 
        :param return: A list or generator of (offset, data) tuples
169
 
        """
170
 
        # offsets may be a generator, we will iterate it several times, so
171
 
        # build a list
172
 
        offsets = list(offsets)
173
 
 
174
 
        try_again = True
175
 
        retried_offset = None
176
 
        while try_again:
177
 
            try_again = False
178
 
 
179
 
            # Coalesce the offsets to minimize the GET requests issued
180
 
            sorted_offsets = sorted(offsets)
181
 
            coalesced = self._coalesce_offsets(
182
 
                sorted_offsets, limit=self._max_readv_combine,
183
 
                fudge_factor=self._bytes_to_read_before_seek,
184
 
                max_size=self._get_max_size)
185
 
 
186
 
            # Turn it into a list, we will iterate it several times
187
 
            coalesced = list(coalesced)
188
 
            if 'http' in debug.debug_flags:
189
 
                mutter('http readv of %s  offsets => %s collapsed %s',
190
 
                    relpath, len(offsets), len(coalesced))
191
 
 
192
 
            # Cache the data read, but only until it's been used
193
 
            data_map = {}
194
 
            # We will iterate on the data received from the GET requests and
195
 
            # serve the corresponding offsets respecting the initial order. We
196
 
            # need an offset iterator for that.
197
 
            iter_offsets = iter(offsets)
198
 
            cur_offset_and_size = iter_offsets.next()
199
 
 
200
 
            try:
201
 
                for cur_coal, rfile in self._coalesce_readv(relpath, coalesced):
202
 
                    # Split the received chunk
203
 
                    for offset, size in cur_coal.ranges:
204
 
                        start = cur_coal.start + offset
205
 
                        rfile.seek(start, os.SEEK_SET)
206
 
                        data = rfile.read(size)
207
 
                        data_len = len(data)
208
 
                        if data_len != size:
209
 
                            raise errors.ShortReadvError(relpath, start, size,
210
 
                                                         actual=data_len)
211
 
                        if (start, size) == cur_offset_and_size:
212
 
                            # The offset requested are sorted as the coalesced
213
 
                            # ones, no need to cache. Win !
214
 
                            yield cur_offset_and_size[0], data
215
 
                            cur_offset_and_size = iter_offsets.next()
216
 
                        else:
217
 
                            # Different sorting. We need to cache.
218
 
                            data_map[(start, size)] = data
219
 
 
220
 
                    # Yield everything we can
221
 
                    while cur_offset_and_size in data_map:
222
 
                        # Clean the cached data since we use it
223
 
                        # XXX: will break if offsets contains duplicates --
224
 
                        # vila20071129
225
 
                        this_data = data_map.pop(cur_offset_and_size)
226
 
                        yield cur_offset_and_size[0], this_data
227
 
                        cur_offset_and_size = iter_offsets.next()
228
 
 
229
 
            except (errors.ShortReadvError, errors.InvalidRange,
230
 
                    errors.InvalidHttpRange, errors.HttpBoundaryMissing), e:
231
 
                mutter('Exception %r: %s during http._readv',e, e)
232
 
                if (not isinstance(e, errors.ShortReadvError)
233
 
                    or retried_offset == cur_offset_and_size):
234
 
                    # We don't degrade the range hint for ShortReadvError since
235
 
                    # they do not indicate a problem with the server ability to
236
 
                    # handle ranges. Except when we fail to get back a required
237
 
                    # offset twice in a row. In that case, falling back to
238
 
                    # single range or whole file should help or end up in a
239
 
                    # fatal exception.
240
 
                    self._degrade_range_hint(relpath, coalesced, sys.exc_info())
241
 
                # Some offsets may have been already processed, so we retry
242
 
                # only the unsuccessful ones.
243
 
                offsets = [cur_offset_and_size] + [o for o in iter_offsets]
244
 
                retried_offset = cur_offset_and_size
245
 
                try_again = True
246
 
 
247
 
    def _coalesce_readv(self, relpath, coalesced):
248
 
        """Issue several GET requests to satisfy the coalesced offsets"""
249
 
 
250
 
        def get_and_yield(relpath, coalesced):
251
 
            if coalesced:
252
 
                # Note that the _get below may raise
253
 
                # errors.InvalidHttpRange. It's the caller's responsibility to
254
 
                # decide how to retry since it may provide different coalesced
255
 
                # offsets.
256
 
                code, rfile = self._get(relpath, coalesced)
257
 
                for coal in coalesced:
258
 
                    yield coal, rfile
259
 
 
260
 
        if self._range_hint is None:
261
 
            # Download whole file
262
 
            for c, rfile in get_and_yield(relpath, coalesced):
263
 
                yield c, rfile
264
 
        else:
265
 
            total = len(coalesced)
266
 
            if self._range_hint == 'multi':
267
 
                max_ranges = self._max_get_ranges
268
 
            elif self._range_hint == 'single':
269
 
                max_ranges = total
270
 
            else:
271
 
                raise AssertionError("Unknown _range_hint %r"
272
 
                                     % (self._range_hint,))
273
 
            # TODO: Some web servers may ignore the range requests and return
274
 
            # the whole file, we may want to detect that and avoid further
275
 
            # requests.
276
 
            # Hint: test_readv_multiple_get_requests will fail once we do that
277
 
            cumul = 0
278
 
            ranges = []
279
 
            for coal in coalesced:
280
 
                if ((self._get_max_size > 0
281
 
                     and cumul + coal.length > self._get_max_size)
282
 
                    or len(ranges) >= max_ranges):
283
 
                    # Get that much and yield
284
 
                    for c, rfile in get_and_yield(relpath, ranges):
285
 
                        yield c, rfile
286
 
                    # Restart with the current offset
287
 
                    ranges = [coal]
288
 
                    cumul = coal.length
289
 
                else:
290
 
                    ranges.append(coal)
291
 
                    cumul += coal.length
292
 
            # Get the rest and yield
293
 
            for c, rfile in get_and_yield(relpath, ranges):
294
 
                yield c, rfile
295
 
 
296
 
    def recommended_page_size(self):
297
 
        """See Transport.recommended_page_size().
298
 
 
299
 
        For HTTP we suggest a large page size to reduce the overhead
300
 
        introduced by latency.
301
 
        """
302
 
        return 64 * 1024
303
 
 
304
 
    def _post(self, body_bytes):
305
 
        """POST body_bytes to .bzr/smart on this transport.
306
 
 
307
 
        :returns: (response code, response body file-like object).
308
 
        """
309
 
        # TODO: Requiring all the body_bytes to be available at the beginning of
310
 
        # the POST may require large client buffers.  It would be nice to have
311
 
        # an interface that allows streaming via POST when possible (and
312
 
        # degrades to a local buffer when not).
313
 
        raise NotImplementedError(self._post)
314
 
 
315
 
    def put_file(self, relpath, f, mode=None):
316
 
        """Copy the file-like object into the location.
 
183
        path = relpath
 
184
        try:
 
185
            path = self.abspath(relpath)
 
186
            return get_url(path)
 
187
        except urllib2.HTTPError, e:
 
188
            mutter('url error code: %s for has url: %r', e.code, path)
 
189
            if e.code == 404:
 
190
                raise NoSuchFile(path, extra=e)
 
191
            raise
 
192
        except (BzrError, IOError), e:
 
193
            if hasattr(e, 'errno'):
 
194
                mutter('io error: %s %s for has url: %r', 
 
195
                    e.errno, errno.errorcode.get(e.errno), path)
 
196
                if e.errno == errno.ENOENT:
 
197
                    raise NoSuchFile(path, extra=e)
 
198
            raise ConnectionError(msg = "Error retrieving %s: %s" 
 
199
                             % (self.abspath(relpath), str(e)),
 
200
                             orig_error=e)
 
201
 
 
202
    def put(self, relpath, f, mode=None):
 
203
        """Copy the file-like or string object into the location.
317
204
 
318
205
        :param relpath: Location to put the contents, relative to base.
319
 
        :param f:       File-like object.
 
206
        :param f:       File-like or string object.
320
207
        """
321
 
        raise errors.TransportNotPossible('http PUT not supported')
 
208
        raise TransportNotPossible('http PUT not supported')
322
209
 
323
210
    def mkdir(self, relpath, mode=None):
324
211
        """Create a directory at the given path."""
325
 
        raise errors.TransportNotPossible('http does not support mkdir()')
 
212
        raise TransportNotPossible('http does not support mkdir()')
326
213
 
327
214
    def rmdir(self, relpath):
328
215
        """See Transport.rmdir."""
329
 
        raise errors.TransportNotPossible('http does not support rmdir()')
 
216
        raise TransportNotPossible('http does not support rmdir()')
330
217
 
331
 
    def append_file(self, relpath, f, mode=None):
 
218
    def append(self, relpath, f):
332
219
        """Append the text in the file-like object into the final
333
220
        location.
334
221
        """
335
 
        raise errors.TransportNotPossible('http does not support append()')
 
222
        raise TransportNotPossible('http does not support append()')
336
223
 
337
224
    def copy(self, rel_from, rel_to):
338
225
        """Copy the item at rel_from to the location at rel_to"""
339
 
        raise errors.TransportNotPossible('http does not support copy()')
 
226
        raise TransportNotPossible('http does not support copy()')
340
227
 
341
228
    def copy_to(self, relpaths, other, mode=None, pb=None):
342
229
        """Copy a set of entries from self into another Transport.
349
236
        # At this point HttpTransport might be able to check and see if
350
237
        # the remote location is the same, and rather than download, and
351
238
        # then upload, it could just issue a remote copy_this command.
352
 
        if isinstance(other, HttpTransportBase):
353
 
            raise errors.TransportNotPossible(
354
 
                'http cannot be the target of copy_to()')
 
239
        if isinstance(other, HttpTransport):
 
240
            raise TransportNotPossible('http cannot be the target of copy_to()')
355
241
        else:
356
 
            return super(HttpTransportBase, self).\
357
 
                    copy_to(relpaths, other, mode=mode, pb=pb)
 
242
            return super(HttpTransport, self).copy_to(relpaths, other, mode=mode, pb=pb)
358
243
 
359
244
    def move(self, rel_from, rel_to):
360
245
        """Move the item at rel_from to the location at rel_to"""
361
 
        raise errors.TransportNotPossible('http does not support move()')
 
246
        raise TransportNotPossible('http does not support move()')
362
247
 
363
248
    def delete(self, relpath):
364
249
        """Delete the item at relpath"""
365
 
        raise errors.TransportNotPossible('http does not support delete()')
366
 
 
367
 
    def external_url(self):
368
 
        """See bzrlib.transport.Transport.external_url."""
369
 
        # HTTP URL's are externally usable as long as they don't mention their
370
 
        # implementation qualifier
371
 
        url = self._parsed_url.clone()
372
 
        url.scheme = self._unqualified_scheme
373
 
        return str(url)
 
250
        raise TransportNotPossible('http does not support delete()')
374
251
 
375
252
    def is_readonly(self):
376
253
        """See Transport.is_readonly."""
383
260
    def stat(self, relpath):
384
261
        """Return the stat information for a file.
385
262
        """
386
 
        raise errors.TransportNotPossible('http does not support stat()')
 
263
        raise TransportNotPossible('http does not support stat()')
387
264
 
388
265
    def lock_read(self, relpath):
389
266
        """Lock the given file for shared (read) access.
404
281
 
405
282
        :return: A lock object, which should be passed to Transport.unlock()
406
283
        """
407
 
        raise errors.TransportNotPossible('http does not support lock_write()')
408
 
 
409
 
    def _attempted_range_header(self, offsets, tail_amount):
410
 
        """Prepare a HTTP Range header at a level the server should accept.
411
 
 
412
 
        :return: the range header representing offsets/tail_amount or None if
413
 
            no header can be built.
414
 
        """
415
 
 
416
 
        if self._range_hint == 'multi':
417
 
            # Generate the header describing all offsets
418
 
            return self._range_header(offsets, tail_amount)
419
 
        elif self._range_hint == 'single':
420
 
            # Combine all the requested ranges into a single
421
 
            # encompassing one
422
 
            if len(offsets) > 0:
423
 
                if tail_amount not in (0, None):
424
 
                    # Nothing we can do here to combine ranges with tail_amount
425
 
                    # in a single range, just returns None. The whole file
426
 
                    # should be downloaded.
427
 
                    return None
428
 
                else:
429
 
                    start = offsets[0].start
430
 
                    last = offsets[-1]
431
 
                    end = last.start + last.length - 1
432
 
                    whole = self._coalesce_offsets([(start, end - start + 1)],
433
 
                                                   limit=0, fudge_factor=0)
434
 
                    return self._range_header(list(whole), 0)
435
 
            else:
436
 
                # Only tail_amount, requested, leave range_header
437
 
                # do its work
438
 
                return self._range_header(offsets, tail_amount)
439
 
        else:
440
 
            return None
441
 
 
442
 
    @staticmethod
443
 
    def _range_header(ranges, tail_amount):
444
 
        """Turn a list of bytes ranges into a HTTP Range header value.
445
 
 
446
 
        :param ranges: A list of _CoalescedOffset
447
 
        :param tail_amount: The amount to get from the end of the file.
448
 
 
449
 
        :return: HTTP range header string.
450
 
 
451
 
        At least a non-empty ranges *or* a tail_amount must be
452
 
        provided.
453
 
        """
454
 
        strings = []
455
 
        for offset in ranges:
456
 
            strings.append('%d-%d' % (offset.start,
457
 
                                      offset.start + offset.length - 1))
458
 
 
459
 
        if tail_amount:
460
 
            strings.append('-%d' % tail_amount)
461
 
 
462
 
        return ','.join(strings)
463
 
 
464
 
    def _redirected_to(self, source, target):
465
 
        """Returns a transport suitable to re-issue a redirected request.
466
 
 
467
 
        :param source: The source url as returned by the server.
468
 
        :param target: The target url as returned by the server.
469
 
 
470
 
        The redirection can be handled only if the relpath involved is not
471
 
        renamed by the redirection.
472
 
 
473
 
        :returns: A transport or None.
474
 
        """
475
 
        parsed_source = self._split_url(source)
476
 
        parsed_target = self._split_url(target)
477
 
        pl = len(self._parsed_url.path)
478
 
        # determine the excess tail - the relative path that was in
479
 
        # the original request but not part of this transports' URL.
480
 
        excess_tail = parsed_source.path[pl:].strip("/")
481
 
        if not target.endswith(excess_tail):
482
 
            # The final part of the url has been renamed, we can't handle the
483
 
            # redirection.
484
 
            return None
485
 
 
486
 
        target_path = parsed_target.path
487
 
        if excess_tail:
488
 
            # Drop the tail that was in the redirect but not part of
489
 
            # the path of this transport.
490
 
            target_path = target_path[:-len(excess_tail)]
491
 
 
492
 
        if parsed_target.scheme in ('http', 'https'):
493
 
            # Same protocol family (i.e. http[s]), we will preserve the same
494
 
            # http client implementation when a redirection occurs from one to
495
 
            # the other (otherwise users may be surprised that bzr switches
496
 
            # from one implementation to the other, and devs may suffer
497
 
            # debugging it).
498
 
            if (parsed_target.scheme == self._unqualified_scheme
499
 
                and parsed_target.host == self._parsed_url.host
500
 
                and parsed_target.port == self._parsed_url.port
501
 
                and (parsed_target.user is None or
502
 
                     parsed_target.user == self._parsed_url.user)):
503
 
                # If a user is specified, it should match, we don't care about
504
 
                # passwords, wrong passwords will be rejected anyway.
505
 
                return self.clone(target_path)
506
 
            else:
507
 
                # Rebuild the url preserving the scheme qualification and the
508
 
                # credentials (if they don't apply, the redirected to server
509
 
                # will tell us, but if they do apply, we avoid prompting the
510
 
                # user)
511
 
                redir_scheme = parsed_target.scheme + '+' + self._impl_name
512
 
                new_url = self._unsplit_url(redir_scheme,
513
 
                    self._parsed_url.user,
514
 
                    self._parsed_url.password,
515
 
                    parsed_target.host, parsed_target.port,
516
 
                    target_path)
517
 
                return transport.get_transport_from_url(new_url)
518
 
        else:
519
 
            # Redirected to a different protocol
520
 
            new_url = self._unsplit_url(parsed_target.scheme,
521
 
                    parsed_target.user,
522
 
                    parsed_target.password,
523
 
                    parsed_target.host, parsed_target.port,
524
 
                    target_path)
525
 
            return transport.get_transport_from_url(new_url)
526
 
 
527
 
 
528
 
# TODO: May be better located in smart/medium.py with the other
529
 
# SmartMedium classes
530
 
class SmartClientHTTPMedium(medium.SmartClientMedium):
531
 
 
532
 
    def __init__(self, http_transport):
533
 
        super(SmartClientHTTPMedium, self).__init__(http_transport.base)
534
 
        # We don't want to create a circular reference between the http
535
 
        # transport and its associated medium. Since the transport will live
536
 
        # longer than the medium, the medium keep only a weak reference to its
537
 
        # transport.
538
 
        self._http_transport_ref = weakref.ref(http_transport)
539
 
 
540
 
    def get_request(self):
541
 
        return SmartClientHTTPMediumRequest(self)
542
 
 
543
 
    def should_probe(self):
544
 
        return True
545
 
 
546
 
    def remote_path_from_transport(self, transport):
547
 
        # Strip the optional 'bzr+' prefix from transport so it will have the
548
 
        # same scheme as self.
549
 
        transport_base = transport.base
550
 
        if transport_base.startswith('bzr+'):
551
 
            transport_base = transport_base[4:]
552
 
        rel_url = urlutils.relative_url(self.base, transport_base)
553
 
        return urlutils.unquote(rel_url)
554
 
 
555
 
    def send_http_smart_request(self, bytes):
556
 
        try:
557
 
            # Get back the http_transport hold by the weak reference
558
 
            t = self._http_transport_ref()
559
 
            code, body_filelike = t._post(bytes)
560
 
            if code != 200:
561
 
                raise errors.InvalidHttpResponse(
562
 
                    t._remote_path('.bzr/smart'),
563
 
                    'Expected 200 response code, got %r' % (code,))
564
 
        except (errors.InvalidHttpResponse, errors.ConnectionReset), e:
565
 
            raise errors.SmartProtocolError(str(e))
566
 
        return body_filelike
567
 
 
568
 
    def _report_activity(self, bytes, direction):
569
 
        """See SmartMedium._report_activity.
570
 
 
571
 
        Does nothing; the underlying plain HTTP transport will report the
572
 
        activity that this medium would report.
573
 
        """
574
 
        pass
575
 
 
576
 
    def disconnect(self):
577
 
        """See SmartClientMedium.disconnect()."""
578
 
        t = self._http_transport_ref()
579
 
        t.disconnect()
580
 
 
581
 
 
582
 
# TODO: May be better located in smart/medium.py with the other
583
 
# SmartMediumRequest classes
584
 
class SmartClientHTTPMediumRequest(medium.SmartClientMediumRequest):
585
 
    """A SmartClientMediumRequest that works with an HTTP medium."""
586
 
 
587
 
    def __init__(self, client_medium):
588
 
        medium.SmartClientMediumRequest.__init__(self, client_medium)
589
 
        self._buffer = ''
590
 
 
591
 
    def _accept_bytes(self, bytes):
592
 
        self._buffer += bytes
593
 
 
594
 
    def _finished_writing(self):
595
 
        data = self._medium.send_http_smart_request(self._buffer)
596
 
        self._response_body = data
597
 
 
598
 
    def _read_bytes(self, count):
599
 
        """See SmartClientMediumRequest._read_bytes."""
600
 
        return self._response_body.read(count)
601
 
 
602
 
    def _read_line(self):
603
 
        line, excess = medium._get_line(self._response_body.read)
604
 
        if excess != '':
605
 
            raise AssertionError(
606
 
                '_get_line returned excess bytes, but this mediumrequest '
607
 
                'cannot handle excess. (%r)' % (excess,))
608
 
        return line
609
 
 
610
 
    def _finished_reading(self):
611
 
        """See SmartClientMediumRequest._finished_reading."""
612
 
        pass
613
 
 
614
 
 
615
 
def unhtml_roughly(maybe_html, length_limit=1000):
616
 
    """Very approximate html->text translation, for presenting error bodies.
617
 
 
618
 
    :param length_limit: Truncate the result to this many characters.
619
 
 
620
 
    >>> unhtml_roughly("<b>bad</b> things happened\\n")
621
 
    ' bad  things happened '
622
 
    """
623
 
    return re.subn(r"(<[^>]*>|\n|&nbsp;)", " ", maybe_html)[0][:length_limit]
 
284
        raise TransportNotPossible('http does not support lock_write()')
 
285
 
 
286
 
 
287
#---------------- test server facilities ----------------
 
288
import BaseHTTPServer, SimpleHTTPServer, socket, time
 
289
import threading
 
290
 
 
291
 
 
292
class WebserverNotAvailable(Exception):
 
293
    pass
 
294
 
 
295
 
 
296
class BadWebserverPath(ValueError):
 
297
    def __str__(self):
 
298
        return 'path %s is not in %s' % self.args
 
299
 
 
300
 
 
301
class TestingHTTPRequestHandler(SimpleHTTPServer.SimpleHTTPRequestHandler):
 
302
 
 
303
    def log_message(self, format, *args):
 
304
        self.server.test_case.log("webserver - %s - - [%s] %s",
 
305
                                  self.address_string(),
 
306
                                  self.log_date_time_string(),
 
307
                                  format%args)
 
308
 
 
309
    def handle_one_request(self):
 
310
        """Handle a single HTTP request.
 
311
 
 
312
        You normally don't need to override this method; see the class
 
313
        __doc__ string for information on how to handle specific HTTP
 
314
        commands such as GET and POST.
 
315
 
 
316
        """
 
317
        for i in xrange(1,11): # Don't try more than 10 times
 
318
            try:
 
319
                self.raw_requestline = self.rfile.readline()
 
320
            except socket.error, e:
 
321
                if e.args[0] in (errno.EAGAIN, errno.EWOULDBLOCK):
 
322
                    # omitted for now because some tests look at the log of
 
323
                    # the server and expect to see no errors.  see recent
 
324
                    # email thread. -- mbp 20051021. 
 
325
                    ## self.log_message('EAGAIN (%d) while reading from raw_requestline' % i)
 
326
                    time.sleep(0.01)
 
327
                    continue
 
328
                raise
 
329
            else:
 
330
                break
 
331
        if not self.raw_requestline:
 
332
            self.close_connection = 1
 
333
            return
 
334
        if not self.parse_request(): # An error code has been sent, just exit
 
335
            return
 
336
        mname = 'do_' + self.command
 
337
        if not hasattr(self, mname):
 
338
            self.send_error(501, "Unsupported method (%r)" % self.command)
 
339
            return
 
340
        method = getattr(self, mname)
 
341
        method()
 
342
 
 
343
class TestingHTTPServer(BaseHTTPServer.HTTPServer):
 
344
    def __init__(self, server_address, RequestHandlerClass, test_case):
 
345
        BaseHTTPServer.HTTPServer.__init__(self, server_address,
 
346
                                                RequestHandlerClass)
 
347
        self.test_case = test_case
 
348
 
 
349
 
 
350
class HttpServer(Server):
 
351
    """A test server for http transports."""
 
352
 
 
353
    _HTTP_PORTS = range(13000, 0x8000)
 
354
 
 
355
    def _http_start(self):
 
356
        httpd = None
 
357
        for port in self._HTTP_PORTS:
 
358
            try:
 
359
                httpd = TestingHTTPServer(('localhost', port),
 
360
                                          TestingHTTPRequestHandler,
 
361
                                          self)
 
362
            except socket.error, e:
 
363
                if e.args[0] == errno.EADDRINUSE:
 
364
                    continue
 
365
                print >>sys.stderr, "Cannot run webserver :-("
 
366
                raise
 
367
            else:
 
368
                break
 
369
 
 
370
        if httpd is None:
 
371
            raise WebserverNotAvailable("Cannot run webserver :-( "
 
372
                                        "no free ports in range %s..%s" %
 
373
                                        (_HTTP_PORTS[0], _HTTP_PORTS[-1]))
 
374
 
 
375
        self._http_base_url = 'http://localhost:%s/' % port
 
376
        self._http_starting.release()
 
377
        httpd.socket.settimeout(0.1)
 
378
 
 
379
        while self._http_running:
 
380
            try:
 
381
                httpd.handle_request()
 
382
            except socket.timeout:
 
383
                pass
 
384
 
 
385
    def _get_remote_url(self, path):
 
386
        path_parts = path.split(os.path.sep)
 
387
        if os.path.isabs(path):
 
388
            if path_parts[:len(self._local_path_parts)] != \
 
389
                   self._local_path_parts:
 
390
                raise BadWebserverPath(path, self.test_dir)
 
391
            remote_path = '/'.join(path_parts[len(self._local_path_parts):])
 
392
        else:
 
393
            remote_path = '/'.join(path_parts)
 
394
 
 
395
        self._http_starting.acquire()
 
396
        self._http_starting.release()
 
397
        return self._http_base_url + remote_path
 
398
 
 
399
    def log(self, *args, **kwargs):
 
400
        """Capture Server log output."""
 
401
        self.logs.append(args[3])
 
402
 
 
403
    def setUp(self):
 
404
        """See bzrlib.transport.Server.setUp."""
 
405
        self._home_dir = os.getcwdu()
 
406
        self._local_path_parts = self._home_dir.split(os.path.sep)
 
407
        self._http_starting = threading.Lock()
 
408
        self._http_starting.acquire()
 
409
        self._http_running = True
 
410
        self._http_base_url = None
 
411
        self._http_thread = threading.Thread(target=self._http_start)
 
412
        self._http_thread.setDaemon(True)
 
413
        self._http_thread.start()
 
414
        self._http_proxy = os.environ.get("http_proxy")
 
415
        if self._http_proxy is not None:
 
416
            del os.environ["http_proxy"]
 
417
        self.logs = []
 
418
 
 
419
    def tearDown(self):
 
420
        """See bzrlib.transport.Server.tearDown."""
 
421
        self._http_running = False
 
422
        self._http_thread.join()
 
423
        if self._http_proxy is not None:
 
424
            import os
 
425
            os.environ["http_proxy"] = self._http_proxy
 
426
 
 
427
    def get_url(self):
 
428
        """See bzrlib.transport.Server.get_url."""
 
429
        return self._get_remote_url(self._home_dir)
 
430
        
 
431
    def get_bogus_url(self):
 
432
        """See bzrlib.transport.Server.get_bogus_url."""
 
433
        return 'http://jasldkjsalkdjalksjdkljasd'
 
434
 
 
435
 
 
436
def get_test_permutations():
 
437
    """Return the permutations to be used in testing."""
 
438
    warn("There are no HTTPS transport provider tests yet.")
 
439
    return [(HttpTransport, HttpServer),
 
440
            ]