~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/transport/http.py

Move branch implementations tests into a package.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2005, 2006, 2007 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
 
 
19
import os, errno
22
20
from cStringIO import StringIO
23
 
import mimetools
24
 
import re
 
21
import urllib, urllib2
25
22
import urlparse
26
 
import urllib
27
 
import sys
28
 
import weakref
 
23
from warnings import warn
29
24
 
30
 
from bzrlib import (
31
 
    debug,
32
 
    errors,
33
 
    ui,
34
 
    urlutils,
35
 
    )
36
 
from bzrlib.smart import medium
37
 
from bzrlib.symbol_versioning import (
38
 
        deprecated_method,
39
 
        )
 
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
40
30
from bzrlib.trace import mutter
41
 
from bzrlib.transport import (
42
 
    ConnectedTransport,
43
 
    _CoalescedOffset,
44
 
    get_transport,
45
 
    Transport,
46
 
    )
47
 
 
48
 
# TODO: This is not used anymore by HttpTransport_urllib
49
 
# (extracting the auth info and prompting the user for a password
50
 
# have been split), only the tests still use it. It should be
51
 
# deleted and the tests rewritten ASAP to stay in sync.
 
31
 
 
32
 
52
33
def extract_auth(url, password_manager):
53
 
    """Extract auth parameters from am HTTP/HTTPS url and add them to the given
 
34
    """
 
35
    Extract auth parameters from am HTTP/HTTPS url and add them to the given
54
36
    password manager.  Return the url, minus those auth parameters (which
55
37
    confuse urllib2).
56
38
    """
57
 
    if not re.match(r'^(https?)(\+\w+)?://', url):
58
 
        raise ValueError(
59
 
            'invalid absolute url %r' % (url,))
60
 
    scheme, netloc, path, query, fragment = urlparse.urlsplit(url)
61
 
 
62
 
    if '@' in netloc:
63
 
        auth, netloc = netloc.split('@', 1)
 
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)
64
49
        if ':' in auth:
65
50
            username, password = auth.split(':', 1)
66
51
        else:
67
52
            username, password = auth, None
68
 
        if ':' in netloc:
69
 
            host = netloc.split(':', 1)[0]
70
 
        else:
71
 
            host = netloc
72
 
        username = urllib.unquote(username)
 
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?
73
57
        if password is not None:
 
58
            username = urllib.unquote(username)
74
59
            password = urllib.unquote(password)
75
 
        else:
76
 
            password = ui.ui_factory.get_password(
77
 
                prompt='HTTP %(user)s@%(host)s password',
78
 
                user=username, host=host)
79
 
        password_manager.add_password(None, host, username, password)
80
 
    url = urlparse.urlunsplit((scheme, netloc, path, query, fragment))
 
60
            password_manager.add_password(None, host, username, password)
 
61
    url = scheme + '//' + host + port + path
81
62
    return url
82
 
 
83
 
 
84
 
class HttpTransportBase(ConnectedTransport):
85
 
    """Base class for http implementations.
86
 
 
87
 
    Does URL parsing, etc, but not any network IO.
88
 
 
89
 
    The protocol can be given as e.g. http+urllib://host/ to use a particular
90
 
    implementation.
 
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.
91
78
    """
92
79
 
93
 
    # _unqualified_scheme: "http" or "https"
94
 
    # _scheme: may have "+pycurl", etc
95
 
 
96
 
    def __init__(self, base, _impl_name, _from_transport=None):
 
80
    def __init__(self, base):
97
81
        """Set the base path where files will be stored."""
98
 
        proto_match = re.match(r'^(https?)(\+\w+)?://', base)
99
 
        if not proto_match:
100
 
            raise AssertionError("not a http url: %r" % base)
101
 
        self._unqualified_scheme = proto_match.group(1)
102
 
        self._impl_name = _impl_name
103
 
        super(HttpTransportBase, self).__init__(base,
104
 
                                                _from_transport=_from_transport)
105
 
        self._medium = None
106
 
        # range hint is handled dynamically throughout the life
107
 
        # of the transport object. We start by trying multi-range
108
 
        # requests and if the server returns bogus results, we
109
 
        # retry with single range requests and, finally, we
110
 
        # forget about range if the server really can't
111
 
        # understand. Once acquired, this piece of info is
112
 
        # propagated to clones.
113
 
        if _from_transport is not None:
114
 
            self._range_hint = _from_transport._range_hint
115
 
        else:
116
 
            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, '', '', ''))
117
145
 
118
146
    def has(self, relpath):
119
 
        raise NotImplementedError("has() is abstract on %r" % self)
120
 
 
121
 
    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):
122
179
        """Get the file at the given relative path.
123
180
 
124
181
        :param relpath: The relative path to the file
125
182
        """
126
 
        code, response_file = self._get(relpath, None)
127
 
        # FIXME: some callers want an iterable... One step forward, three steps
128
 
        # backwards :-/ And not only an iterable, but an iterable that can be
129
 
        # seeked backwards, so we will never be able to do that.  One such
130
 
        # known client is bzrlib.bundle.serializer.v4.get_bundle_reader. At the
131
 
        # time of this writing it's even the only known client -- vila20071203
132
 
        return StringIO(response_file.read())
133
 
 
134
 
    def _get(self, relpath, ranges, tail_amount=0):
135
 
        """Get a file, or part of a file.
136
 
 
137
 
        :param relpath: Path relative to transport base URL
138
 
        :param ranges: None to get the whole file;
139
 
            or  a list of _CoalescedOffset to fetch parts of a file.
140
 
        :param tail_amount: The amount to get from the end of the file.
141
 
 
142
 
        :returns: (http_code, result_file)
143
 
        """
144
 
        raise NotImplementedError(self._get)
145
 
 
146
 
    def _remote_path(self, relpath):
147
 
        """See ConnectedTransport._remote_path.
148
 
 
149
 
        user and passwords are not embedded in the path provided to the server.
150
 
        """
151
 
        relative = urlutils.unescape(relpath).encode('utf-8')
152
 
        path = self._combine_paths(self._path, relative)
153
 
        return self._unsplit_url(self._unqualified_scheme,
154
 
                                 None, None, self._host, self._port, path)
155
 
 
156
 
    def _create_auth(self):
157
 
        """Returns a dict returning the credentials provided at build time."""
158
 
        auth = dict(host=self._host, port=self._port,
159
 
                    user=self._user, password=self._password,
160
 
                    protocol=self._unqualified_scheme,
161
 
                    path=self._path)
162
 
        return auth
163
 
 
164
 
    def get_smart_medium(self):
165
 
        """See Transport.get_smart_medium."""
166
 
        if self._medium is None:
167
 
            # Since medium holds some state (smart server probing at least), we
168
 
            # need to keep it around. Note that this is needed because medium
169
 
            # has the same 'base' attribute as the transport so it can't be
170
 
            # shared between transports having different bases.
171
 
            self._medium = SmartClientHTTPMedium(self)
172
 
        return self._medium
173
 
 
174
 
 
175
 
    def _degrade_range_hint(self, relpath, ranges, exc_info):
176
 
        if self._range_hint == 'multi':
177
 
            self._range_hint = 'single'
178
 
            mutter('Retry "%s" with single range request' % relpath)
179
 
        elif self._range_hint == 'single':
180
 
            self._range_hint = None
181
 
            mutter('Retry "%s" without ranges' % relpath)
182
 
        else:
183
 
            # We tried all the tricks, but nothing worked. We re-raise the
184
 
            # original exception; the 'mutter' calls above will indicate that
185
 
            # further tries were unsuccessful
186
 
            raise exc_info[0], exc_info[1], exc_info[2]
187
 
 
188
 
    # _coalesce_offsets is a helper for readv, it try to combine ranges without
189
 
    # degrading readv performances. _bytes_to_read_before_seek is the value
190
 
    # used for the limit parameter and has been tuned for other transports. For
191
 
    # HTTP, the name is inappropriate but the parameter is still useful and
192
 
    # helps reduce the number of chunks in the response. The overhead for a
193
 
    # chunk (headers, length, footer around the data itself is variable but
194
 
    # around 50 bytes. We use 128 to reduce the range specifiers that appear in
195
 
    # the header, some servers (notably Apache) enforce a maximum length for a
196
 
    # header and issue a '400: Bad request' error when too much ranges are
197
 
    # specified.
198
 
    _bytes_to_read_before_seek = 128
199
 
    # No limit on the offset number that get combined into one, we are trying
200
 
    # to avoid downloading the whole file.
201
 
    _max_readv_combine = 0
202
 
    # By default Apache has a limit of ~400 ranges before replying with a 400
203
 
    # Bad Request. So we go underneath that amount to be safe.
204
 
    _max_get_ranges = 200
205
 
    # We impose no limit on the range size. But see _pycurl.py for a different
206
 
    # use.
207
 
    _get_max_size = 0
208
 
 
209
 
    def _readv(self, relpath, offsets):
210
 
        """Get parts of the file at the given relative path.
211
 
 
212
 
        :param offsets: A list of (offset, size) tuples.
213
 
        :param return: A list or generator of (offset, data) tuples
214
 
        """
215
 
        # offsets may be a generator, we will iterate it several times, so
216
 
        # build a list
217
 
        offsets = list(offsets)
218
 
 
219
 
        try_again = True
220
 
        retried_offset = None
221
 
        while try_again:
222
 
            try_again = False
223
 
 
224
 
            # Coalesce the offsets to minimize the GET requests issued
225
 
            sorted_offsets = sorted(offsets)
226
 
            coalesced = self._coalesce_offsets(
227
 
                sorted_offsets, limit=self._max_readv_combine,
228
 
                fudge_factor=self._bytes_to_read_before_seek,
229
 
                max_size=self._get_max_size)
230
 
 
231
 
            # Turn it into a list, we will iterate it several times
232
 
            coalesced = list(coalesced)
233
 
            if 'http' in debug.debug_flags:
234
 
                mutter('http readv of %s  offsets => %s collapsed %s',
235
 
                    relpath, len(offsets), len(coalesced))
236
 
 
237
 
            # Cache the data read, but only until it's been used
238
 
            data_map = {}
239
 
            # We will iterate on the data received from the GET requests and
240
 
            # serve the corresponding offsets respecting the initial order. We
241
 
            # need an offset iterator for that.
242
 
            iter_offsets = iter(offsets)
243
 
            cur_offset_and_size = iter_offsets.next()
244
 
 
245
 
            try:
246
 
                for cur_coal, rfile in self._coalesce_readv(relpath, coalesced):
247
 
                    # Split the received chunk
248
 
                    for offset, size in cur_coal.ranges:
249
 
                        start = cur_coal.start + offset
250
 
                        rfile.seek(start, 0)
251
 
                        data = rfile.read(size)
252
 
                        data_len = len(data)
253
 
                        if data_len != size:
254
 
                            raise errors.ShortReadvError(relpath, start, size,
255
 
                                                         actual=data_len)
256
 
                        if (start, size) == cur_offset_and_size:
257
 
                            # The offset requested are sorted as the coalesced
258
 
                            # ones, no need to cache. Win !
259
 
                            yield cur_offset_and_size[0], data
260
 
                            cur_offset_and_size = iter_offsets.next()
261
 
                        else:
262
 
                            # Different sorting. We need to cache.
263
 
                            data_map[(start, size)] = data
264
 
 
265
 
                    # Yield everything we can
266
 
                    while cur_offset_and_size in data_map:
267
 
                        # Clean the cached data since we use it
268
 
                        # XXX: will break if offsets contains duplicates --
269
 
                        # vila20071129
270
 
                        this_data = data_map.pop(cur_offset_and_size)
271
 
                        yield cur_offset_and_size[0], this_data
272
 
                        cur_offset_and_size = iter_offsets.next()
273
 
 
274
 
            except (errors.ShortReadvError, errors.InvalidRange,
275
 
                    errors.InvalidHttpRange), e:
276
 
                mutter('Exception %r: %s during http._readv',e, e)
277
 
                if (not isinstance(e, errors.ShortReadvError)
278
 
                    or retried_offset == cur_offset_and_size):
279
 
                    # We don't degrade the range hint for ShortReadvError since
280
 
                    # they do not indicate a problem with the server ability to
281
 
                    # handle ranges. Except when we fail to get back a required
282
 
                    # offset twice in a row. In that case, falling back to
283
 
                    # single range or whole file should help or end up in a
284
 
                    # fatal exception.
285
 
                    self._degrade_range_hint(relpath, coalesced, sys.exc_info())
286
 
                # Some offsets may have been already processed, so we retry
287
 
                # only the unsuccessful ones.
288
 
                offsets = [cur_offset_and_size] + [o for o in iter_offsets]
289
 
                retried_offset = cur_offset_and_size
290
 
                try_again = True
291
 
 
292
 
    def _coalesce_readv(self, relpath, coalesced):
293
 
        """Issue several GET requests to satisfy the coalesced offsets"""
294
 
 
295
 
        def get_and_yield(relpath, coalesced):
296
 
            if coalesced:
297
 
                # Note that the _get below may raise
298
 
                # errors.InvalidHttpRange. It's the caller's responsibility to
299
 
                # decide how to retry since it may provide different coalesced
300
 
                # offsets.
301
 
                code, rfile = self._get(relpath, coalesced)
302
 
                for coal in coalesced:
303
 
                    yield coal, rfile
304
 
 
305
 
        if self._range_hint is None:
306
 
            # Download whole file
307
 
            for c, rfile in get_and_yield(relpath, coalesced):
308
 
                yield c, rfile
309
 
        else:
310
 
            total = len(coalesced)
311
 
            if self._range_hint == 'multi':
312
 
                max_ranges = self._max_get_ranges
313
 
            elif self._range_hint == 'single':
314
 
                max_ranges = total
315
 
            else:
316
 
                raise AssertionError("Unknown _range_hint %r"
317
 
                                     % (self._range_hint,))
318
 
            # TODO: Some web servers may ignore the range requests and return
319
 
            # the whole file, we may want to detect that and avoid further
320
 
            # requests.
321
 
            # Hint: test_readv_multiple_get_requests will fail once we do that
322
 
            cumul = 0
323
 
            ranges = []
324
 
            for coal in coalesced:
325
 
                if ((self._get_max_size > 0
326
 
                     and cumul + coal.length > self._get_max_size)
327
 
                    or len(ranges) >= max_ranges):
328
 
                    # Get that much and yield
329
 
                    for c, rfile in get_and_yield(relpath, ranges):
330
 
                        yield c, rfile
331
 
                    # Restart with the current offset
332
 
                    ranges = [coal]
333
 
                    cumul = coal.length
334
 
                else:
335
 
                    ranges.append(coal)
336
 
                    cumul += coal.length
337
 
            # Get the rest and yield
338
 
            for c, rfile in get_and_yield(relpath, ranges):
339
 
                yield c, rfile
340
 
 
341
 
    def recommended_page_size(self):
342
 
        """See Transport.recommended_page_size().
343
 
 
344
 
        For HTTP we suggest a large page size to reduce the overhead
345
 
        introduced by latency.
346
 
        """
347
 
        return 64 * 1024
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.
 
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.
362
204
 
363
205
        :param relpath: Location to put the contents, relative to base.
364
 
        :param f:       File-like object.
 
206
        :param f:       File-like or string object.
365
207
        """
366
 
        raise errors.TransportNotPossible('http PUT not supported')
 
208
        raise TransportNotPossible('http PUT not supported')
367
209
 
368
210
    def mkdir(self, relpath, mode=None):
369
211
        """Create a directory at the given path."""
370
 
        raise errors.TransportNotPossible('http does not support mkdir()')
 
212
        raise TransportNotPossible('http does not support mkdir()')
371
213
 
372
214
    def rmdir(self, relpath):
373
215
        """See Transport.rmdir."""
374
 
        raise errors.TransportNotPossible('http does not support rmdir()')
 
216
        raise TransportNotPossible('http does not support rmdir()')
375
217
 
376
 
    def append_file(self, relpath, f, mode=None):
 
218
    def append(self, relpath, f):
377
219
        """Append the text in the file-like object into the final
378
220
        location.
379
221
        """
380
 
        raise errors.TransportNotPossible('http does not support append()')
 
222
        raise TransportNotPossible('http does not support append()')
381
223
 
382
224
    def copy(self, rel_from, rel_to):
383
225
        """Copy the item at rel_from to the location at rel_to"""
384
 
        raise errors.TransportNotPossible('http does not support copy()')
 
226
        raise TransportNotPossible('http does not support copy()')
385
227
 
386
228
    def copy_to(self, relpaths, other, mode=None, pb=None):
387
229
        """Copy a set of entries from self into another Transport.
394
236
        # At this point HttpTransport might be able to check and see if
395
237
        # the remote location is the same, and rather than download, and
396
238
        # 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()')
 
239
        if isinstance(other, HttpTransport):
 
240
            raise TransportNotPossible('http cannot be the target of copy_to()')
400
241
        else:
401
 
            return super(HttpTransportBase, self).\
402
 
                    copy_to(relpaths, other, mode=mode, pb=pb)
 
242
            return super(HttpTransport, self).copy_to(relpaths, other, mode=mode, pb=pb)
403
243
 
404
244
    def move(self, rel_from, rel_to):
405
245
        """Move the item at rel_from to the location at rel_to"""
406
 
        raise errors.TransportNotPossible('http does not support move()')
 
246
        raise TransportNotPossible('http does not support move()')
407
247
 
408
248
    def delete(self, relpath):
409
249
        """Delete the item at relpath"""
410
 
        raise errors.TransportNotPossible('http does not support delete()')
411
 
 
412
 
    def external_url(self):
413
 
        """See bzrlib.transport.Transport.external_url."""
414
 
        # HTTP URL's are externally usable as long as they don't mention their
415
 
        # implementation qualifier
416
 
        return self._unsplit_url(self._unqualified_scheme,
417
 
                                 self._user, self._password,
418
 
                                 self._host, self._port,
419
 
                                 self._path)
 
250
        raise TransportNotPossible('http does not support delete()')
420
251
 
421
252
    def is_readonly(self):
422
253
        """See Transport.is_readonly."""
429
260
    def stat(self, relpath):
430
261
        """Return the stat information for a file.
431
262
        """
432
 
        raise errors.TransportNotPossible('http does not support stat()')
 
263
        raise TransportNotPossible('http does not support stat()')
433
264
 
434
265
    def lock_read(self, relpath):
435
266
        """Lock the given file for shared (read) access.
450
281
 
451
282
        :return: A lock object, which should be passed to Transport.unlock()
452
283
        """
453
 
        raise errors.TransportNotPossible('http does not support lock_write()')
454
 
 
455
 
    def _attempted_range_header(self, offsets, tail_amount):
456
 
        """Prepare a HTTP Range header at a level the server should accept.
457
 
 
458
 
        :return: the range header representing offsets/tail_amount or None if
459
 
            no header can be built.
460
 
        """
461
 
 
462
 
        if self._range_hint == 'multi':
463
 
            # Generate the header describing all offsets
464
 
            return self._range_header(offsets, tail_amount)
465
 
        elif self._range_hint == 'single':
466
 
            # Combine all the requested ranges into a single
467
 
            # encompassing one
468
 
            if len(offsets) > 0:
469
 
                if tail_amount not in (0, None):
470
 
                    # Nothing we can do here to combine ranges with tail_amount
471
 
                    # in a single range, just returns None. The whole file
472
 
                    # should be downloaded.
473
 
                    return None
474
 
                else:
475
 
                    start = offsets[0].start
476
 
                    last = offsets[-1]
477
 
                    end = last.start + last.length - 1
478
 
                    whole = self._coalesce_offsets([(start, end - start + 1)],
479
 
                                                   limit=0, fudge_factor=0)
480
 
                    return self._range_header(list(whole), 0)
481
 
            else:
482
 
                # Only tail_amount, requested, leave range_header
483
 
                # do its work
484
 
                return self._range_header(offsets, tail_amount)
485
 
        else:
486
 
            return None
487
 
 
488
 
    @staticmethod
489
 
    def _range_header(ranges, tail_amount):
490
 
        """Turn a list of bytes ranges into a HTTP Range header value.
491
 
 
492
 
        :param ranges: A list of _CoalescedOffset
493
 
        :param tail_amount: The amount to get from the end of the file.
494
 
 
495
 
        :return: HTTP range header string.
496
 
 
497
 
        At least a non-empty ranges *or* a tail_amount must be
498
 
        provided.
499
 
        """
500
 
        strings = []
501
 
        for offset in ranges:
502
 
            strings.append('%d-%d' % (offset.start,
503
 
                                      offset.start + offset.length - 1))
504
 
 
505
 
        if tail_amount:
506
 
            strings.append('-%d' % tail_amount)
507
 
 
508
 
        return ','.join(strings)
509
 
 
510
 
    def _redirected_to(self, source, target):
511
 
        """Returns a transport suitable to re-issue a redirected request.
512
 
 
513
 
        :param source: The source url as returned by the server.
514
 
        :param target: The target url as returned by the server.
515
 
 
516
 
        The redirection can be handled only if the relpath involved is not
517
 
        renamed by the redirection.
518
 
 
519
 
        :returns: A transport or None.
520
 
        """
521
 
        def relpath(abspath):
522
 
            """Returns the path relative to our base.
523
 
 
524
 
            The constraints are weaker than the real relpath method because the
525
 
            abspath is coming from the server and may slightly differ from our
526
 
            base. We don't check the scheme, host, port, user, password parts,
527
 
            relying on the caller to give us a proper url (i.e. one returned by
528
 
            the server mirroring the one we sent).
529
 
            """
530
 
            (scheme,
531
 
             user, password,
532
 
             host, port,
533
 
             path) = self._split_url(abspath)
534
 
            pl = len(self._path)
535
 
            return path[pl:].strip('/')
536
 
 
537
 
        relpath = relpath(source)
538
 
        if not target.endswith(relpath):
539
 
            # The final part of the url has been renamed, we can't handle the
540
 
            # redirection.
541
 
            return None
542
 
        new_transport = None
543
 
        (scheme,
544
 
         user, password,
545
 
         host, port,
546
 
         path) = self._split_url(target)
547
 
        # Recalculate base path. This is needed to ensure that when the
548
 
        # redirected tranport will be used to re-try whatever request was
549
 
        # redirected, we end up with the same url
550
 
        base_path = path[:-len(relpath)]
551
 
        if scheme in ('http', 'https'):
552
 
            # Same protocol family (i.e. http[s]), we will preserve the same
553
 
            # http client implementation when a redirection occurs from one to
554
 
            # the other (otherwise users may be surprised that bzr switches
555
 
            # from one implementation to the other, and devs may suffer
556
 
            # debugging it).
557
 
            if (scheme == self._unqualified_scheme
558
 
                and host == self._host
559
 
                and port == self._port
560
 
                and (user is None or user == self._user)):
561
 
                # If a user is specified, it should match, we don't care about
562
 
                # passwords, wrong passwords will be rejected anyway.
563
 
                new_transport = self.clone(base_path)
564
 
            else:
565
 
                # Rebuild the url preserving the scheme qualification and the
566
 
                # credentials (if they don't apply, the redirected to server
567
 
                # will tell us, but if they do apply, we avoid prompting the
568
 
                # user)
569
 
                redir_scheme = scheme + '+' + self._impl_name
570
 
                new_url = self._unsplit_url(redir_scheme,
571
 
                                            self._user, self._password,
572
 
                                            host, port,
573
 
                                            base_path)
574
 
                new_transport = get_transport(new_url)
575
 
        else:
576
 
            # Redirected to a different protocol
577
 
            new_url = self._unsplit_url(scheme,
578
 
                                        user, password,
579
 
                                        host, port,
580
 
                                        base_path)
581
 
            new_transport = get_transport(new_url)
582
 
        return new_transport
583
 
 
584
 
 
585
 
# TODO: May be better located in smart/medium.py with the other
586
 
# SmartMedium classes
587
 
class SmartClientHTTPMedium(medium.SmartClientMedium):
588
 
 
589
 
    def __init__(self, http_transport):
590
 
        super(SmartClientHTTPMedium, self).__init__(http_transport.base)
591
 
        # We don't want to create a circular reference between the http
592
 
        # transport and its associated medium. Since the transport will live
593
 
        # longer than the medium, the medium keep only a weak reference to its
594
 
        # transport.
595
 
        self._http_transport_ref = weakref.ref(http_transport)
596
 
 
597
 
    def get_request(self):
598
 
        return SmartClientHTTPMediumRequest(self)
599
 
 
600
 
    def should_probe(self):
601
 
        return True
602
 
 
603
 
    def remote_path_from_transport(self, transport):
604
 
        # Strip the optional 'bzr+' prefix from transport so it will have the
605
 
        # same scheme as self.
606
 
        transport_base = transport.base
607
 
        if transport_base.startswith('bzr+'):
608
 
            transport_base = transport_base[4:]
609
 
        rel_url = urlutils.relative_url(self.base, transport_base)
610
 
        return urllib.unquote(rel_url)
611
 
 
612
 
    def send_http_smart_request(self, bytes):
613
 
        try:
614
 
            # Get back the http_transport hold by the weak reference
615
 
            t = self._http_transport_ref()
616
 
            code, body_filelike = t._post(bytes)
617
 
            if code != 200:
618
 
                raise InvalidHttpResponse(
619
 
                    t._remote_path('.bzr/smart'),
620
 
                    'Expected 200 response code, got %r' % (code,))
621
 
        except errors.InvalidHttpResponse, e:
622
 
            raise errors.SmartProtocolError(str(e))
623
 
        return body_filelike
624
 
 
625
 
    def _report_activity(self, bytes, direction):
626
 
        """See SmartMedium._report_activity.
627
 
 
628
 
        Does nothing; the underlying plain HTTP transport will report the
629
 
        activity that this medium would report.
630
 
        """
631
 
        pass
632
 
 
633
 
 
634
 
# TODO: May be better located in smart/medium.py with the other
635
 
# SmartMediumRequest classes
636
 
class SmartClientHTTPMediumRequest(medium.SmartClientMediumRequest):
637
 
    """A SmartClientMediumRequest that works with an HTTP medium."""
638
 
 
639
 
    def __init__(self, client_medium):
640
 
        medium.SmartClientMediumRequest.__init__(self, client_medium)
641
 
        self._buffer = ''
642
 
 
643
 
    def _accept_bytes(self, bytes):
644
 
        self._buffer += bytes
645
 
 
646
 
    def _finished_writing(self):
647
 
        data = self._medium.send_http_smart_request(self._buffer)
648
 
        self._response_body = data
649
 
 
650
 
    def _read_bytes(self, count):
651
 
        """See SmartClientMediumRequest._read_bytes."""
652
 
        return self._response_body.read(count)
653
 
 
654
 
    def _read_line(self):
655
 
        line, excess = medium._get_line(self._response_body.read)
656
 
        if excess != '':
657
 
            raise AssertionError(
658
 
                '_get_line returned excess bytes, but this mediumrequest '
659
 
                'cannot handle excess. (%r)' % (excess,))
660
 
        return line
661
 
 
662
 
    def _finished_reading(self):
663
 
        """See SmartClientMediumRequest._finished_reading."""
664
 
        pass
 
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
            ]