~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/transport/http.py

Merge in format-5 work - release bzr 0.1rc1.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright (C) 2005 Canonical Ltd
 
2
 
 
3
# This program is free software; you can redistribute it and/or modify
 
4
# it under the terms of the GNU General Public License as published by
 
5
# the Free Software Foundation; either version 2 of the License, or
 
6
# (at your option) any later version.
 
7
 
 
8
# This program is distributed in the hope that it will be useful,
 
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
11
# GNU General Public License for more details.
 
12
 
 
13
# You should have received a copy of the GNU General Public License
 
14
# along with this program; if not, write to the Free Software
 
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 
16
"""Implementation of Transport over http.
 
17
"""
 
18
 
 
19
from bzrlib.transport import Transport, register_transport
 
20
from bzrlib.errors import (TransportNotPossible, NoSuchFile, 
 
21
                           NonRelativePath, TransportError)
 
22
import os, errno
 
23
from cStringIO import StringIO
 
24
import urllib2
 
25
import urlparse
 
26
 
 
27
from bzrlib.errors import BzrError, BzrCheckError
 
28
from bzrlib.branch import Branch
 
29
from bzrlib.trace import mutter
 
30
 
 
31
# velocitynet.com.au transparently proxies connections and thereby
 
32
# breaks keep-alive -- sucks!
 
33
 
 
34
 
 
35
def get_url(url):
 
36
    import urllib2
 
37
    mutter("get_url %s" % url)
 
38
    url_f = urllib2.urlopen(url)
 
39
    return url_f
 
40
 
 
41
class HttpTransportError(TransportError):
 
42
    pass
 
43
 
 
44
class HttpTransport(Transport):
 
45
    """This is the transport agent for http:// access.
 
46
    
 
47
    TODO: Implement pipelined versions of all of the *_multi() functions.
 
48
    """
 
49
 
 
50
    def __init__(self, base):
 
51
        """Set the base path where files will be stored."""
 
52
        assert base.startswith('http://') or base.startswith('https://')
 
53
        super(HttpTransport, self).__init__(base)
 
54
        # In the future we might actually connect to the remote host
 
55
        # rather than using get_url
 
56
        # self._connection = None
 
57
        (self._proto, self._host,
 
58
            self._path, self._parameters,
 
59
            self._query, self._fragment) = urlparse.urlparse(self.base)
 
60
 
 
61
    def should_cache(self):
 
62
        """Return True if the data pulled across should be cached locally.
 
63
        """
 
64
        return True
 
65
 
 
66
    def clone(self, offset=None):
 
67
        """Return a new HttpTransport with root at self.base + offset
 
68
        For now HttpTransport does not actually connect, so just return
 
69
        a new HttpTransport object.
 
70
        """
 
71
        if offset is None:
 
72
            return HttpTransport(self.base)
 
73
        else:
 
74
            return HttpTransport(self.abspath(offset))
 
75
 
 
76
    def abspath(self, relpath):
 
77
        """Return the full url to the given relative path.
 
78
        This can be supplied with a string or a list
 
79
        """
 
80
        if isinstance(relpath, basestring):
 
81
            relpath = [relpath]
 
82
        basepath = self._path.split('/')
 
83
        if len(basepath) > 0 and basepath[-1] == '':
 
84
            basepath = basepath[:-1]
 
85
 
 
86
        for p in relpath:
 
87
            if p == '..':
 
88
                if len(basepath) < 0:
 
89
                    # In most filesystems, a request for the parent
 
90
                    # of root, just returns root.
 
91
                    continue
 
92
                basepath.pop()
 
93
            elif p == '.':
 
94
                continue # No-op
 
95
            else:
 
96
                basepath.append(p)
 
97
 
 
98
        # Possibly, we could use urlparse.urljoin() here, but
 
99
        # I'm concerned about when it chooses to strip the last
 
100
        # portion of the path, and when it doesn't.
 
101
        path = '/'.join(basepath)
 
102
        return urlparse.urlunparse((self._proto,
 
103
                self._host, path, '', '', ''))
 
104
 
 
105
    def relpath(self, abspath):
 
106
        if not abspath.startswith(self.base):
 
107
            raise NonRelativePath('path %r is not under base URL %r'
 
108
                           % (abspath, self.base))
 
109
        pl = len(self.base)
 
110
        return abspath[pl:].lstrip('/')
 
111
 
 
112
    def has(self, relpath):
 
113
        """Does the target location exist?
 
114
 
 
115
        TODO: HttpTransport.has() should use a HEAD request,
 
116
        not a full GET request.
 
117
 
 
118
        TODO: This should be changed so that we don't use
 
119
        urllib2 and get an exception, the code path would be
 
120
        cleaner if we just do an http HEAD request, and parse
 
121
        the return code.
 
122
        """
 
123
        try:
 
124
            f = get_url(self.abspath(relpath))
 
125
            # Without the read and then close()
 
126
            # we tend to have busy sockets.
 
127
            f.read()
 
128
            f.close()
 
129
            return True
 
130
        except BzrError:
 
131
            return False
 
132
        except urllib2.URLError:
 
133
            return False
 
134
        except IOError, e:
 
135
            if e.errno == errno.ENOENT:
 
136
                return False
 
137
            raise HttpTransportError(orig_error=e)
 
138
 
 
139
    def get(self, relpath, decode=False):
 
140
        """Get the file at the given relative path.
 
141
 
 
142
        :param relpath: The relative path to the file
 
143
        """
 
144
        try:
 
145
            return get_url(self.abspath(relpath))
 
146
        except (BzrError, urllib2.URLError, IOError), e:
 
147
            raise NoSuchFile(orig_error=e)
 
148
        except Exception,e:
 
149
            raise HttpTransportError(orig_error=e)
 
150
 
 
151
    def get_partial(self, relpath, start, length=None):
 
152
        """Get just part of a file.
 
153
 
 
154
        :param relpath: Path to the file, relative to base
 
155
        :param start: The starting position to read from
 
156
        :param length: The length to read. A length of None indicates
 
157
                       read to the end of the file.
 
158
        :return: A file-like object containing at least the specified bytes.
 
159
                 Some implementations may return objects which can be read
 
160
                 past this length, but this is not guaranteed.
 
161
        """
 
162
        # TODO: You can make specialized http requests for just
 
163
        # a portion of the file. Figure out how to do that.
 
164
        # For now, urllib2 returns files that cannot seek() so
 
165
        # we just read bytes off the beginning, until we
 
166
        # get to the point that we care about.
 
167
        f = self.get(relpath)
 
168
        # TODO: read in smaller chunks, in case things are
 
169
        # buffered internally.
 
170
        f.read(start)
 
171
        return f
 
172
 
 
173
    def put(self, relpath, f):
 
174
        """Copy the file-like or string object into the location.
 
175
 
 
176
        :param relpath: Location to put the contents, relative to base.
 
177
        :param f:       File-like or string object.
 
178
        """
 
179
        raise TransportNotPossible('http PUT not supported')
 
180
 
 
181
    def mkdir(self, relpath):
 
182
        """Create a directory at the given path."""
 
183
        raise TransportNotPossible('http does not support mkdir()')
 
184
 
 
185
    def append(self, relpath, f):
 
186
        """Append the text in the file-like object into the final
 
187
        location.
 
188
        """
 
189
        raise TransportNotPossible('http does not support append()')
 
190
 
 
191
    def copy(self, rel_from, rel_to):
 
192
        """Copy the item at rel_from to the location at rel_to"""
 
193
        raise TransportNotPossible('http does not support copy()')
 
194
 
 
195
    def copy_to(self, relpaths, other, pb=None):
 
196
        """Copy a set of entries from self into another Transport.
 
197
 
 
198
        :param relpaths: A list/generator of entries to be copied.
 
199
 
 
200
        TODO: if other is LocalTransport, is it possible to
 
201
              do better than put(get())?
 
202
        """
 
203
        # At this point HttpTransport might be able to check and see if
 
204
        # the remote location is the same, and rather than download, and
 
205
        # then upload, it could just issue a remote copy_this command.
 
206
        if isinstance(other, HttpTransport):
 
207
            raise TransportNotPossible('http cannot be the target of copy_to()')
 
208
        else:
 
209
            return super(HttpTransport, self).copy_to(relpaths, other, pb=pb)
 
210
 
 
211
    def move(self, rel_from, rel_to):
 
212
        """Move the item at rel_from to the location at rel_to"""
 
213
        raise TransportNotPossible('http does not support move()')
 
214
 
 
215
    def delete(self, relpath):
 
216
        """Delete the item at relpath"""
 
217
        raise TransportNotPossible('http does not support delete()')
 
218
 
 
219
    def listable(self):
 
220
        """See Transport.listable."""
 
221
        return False
 
222
 
 
223
    def stat(self, relpath):
 
224
        """Return the stat information for a file.
 
225
        """
 
226
        raise TransportNotPossible('http does not support stat()')
 
227
 
 
228
    def lock_read(self, relpath):
 
229
        """Lock the given file for shared (read) access.
 
230
        :return: A lock object, which should be passed to Transport.unlock()
 
231
        """
 
232
        # The old RemoteBranch ignore lock for reading, so we will
 
233
        # continue that tradition and return a bogus lock object.
 
234
        class BogusLock(object):
 
235
            def __init__(self, path):
 
236
                self.path = path
 
237
            def unlock(self):
 
238
                pass
 
239
        return BogusLock(relpath)
 
240
 
 
241
    def lock_write(self, relpath):
 
242
        """Lock the given file for exclusive (write) access.
 
243
        WARNING: many transports do not support this, so trying avoid using it
 
244
 
 
245
        :return: A lock object, which should be passed to Transport.unlock()
 
246
        """
 
247
        raise TransportNotPossible('http does not support lock_write()')
 
248
 
 
249
register_transport('http://', HttpTransport)
 
250
register_transport('https://', HttpTransport)