~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/transport/memory.py

  • Committer: Robert Collins
  • Date: 2009-12-16 22:29:31 UTC
  • mto: This revision was merged to the branch mainline in revision 4920.
  • Revision ID: robertc@robertcollins.net-20091216222931-wbbn5ey4mwmpatwd
Review feedback.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright (C) 2005, 2006 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
 
16
 
 
17
"""Implementation of Transport that uses memory for its storage.
 
18
 
 
19
The contents of the transport will be lost when the object is discarded,
 
20
so this is primarily useful for testing.
 
21
"""
 
22
 
 
23
import os
 
24
import errno
 
25
import re
 
26
from stat import S_IFREG, S_IFDIR
 
27
from cStringIO import StringIO
 
28
import warnings
 
29
 
 
30
from bzrlib.errors import (
 
31
    FileExists,
 
32
    LockError,
 
33
    InProcessTransport,
 
34
    NoSuchFile,
 
35
    TransportError,
 
36
    )
 
37
from bzrlib.trace import mutter
 
38
from bzrlib.transport import (
 
39
    AppendBasedFileStream,
 
40
    _file_streams,
 
41
    LateReadError,
 
42
    register_transport,
 
43
    Server,
 
44
    Transport,
 
45
    )
 
46
import bzrlib.urlutils as urlutils
 
47
 
 
48
 
 
49
 
 
50
class MemoryStat(object):
 
51
 
 
52
    def __init__(self, size, is_dir, perms):
 
53
        self.st_size = size
 
54
        if not is_dir:
 
55
            if perms is None:
 
56
                perms = 0644
 
57
            self.st_mode = S_IFREG | perms
 
58
        else:
 
59
            if perms is None:
 
60
                perms = 0755
 
61
            self.st_mode = S_IFDIR | perms
 
62
 
 
63
 
 
64
class MemoryTransport(Transport):
 
65
    """This is an in memory file system for transient data storage."""
 
66
 
 
67
    def __init__(self, url=""):
 
68
        """Set the 'base' path where files will be stored."""
 
69
        if url == "":
 
70
            url = "memory:///"
 
71
        if url[-1] != '/':
 
72
            url = url + '/'
 
73
        super(MemoryTransport, self).__init__(url)
 
74
        split = url.find(':') + 3
 
75
        self._scheme = url[:split]
 
76
        self._cwd = url[split:]
 
77
        # dictionaries from absolute path to file mode
 
78
        self._dirs = {'/':None}
 
79
        self._files = {}
 
80
        self._locks = {}
 
81
 
 
82
    def clone(self, offset=None):
 
83
        """See Transport.clone()."""
 
84
        path = self._combine_paths(self._cwd, offset)
 
85
        if len(path) == 0 or path[-1] != '/':
 
86
            path += '/'
 
87
        url = self._scheme + path
 
88
        result = self.__class__(url)
 
89
        result._dirs = self._dirs
 
90
        result._files = self._files
 
91
        result._locks = self._locks
 
92
        return result
 
93
 
 
94
    def abspath(self, relpath):
 
95
        """See Transport.abspath()."""
 
96
        # while a little slow, this is sufficiently fast to not matter in our
 
97
        # current environment - XXX RBC 20060404 move the clone '..' handling
 
98
        # into here and call abspath from clone
 
99
        temp_t = self.clone(relpath)
 
100
        if temp_t.base.count('/') == 3:
 
101
            return temp_t.base
 
102
        else:
 
103
            return temp_t.base[:-1]
 
104
 
 
105
    def append_file(self, relpath, f, mode=None):
 
106
        """See Transport.append_file()."""
 
107
        _abspath = self._abspath(relpath)
 
108
        self._check_parent(_abspath)
 
109
        orig_content, orig_mode = self._files.get(_abspath, ("", None))
 
110
        if mode is None:
 
111
            mode = orig_mode
 
112
        self._files[_abspath] = (orig_content + f.read(), mode)
 
113
        return len(orig_content)
 
114
 
 
115
    def _check_parent(self, _abspath):
 
116
        dir = os.path.dirname(_abspath)
 
117
        if dir != '/':
 
118
            if not dir in self._dirs:
 
119
                raise NoSuchFile(_abspath)
 
120
 
 
121
    def has(self, relpath):
 
122
        """See Transport.has()."""
 
123
        _abspath = self._abspath(relpath)
 
124
        return (_abspath in self._files) or (_abspath in self._dirs)
 
125
 
 
126
    def delete(self, relpath):
 
127
        """See Transport.delete()."""
 
128
        _abspath = self._abspath(relpath)
 
129
        if not _abspath in self._files:
 
130
            raise NoSuchFile(relpath)
 
131
        del self._files[_abspath]
 
132
 
 
133
    def external_url(self):
 
134
        """See bzrlib.transport.Transport.external_url."""
 
135
        # MemoryTransport's are only accessible in-process
 
136
        # so we raise here
 
137
        raise InProcessTransport(self)
 
138
 
 
139
    def get(self, relpath):
 
140
        """See Transport.get()."""
 
141
        _abspath = self._abspath(relpath)
 
142
        if not _abspath in self._files:
 
143
            if _abspath in self._dirs:
 
144
                return LateReadError(relpath)
 
145
            else:
 
146
                raise NoSuchFile(relpath)
 
147
        return StringIO(self._files[_abspath][0])
 
148
 
 
149
    def put_file(self, relpath, f, mode=None):
 
150
        """See Transport.put_file()."""
 
151
        _abspath = self._abspath(relpath)
 
152
        self._check_parent(_abspath)
 
153
        bytes = f.read()
 
154
        if type(bytes) is not str:
 
155
            # Although not strictly correct, we raise UnicodeEncodeError to be
 
156
            # compatible with other transports.
 
157
            raise UnicodeEncodeError(
 
158
                'undefined', bytes, 0, 1,
 
159
                'put_file must be given a file of bytes, not unicode.')
 
160
        self._files[_abspath] = (bytes, mode)
 
161
        return len(bytes)
 
162
 
 
163
    def mkdir(self, relpath, mode=None):
 
164
        """See Transport.mkdir()."""
 
165
        _abspath = self._abspath(relpath)
 
166
        self._check_parent(_abspath)
 
167
        if _abspath in self._dirs:
 
168
            raise FileExists(relpath)
 
169
        self._dirs[_abspath]=mode
 
170
 
 
171
    def open_write_stream(self, relpath, mode=None):
 
172
        """See Transport.open_write_stream."""
 
173
        self.put_bytes(relpath, "", mode)
 
174
        result = AppendBasedFileStream(self, relpath)
 
175
        _file_streams[self.abspath(relpath)] = result
 
176
        return result
 
177
 
 
178
    def listable(self):
 
179
        """See Transport.listable."""
 
180
        return True
 
181
 
 
182
    def iter_files_recursive(self):
 
183
        for file in self._files:
 
184
            if file.startswith(self._cwd):
 
185
                yield urlutils.escape(file[len(self._cwd):])
 
186
 
 
187
    def list_dir(self, relpath):
 
188
        """See Transport.list_dir()."""
 
189
        _abspath = self._abspath(relpath)
 
190
        if _abspath != '/' and _abspath not in self._dirs:
 
191
            raise NoSuchFile(relpath)
 
192
        result = []
 
193
 
 
194
        if not _abspath.endswith('/'):
 
195
            _abspath += '/'
 
196
 
 
197
        for path_group in self._files, self._dirs:
 
198
            for path in path_group:
 
199
                if path.startswith(_abspath):
 
200
                    trailing = path[len(_abspath):]
 
201
                    if trailing and '/' not in trailing:
 
202
                        result.append(trailing)
 
203
        return map(urlutils.escape, result)
 
204
 
 
205
    def rename(self, rel_from, rel_to):
 
206
        """Rename a file or directory; fail if the destination exists"""
 
207
        abs_from = self._abspath(rel_from)
 
208
        abs_to = self._abspath(rel_to)
 
209
        def replace(x):
 
210
            if x == abs_from:
 
211
                x = abs_to
 
212
            elif x.startswith(abs_from + '/'):
 
213
                x = abs_to + x[len(abs_from):]
 
214
            return x
 
215
        def do_renames(container):
 
216
            for path in container:
 
217
                new_path = replace(path)
 
218
                if new_path != path:
 
219
                    if new_path in container:
 
220
                        raise FileExists(new_path)
 
221
                    container[new_path] = container[path]
 
222
                    del container[path]
 
223
        do_renames(self._files)
 
224
        do_renames(self._dirs)
 
225
 
 
226
    def rmdir(self, relpath):
 
227
        """See Transport.rmdir."""
 
228
        _abspath = self._abspath(relpath)
 
229
        if _abspath in self._files:
 
230
            self._translate_error(IOError(errno.ENOTDIR, relpath), relpath)
 
231
        for path in self._files:
 
232
            if path.startswith(_abspath + '/'):
 
233
                self._translate_error(IOError(errno.ENOTEMPTY, relpath),
 
234
                                      relpath)
 
235
        for path in self._dirs:
 
236
            if path.startswith(_abspath + '/') and path != _abspath:
 
237
                self._translate_error(IOError(errno.ENOTEMPTY, relpath), relpath)
 
238
        if not _abspath in self._dirs:
 
239
            raise NoSuchFile(relpath)
 
240
        del self._dirs[_abspath]
 
241
 
 
242
    def stat(self, relpath):
 
243
        """See Transport.stat()."""
 
244
        _abspath = self._abspath(relpath)
 
245
        if _abspath in self._files:
 
246
            return MemoryStat(len(self._files[_abspath][0]), False,
 
247
                              self._files[_abspath][1])
 
248
        elif _abspath in self._dirs:
 
249
            return MemoryStat(0, True, self._dirs[_abspath])
 
250
        else:
 
251
            raise NoSuchFile(_abspath)
 
252
 
 
253
    def lock_read(self, relpath):
 
254
        """See Transport.lock_read()."""
 
255
        return _MemoryLock(self._abspath(relpath), self)
 
256
 
 
257
    def lock_write(self, relpath):
 
258
        """See Transport.lock_write()."""
 
259
        return _MemoryLock(self._abspath(relpath), self)
 
260
 
 
261
    def _abspath(self, relpath):
 
262
        """Generate an internal absolute path."""
 
263
        relpath = urlutils.unescape(relpath)
 
264
        if relpath[:1] == '/':
 
265
            return relpath
 
266
        cwd_parts = self._cwd.split('/')
 
267
        rel_parts = relpath.split('/')
 
268
        r = []
 
269
        for i in cwd_parts + rel_parts:
 
270
            if i == '..':
 
271
                if not r:
 
272
                    raise ValueError("illegal relpath %r under %r"
 
273
                        % (relpath, self._cwd))
 
274
                r = r[:-1]
 
275
            elif i == '.' or i == '':
 
276
                pass
 
277
            else:
 
278
                r.append(i)
 
279
        return '/' + '/'.join(r)
 
280
 
 
281
 
 
282
class _MemoryLock(object):
 
283
    """This makes a lock."""
 
284
 
 
285
    def __init__(self, path, transport):
 
286
        self.path = path
 
287
        self.transport = transport
 
288
        if self.path in self.transport._locks:
 
289
            raise LockError('File %r already locked' % (self.path,))
 
290
        self.transport._locks[self.path] = self
 
291
 
 
292
    def __del__(self):
 
293
        # Should this warn, or actually try to cleanup?
 
294
        if self.transport:
 
295
            warnings.warn("MemoryLock %r not explicitly unlocked" % (self.path,))
 
296
            self.unlock()
 
297
 
 
298
    def unlock(self):
 
299
        del self.transport._locks[self.path]
 
300
        self.transport = None
 
301
 
 
302
 
 
303
class MemoryServer(Server):
 
304
    """Server for the MemoryTransport for testing with."""
 
305
 
 
306
    def setUp(self):
 
307
        """See bzrlib.transport.Server.setUp."""
 
308
        self._dirs = {'/':None}
 
309
        self._files = {}
 
310
        self._locks = {}
 
311
        self._scheme = "memory+%s:///" % id(self)
 
312
        def memory_factory(url):
 
313
            result = MemoryTransport(url)
 
314
            result._dirs = self._dirs
 
315
            result._files = self._files
 
316
            result._locks = self._locks
 
317
            return result
 
318
        register_transport(self._scheme, memory_factory)
 
319
 
 
320
    def tearDown(self):
 
321
        """See bzrlib.transport.Server.tearDown."""
 
322
        # unregister this server
 
323
 
 
324
    def get_url(self):
 
325
        """See bzrlib.transport.Server.get_url."""
 
326
        return self._scheme
 
327
 
 
328
 
 
329
def get_test_permutations():
 
330
    """Return the permutations to be used in testing."""
 
331
    return [(MemoryTransport, MemoryServer),
 
332
            ]