~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

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

Connection sharing, with redirection. without authentification.

* bzrlib/transport/http/_urllib.py:
(Request): Deleted. The _urllib2 version is shiniest.
(HttpTransport_urllib): Share connections.
(HttpTransport_urllib._perform): New method.
(HttpTransport_urllib._get): Takes the _urllib2_wrappers into
account.
(HttpTransport_urllib._get_url_impl): Deleted.
(HttpTransport_urllib._head): New method.
(HttpTransport_urllib.has): Takes the _urllib2_wrappers into
account.
(HttpTransport_urllib.copy_to, HttpTransport_urllib.move,
HttpTransport_urllib.delete): Deleted. Were carbon copies of
HttpTransportBase.

* bzrlib/tests/test_http.py:
(TestHttpConnections_urllib.test_has_on_bogus_host): The timeout
was too high, at least on Mac OS X 10.3 the test was taking a
whole *minute*. Also, the new implementation raise a nice
ConnectionError.

* bzrlib/errors.py:
(ConnectionError): Don't don't repeat ConnectionError.

* bzrlib/transport/http/_urllib2_wrappers.py: 
New file. Wrappers around urllib2 framework.

Show diffs side-by-side

added added

removed removed

Lines of Context:
20
20
from StringIO import StringIO
21
21
 
22
22
import bzrlib  # for the version
23
 
from bzrlib.errors import (TransportNotPossible, NoSuchFile, BzrError,
24
 
                           TransportError, ConnectionError)
 
23
from bzrlib.errors import (TransportNotPossible,
 
24
                           NoSuchFile,
 
25
                           BzrError,
 
26
                           TransportError,
 
27
                           ConnectionError,
 
28
                           )
25
29
from bzrlib.trace import mutter
26
30
from bzrlib.transport import register_urlparse_netloc_protocol
27
 
from bzrlib.transport.http import (HttpTransportBase, HttpServer,
28
 
                                   extract_auth, response)
 
31
from bzrlib.transport.http import (HttpTransportBase,
 
32
                                   HttpServer,
 
33
                                   extract_auth)
 
34
# TODO: handle_response should integrated into the _urllib2_wrappers
 
35
from bzrlib.transport.http.response import (
 
36
    handle_response
 
37
    )
 
38
from bzrlib.transport.http._urllib2_wrappers import (
 
39
    Request,
 
40
    Opener,
 
41
    )
29
42
 
30
43
register_urlparse_netloc_protocol('http+urllib')
31
44
 
32
 
 
33
 
class Request(urllib2.Request):
34
 
    """Request object for urllib2 that allows the method to be overridden."""
35
 
 
36
 
    method = None
37
 
 
38
 
    def get_method(self):
39
 
        if self.method is not None:
40
 
            return self.method
41
 
        else:
42
 
            return urllib2.Request.get_method(self)
43
 
 
44
 
 
45
45
class HttpTransport_urllib(HttpTransportBase):
46
46
    """Python urllib transport for http and https."""
47
47
 
 
48
    # In order to debug we have to issue our traces in syc with
 
49
    # httplib, which use print :(
 
50
    _debuglevel = 0
 
51
 
48
52
    # TODO: Implement pipelined versions of all of the *_multi() functions.
49
53
 
50
 
    def __init__(self, base, from_transport=None):
 
54
    def __init__(self, base, from_transport=None, opener=Opener()):
51
55
        """Set the base path where files will be stored."""
52
56
        super(HttpTransport_urllib, self).__init__(base)
53
 
        # HttpTransport_urllib doesn't maintain any per-transport state yet
54
 
        # so nothing to do with from_transport
 
57
        if from_transport is not None:
 
58
            self._accept_ranges = from_transport._accept_ranges
 
59
            self._connection = from_transport._connection
 
60
        else:
 
61
            self._accept_ranges = True
 
62
            self._connection = None
 
63
        self._opener = opener
 
64
 
 
65
    def _perform(self, request):
 
66
        """Send the request to the server and handles common errors.
 
67
        """
 
68
        # Give back connection if we have one (see below)
 
69
        if self._connection is not None:
 
70
            request.connection = self._connection
 
71
 
 
72
        mutter('%s: [%s]' % (request.method, request.get_full_url()))
 
73
        if self._debuglevel > 0:
 
74
            print 'perform: %s base: %s, url: %s' % (request.method, self.base,
 
75
                                                     request.get_full_url())
 
76
 
 
77
        response = self._opener.open(request)
 
78
        if self._connection is None:
 
79
            # Acquire connection when the first request is able
 
80
            # to connect to the server
 
81
            self._connection = request.connection
 
82
 
 
83
        if request.redirected_to is not None:
 
84
            # TODO: Update the transport so that subsequent
 
85
            # requests goes directly to the right host
 
86
            mutter('redirected from: %s to: %s' % (request.get_full_url(),
 
87
                                                   request.redirected_to))
 
88
 
 
89
        return response
55
90
 
56
91
    def _get(self, relpath, ranges, tail_amount=0):
57
 
        path = relpath
58
 
        try:
59
 
            path = self._real_abspath(relpath)
60
 
            resp = self._get_url_impl(path, method='GET', ranges=ranges,
61
 
                                      tail_amount=tail_amount)
62
 
            return resp.code, response.handle_response(path,
63
 
                                resp.code, resp.headers, resp)
64
 
        except urllib2.HTTPError, e:
65
 
            mutter('url error code: %s for has url: %r', e.code, path)
66
 
            if e.code == 404:
67
 
                raise NoSuchFile(path, extra=e)
68
 
            raise
69
 
        except (BzrError, IOError), e:
70
 
            if getattr(e, 'errno', None) is not None:
71
 
                mutter('io error: %s %s for has url: %r',
72
 
                    e.errno, errno.errorcode.get(e.errno), path)
73
 
                if e.errno == errno.ENOENT:
74
 
                    raise NoSuchFile(path, extra=e)
75
 
            raise ConnectionError(msg = "Error retrieving %s: %s"
76
 
                                  % (self.abspath(relpath), str(e)),
77
 
                                  orig_error=e)
78
 
 
79
 
    def _get_url_impl(self, url, method, ranges, tail_amount=0):
80
 
        """Actually pass get request into urllib
81
 
 
82
 
        :returns: urllib Response object
83
 
        """
84
 
        manager = urllib2.HTTPPasswordMgrWithDefaultRealm()
85
 
        url = extract_auth(url, manager)
86
 
        auth_handler = urllib2.HTTPBasicAuthHandler(manager)
87
 
        opener = urllib2.build_opener(auth_handler)
88
 
        request = Request(url)
89
 
        request.method = method
90
 
        request.add_header('Pragma', 'no-cache')
91
 
        request.add_header('Cache-control', 'max-age=0')
92
 
        request.add_header('User-Agent',
93
 
                           'bzr/%s (urllib)' % (bzrlib.__version__,))
 
92
        """See HttpTransport._get"""
 
93
 
 
94
        abspath = self._real_abspath(relpath)
 
95
        headers = {}
94
96
        if ranges or tail_amount:
95
97
            bytes = 'bytes=' + self.range_header(ranges, tail_amount)
96
 
            request.add_header('Range', bytes)
97
 
        response = opener.open(request)
98
 
        return response
 
98
            headers = {'Range': bytes}
 
99
        
 
100
        request = Request('GET', abspath, None, headers)
 
101
        response = self._perform(request)
 
102
 
 
103
        code = response.code
 
104
        if code == 404: # not found
 
105
            # FIXME: Check that there is really no message to be read
 
106
            self._connection.fake_close()
 
107
            raise NoSuchFile(abspath)
 
108
 
 
109
        data = handle_response(abspath, code, response.headers, response)
 
110
        # Close response to free the httplib.HTTPConnection pipeline
 
111
        self._connection.fake_close()
 
112
        return code, data
99
113
 
100
114
    def should_cache(self):
101
115
        """Return True if the data pulled across should be cached locally.
102
116
        """
103
117
        return True
104
118
 
 
119
    def _head(self, relpath):
 
120
        """Request the HEAD of a file.
 
121
 
 
122
        Performs the request and leaves callers handle the results.
 
123
        """
 
124
        abspath = self._real_abspath(relpath)
 
125
        request = Request('HEAD', abspath)
 
126
        response = self._perform(request)
 
127
 
 
128
        self._connection.fake_close()
 
129
        return response
 
130
 
105
131
    def has(self, relpath):
106
132
        """Does the target location exist?
107
133
        """
108
 
        abspath = self._real_abspath(relpath)
109
 
        try:
110
 
            f = self._get_url_impl(abspath, 'HEAD', [])
111
 
            # Without the read and then close()
112
 
            # we tend to have busy sockets.
113
 
            f.read()
114
 
            f.close()
 
134
        response = self._head(relpath)
 
135
 
 
136
        code = response.code
 
137
        # FIXME: 302 MAY have been already processed by the
 
138
        # redirection handler
 
139
        if code in (200, 302): # "ok", "found"
115
140
            return True
116
 
        except urllib2.HTTPError, e:
117
 
            mutter('url error code: %s, for has url: %r', e.code, abspath)
118
 
            if e.code == 404:
119
 
                return False
120
 
            raise
121
 
        except urllib2.URLError, e:
122
 
            mutter('url error: %s, for has url: %r', e.reason, abspath)
123
 
            raise
124
 
        except IOError, e:
125
 
            mutter('io error: %s %s for has url: %r',
126
 
                e.errno, errno.errorcode.get(e.errno), abspath)
127
 
            if e.errno == errno.ENOENT:
128
 
                return False
129
 
            raise TransportError(orig_error=e)
130
 
 
131
 
    def copy_to(self, relpaths, other, mode=None, pb=None):
132
 
        """Copy a set of entries from self into another Transport.
133
 
 
134
 
        :param relpaths: A list/generator of entries to be copied.
135
 
 
136
 
        TODO: if other is LocalTransport, is it possible to
137
 
              do better than put(get())?
138
 
        """
139
 
        # At this point HttpTransport_urllib might be able to check and see if
140
 
        # the remote location is the same, and rather than download, and
141
 
        # then upload, it could just issue a remote copy_this command.
142
 
        if isinstance(other, HttpTransport_urllib):
143
 
            raise TransportNotPossible('http cannot be the target of copy_to()')
144
141
        else:
145
 
            return super(HttpTransport_urllib, self).copy_to(relpaths, other, mode=mode, pb=pb)
146
 
 
147
 
    def move(self, rel_from, rel_to):
148
 
        """Move the item at rel_from to the location at rel_to"""
149
 
        raise TransportNotPossible('http does not support move()')
150
 
 
151
 
    def delete(self, relpath):
152
 
        """Delete the item at relpath"""
153
 
        raise TransportNotPossible('http does not support delete()')
 
142
            assert(code == 404, 'Only 200, 404 or may be 302 are correct')
 
143
            return False
154
144
 
155
145
 
156
146
class HttpServer_urllib(HttpServer):