~ubuntu-branches/ubuntu/hardy/bzr/hardy-updates

« back to all changes in this revision

Viewing changes to bzrlib/transport/__init__.py

  • Committer: Bazaar Package Importer
  • Author(s): Jeff Bailey
  • Date: 2005-11-07 13:17:53 UTC
  • Revision ID: james.westby@ubuntu.com-20051107131753-qsy145z1rfug5i27
Tags: upstream-0.6.2
ImportĀ upstreamĀ versionĀ 0.6.2

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
"""Transport is an abstraction layer to handle file access.
 
17
 
 
18
The abstraction is to allow access from the local filesystem, as well
 
19
as remote (such as http or sftp).
 
20
"""
 
21
 
 
22
from bzrlib.trace import mutter
 
23
from bzrlib.errors import (BzrError, 
 
24
    TransportError, TransportNotPossible, NonRelativePath,
 
25
    NoSuchFile, FileExists, PermissionDenied,
 
26
    ConnectionReset)
 
27
 
 
28
_protocol_handlers = {
 
29
}
 
30
 
 
31
def register_transport(prefix, klass, override=True):
 
32
    global _protocol_handlers
 
33
    # trace messages commented out because they're typically 
 
34
    # run during import before trace is set up
 
35
    if _protocol_handlers.has_key(prefix):
 
36
        if override:
 
37
            ## mutter('overriding transport: %s => %s' % (prefix, klass.__name__))
 
38
            _protocol_handlers[prefix] = klass
 
39
    else:
 
40
        ## mutter('registering transport: %s => %s' % (prefix, klass.__name__))
 
41
        _protocol_handlers[prefix] = klass
 
42
 
 
43
 
 
44
class Transport(object):
 
45
    """This class encapsulates methods for retrieving or putting a file
 
46
    from/to a storage location.
 
47
 
 
48
    Most functions have a _multi variant, which allows you to queue up
 
49
    multiple requests. They generally have a dumb base implementation 
 
50
    which just iterates over the arguments, but smart Transport
 
51
    implementations can do pipelining.
 
52
    In general implementations should support having a generator or a list
 
53
    as an argument (ie always iterate, never index)
 
54
    """
 
55
 
 
56
    def __init__(self, base):
 
57
        super(Transport, self).__init__()
 
58
        self.base = base
 
59
 
 
60
    def clone(self, offset=None):
 
61
        """Return a new Transport object, cloned from the current location,
 
62
        using a subdirectory or parent directory. This allows connections 
 
63
        to be pooled, rather than a new one needed for each subdir.
 
64
        """
 
65
        raise NotImplementedError
 
66
 
 
67
    def should_cache(self):
 
68
        """Return True if the data pulled across should be cached locally.
 
69
        """
 
70
        return False
 
71
 
 
72
    def _pump(self, from_file, to_file):
 
73
        """Most children will need to copy from one file-like 
 
74
        object or string to another one.
 
75
        This just gives them something easy to call.
 
76
        """
 
77
        if isinstance(from_file, basestring):
 
78
            to_file.write(from_file)
 
79
        else:
 
80
            from bzrlib.osutils import pumpfile
 
81
            pumpfile(from_file, to_file)
 
82
 
 
83
    def _get_total(self, multi):
 
84
        """Try to figure out how many entries are in multi,
 
85
        but if not possible, return None.
 
86
        """
 
87
        try:
 
88
            return len(multi)
 
89
        except TypeError: # We can't tell how many, because relpaths is a generator
 
90
            return None
 
91
 
 
92
    def _update_pb(self, pb, msg, count, total):
 
93
        """Update the progress bar based on the current count
 
94
        and total available, total may be None if it was
 
95
        not possible to determine.
 
96
        """
 
97
        if pb is None:
 
98
            return
 
99
        if total is None:
 
100
            pb.update(msg, count, count+1)
 
101
        else:
 
102
            pb.update(msg, count, total)
 
103
 
 
104
    def _iterate_over(self, multi, func, pb, msg, expand=True):
 
105
        """Iterate over all entries in multi, passing them to func,
 
106
        and update the progress bar as you go along.
 
107
 
 
108
        :param expand:  If True, the entries will be passed to the function
 
109
                        by expanding the tuple. If False, it will be passed
 
110
                        as a single parameter.
 
111
        """
 
112
        total = self._get_total(multi)
 
113
        count = 0
 
114
        for entry in multi:
 
115
            self._update_pb(pb, msg, count, total)
 
116
            if expand:
 
117
                func(*entry)
 
118
            else:
 
119
                func(entry)
 
120
            count += 1
 
121
        return count
 
122
 
 
123
    def abspath(self, relpath):
 
124
        """Return the full url to the given relative path.
 
125
        This can be supplied with a string or a list
 
126
 
 
127
        XXX: Robert Collins 20051016 - is this really needed in the public
 
128
             interface ?
 
129
        """
 
130
        raise NotImplementedError
 
131
 
 
132
    def relpath(self, abspath):
 
133
        """Return the local path portion from a given absolute path.
 
134
 
 
135
        This default implementation is not suitable for filesystems with
 
136
        aliasing, such as that given by symlinks, where a path may not 
 
137
        start with our base, but still be a relpath once aliasing is 
 
138
        resolved.
 
139
        """
 
140
        if not abspath.startswith(self.base):
 
141
            raise NonRelativePath('path %r is not under base URL %r'
 
142
                           % (abspath, self.base))
 
143
        pl = len(self.base)
 
144
        return abspath[pl:].lstrip('/')
 
145
 
 
146
 
 
147
    def has(self, relpath):
 
148
        """Does the file relpath exist?
 
149
        
 
150
        Note that some transports MAY allow querying on directories, but this
 
151
        is not part of the protocol.
 
152
        """
 
153
        raise NotImplementedError
 
154
 
 
155
    def has_multi(self, relpaths, pb=None):
 
156
        """Return True/False for each entry in relpaths"""
 
157
        total = self._get_total(relpaths)
 
158
        count = 0
 
159
        for relpath in relpaths:
 
160
            self._update_pb(pb, 'has', count, total)
 
161
            yield self.has(relpath)
 
162
            count += 1
 
163
 
 
164
    def iter_files_recursive(self):
 
165
        """Iter the relative paths of files in the transports sub-tree.
 
166
        
 
167
        As with other listing functions, only some transports implement this,.
 
168
        you may check via is_listable to determine if it will.
 
169
        """
 
170
        raise NotImplementedError
 
171
 
 
172
    def get(self, relpath):
 
173
        """Get the file at the given relative path.
 
174
 
 
175
        :param relpath: The relative path to the file
 
176
        """
 
177
        raise NotImplementedError
 
178
 
 
179
    def get_multi(self, relpaths, pb=None):
 
180
        """Get a list of file-like objects, one for each entry in relpaths.
 
181
 
 
182
        :param relpaths: A list of relative paths.
 
183
        :param pb:  An optional ProgressBar for indicating percent done.
 
184
        :return: A list or generator of file-like objects
 
185
        """
 
186
        # TODO: Consider having this actually buffer the requests,
 
187
        # in the default mode, it probably won't give worse performance,
 
188
        # and all children wouldn't have to implement buffering
 
189
        total = self._get_total(relpaths)
 
190
        count = 0
 
191
        for relpath in relpaths:
 
192
            self._update_pb(pb, 'get', count, total)
 
193
            yield self.get(relpath)
 
194
            count += 1
 
195
 
 
196
    def put(self, relpath, f):
 
197
        """Copy the file-like or string object into the location.
 
198
 
 
199
        :param relpath: Location to put the contents, relative to base.
 
200
        :param f:       File-like or string object.
 
201
        """
 
202
        raise NotImplementedError
 
203
 
 
204
    def put_multi(self, files, pb=None):
 
205
        """Put a set of files or strings into the location.
 
206
 
 
207
        :param files: A list of tuples of relpath, file object [(path1, file1), (path2, file2),...]
 
208
        :param pb:  An optional ProgressBar for indicating percent done.
 
209
        :return: The number of files copied.
 
210
        """
 
211
        return self._iterate_over(files, self.put, pb, 'put', expand=True)
 
212
 
 
213
    def mkdir(self, relpath):
 
214
        """Create a directory at the given path."""
 
215
        raise NotImplementedError
 
216
 
 
217
    def mkdir_multi(self, relpaths, pb=None):
 
218
        """Create a group of directories"""
 
219
        return self._iterate_over(relpaths, self.mkdir, pb, 'mkdir', expand=False)
 
220
 
 
221
    def append(self, relpath, f):
 
222
        """Append the text in the file-like or string object to 
 
223
        the supplied location.
 
224
        """
 
225
        raise NotImplementedError
 
226
 
 
227
    def append_multi(self, files, pb=None):
 
228
        """Append the text in each file-like or string object to
 
229
        the supplied location.
 
230
 
 
231
        :param files: A set of (path, f) entries
 
232
        :param pb:  An optional ProgressBar for indicating percent done.
 
233
        """
 
234
        return self._iterate_over(files, self.append, pb, 'append', expand=True)
 
235
 
 
236
    def copy(self, rel_from, rel_to):
 
237
        """Copy the item at rel_from to the location at rel_to"""
 
238
        raise NotImplementedError
 
239
 
 
240
    def copy_multi(self, relpaths, pb=None):
 
241
        """Copy a bunch of entries.
 
242
        
 
243
        :param relpaths: A list of tuples of the form [(from, to), (from, to),...]
 
244
        """
 
245
        # This is the non-pipelined implementation, so that
 
246
        # implementors don't have to implement everything.
 
247
        return self._iterate_over(relpaths, self.copy, pb, 'copy', expand=True)
 
248
 
 
249
    def copy_to(self, relpaths, other, pb=None):
 
250
        """Copy a set of entries from self into another Transport.
 
251
 
 
252
        :param relpaths: A list/generator of entries to be copied.
 
253
        """
 
254
        # The dummy implementation just does a simple get + put
 
255
        def copy_entry(path):
 
256
            other.put(path, self.get(path))
 
257
 
 
258
        return self._iterate_over(relpaths, copy_entry, pb, 'copy_to', expand=False)
 
259
 
 
260
 
 
261
    def move(self, rel_from, rel_to):
 
262
        """Move the item at rel_from to the location at rel_to"""
 
263
        raise NotImplementedError
 
264
 
 
265
    def move_multi(self, relpaths, pb=None):
 
266
        """Move a bunch of entries.
 
267
        
 
268
        :param relpaths: A list of tuples of the form [(from1, to1), (from2, to2),...]
 
269
        """
 
270
        return self._iterate_over(relpaths, self.move, pb, 'move', expand=True)
 
271
 
 
272
    def move_multi_to(self, relpaths, rel_to):
 
273
        """Move a bunch of entries to a single location.
 
274
        This differs from move_multi in that you give a list of from, and
 
275
        a single destination, rather than multiple destinations.
 
276
 
 
277
        :param relpaths: A list of relative paths [from1, from2, from3, ...]
 
278
        :param rel_to: A directory where each entry should be placed.
 
279
        """
 
280
        # This is not implemented, because you need to do special tricks to
 
281
        # extract the basename, and add it to rel_to
 
282
        raise NotImplementedError
 
283
 
 
284
    def delete(self, relpath):
 
285
        """Delete the item at relpath"""
 
286
        raise NotImplementedError
 
287
 
 
288
    def delete_multi(self, relpaths, pb=None):
 
289
        """Queue up a bunch of deletes to be done.
 
290
        """
 
291
        return self._iterate_over(relpaths, self.delete, pb, 'delete', expand=False)
 
292
 
 
293
    def stat(self, relpath):
 
294
        """Return the stat information for a file.
 
295
        WARNING: This may not be implementable for all protocols, so use
 
296
        sparingly.
 
297
        NOTE: This returns an object with fields such as 'st_size'. It MAY
 
298
        or MAY NOT return the literal result of an os.stat() call, so all
 
299
        access should be via named fields.
 
300
        ALSO NOTE: Stats of directories may not be supported on some 
 
301
        transports.
 
302
        """
 
303
        raise NotImplementedError
 
304
 
 
305
    def stat_multi(self, relpaths, pb=None):
 
306
        """Stat multiple files and return the information.
 
307
        """
 
308
        #TODO:  Is it worth making this a generator instead of a
 
309
        #       returning a list?
 
310
        stats = []
 
311
        def gather(path):
 
312
            stats.append(self.stat(path))
 
313
 
 
314
        count = self._iterate_over(relpaths, gather, pb, 'stat', expand=False)
 
315
        return stats
 
316
 
 
317
    def listable(self):
 
318
        """Return True if this store supports listing."""
 
319
        raise NotImplementedError
 
320
 
 
321
    def list_dir(self, relpath):
 
322
        """Return a list of all files at the given location.
 
323
        WARNING: many transports do not support this, so trying avoid using
 
324
        it if at all possible.
 
325
        """
 
326
        raise TransportNotPossible("This transport has not "
 
327
                                   "implemented list_dir.")
 
328
 
 
329
    def lock_read(self, relpath):
 
330
        """Lock the given file for shared (read) access.
 
331
        WARNING: many transports do not support this, so trying avoid using it
 
332
 
 
333
        :return: A lock object, which should contain an unlock() function.
 
334
        """
 
335
        raise NotImplementedError
 
336
 
 
337
    def lock_write(self, relpath):
 
338
        """Lock the given file for exclusive (write) access.
 
339
        WARNING: many transports do not support this, so trying avoid using it
 
340
 
 
341
        :return: A lock object, which should contain an unlock() function.
 
342
        """
 
343
        raise NotImplementedError
 
344
 
 
345
 
 
346
def get_transport(base):
 
347
    global _protocol_handlers
 
348
    if base is None:
 
349
        base = '.'
 
350
    for proto, klass in _protocol_handlers.iteritems():
 
351
        if proto is not None and base.startswith(proto):
 
352
            return klass(base)
 
353
    # The default handler is the filesystem handler
 
354
    # which has a lookup of None
 
355
    return _protocol_handlers[None](base)
 
356
 
 
357
 
 
358
def register_lazy_transport(scheme, module, classname):
 
359
    """Register lazy-loaded transport class.
 
360
 
 
361
    When opening a URL with the given scheme, load the module and then
 
362
    instantiate the particular class.  
 
363
    """
 
364
    def _loader(base):
 
365
        mod = __import__(module, globals(), locals(), [classname])
 
366
        klass = getattr(mod, classname)
 
367
        return klass(base)
 
368
    register_transport(scheme, _loader)
 
369
 
 
370
 
 
371
def urlescape(relpath):
 
372
    """Escape relpath to be a valid url."""
 
373
    # TODO utf8 it first. utf8relpath = relpath.encode('utf8')
 
374
    import urllib
 
375
    return urllib.quote(relpath)
 
376
 
 
377
 
 
378
# None is the default transport, for things with no url scheme
 
379
register_lazy_transport(None, 'bzrlib.transport.local', 'LocalTransport')
 
380
register_lazy_transport('file://', 'bzrlib.transport.local', 'LocalTransport')
 
381
register_lazy_transport('sftp://', 'bzrlib.transport.sftp', 'SFTPTransport')
 
382
register_lazy_transport('http://', 'bzrlib.transport.http', 'HttpTransport')
 
383
register_lazy_transport('https://', 'bzrlib.transport.http', 'HttpTransport')