1
# Copyright (C) 2005, 2006 Canonical Ltd
 
 
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.
 
 
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.
 
 
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
 
 
17
"""Implementation of Transport that uses memory for its storage.
 
 
19
The contents of the transport will be lost when the object is discarded,
 
 
20
so this is primarily useful for testing.
 
 
26
from stat import S_IFREG, S_IFDIR
 
 
27
from cStringIO import StringIO
 
 
30
from bzrlib.errors import (
 
 
37
from bzrlib.trace import mutter
 
 
38
from bzrlib.transport import (
 
 
39
    AppendBasedFileStream,
 
 
46
import bzrlib.urlutils as urlutils
 
 
50
class MemoryStat(object):
 
 
52
    def __init__(self, size, is_dir, perms):
 
 
57
            self.st_mode = S_IFREG | perms
 
 
61
            self.st_mode = S_IFDIR | perms
 
 
64
class MemoryTransport(Transport):
 
 
65
    """This is an in memory file system for transient data storage."""
 
 
67
    def __init__(self, url=""):
 
 
68
        """Set the 'base' path where files will be stored."""
 
 
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}
 
 
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] != '/':
 
 
87
        url = self._scheme + path
 
 
88
        result = MemoryTransport(url)
 
 
89
        result._dirs = self._dirs
 
 
90
        result._files = self._files
 
 
91
        result._locks = self._locks
 
 
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:
 
 
103
            return temp_t.base[:-1]
 
 
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))
 
 
112
        self._files[_abspath] = (orig_content + f.read(), mode)
 
 
113
        return len(orig_content)
 
 
115
    def _check_parent(self, _abspath):
 
 
116
        dir = os.path.dirname(_abspath)
 
 
118
            if not dir in self._dirs:
 
 
119
                raise NoSuchFile(_abspath)
 
 
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)
 
 
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]
 
 
133
    def external_url(self):
 
 
134
        """See bzrlib.transport.Transport.external_url."""
 
 
135
        # MemoryTransport's are only accessible in-process
 
 
137
        raise InProcessTransport(self)
 
 
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)
 
 
146
                raise NoSuchFile(relpath)
 
 
147
        return StringIO(self._files[_abspath][0])
 
 
149
    def put_file(self, relpath, f, mode=None):
 
 
150
        """See Transport.put_file()."""
 
 
151
        _abspath = self._abspath(relpath)
 
 
152
        self._check_parent(_abspath)
 
 
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)
 
 
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
 
 
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
 
 
179
        """See Transport.listable."""
 
 
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):])
 
 
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)
 
 
194
        if not _abspath.endswith('/'):
 
 
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)
 
 
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)
 
 
212
            elif x.startswith(abs_from + '/'):
 
 
213
                x = abs_to + x[len(abs_from):]
 
 
215
        def do_renames(container):
 
 
216
            for path in container:
 
 
217
                new_path = replace(path)
 
 
219
                    if new_path in container:
 
 
220
                        raise FileExists(new_path)
 
 
221
                    container[new_path] = container[path]
 
 
223
        do_renames(self._files)
 
 
224
        do_renames(self._dirs)
 
 
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),
 
 
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]
 
 
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])
 
 
251
            raise NoSuchFile(_abspath)
 
 
253
    def lock_read(self, relpath):
 
 
254
        """See Transport.lock_read()."""
 
 
255
        return _MemoryLock(self._abspath(relpath), self)
 
 
257
    def lock_write(self, relpath):
 
 
258
        """See Transport.lock_write()."""
 
 
259
        return _MemoryLock(self._abspath(relpath), self)
 
 
261
    def _abspath(self, relpath):
 
 
262
        """Generate an internal absolute path."""
 
 
263
        relpath = urlutils.unescape(relpath)
 
 
266
        if relpath[0] == '/':
 
 
268
        cwd_parts = self._cwd.split('/')
 
 
269
        rel_parts = relpath.split('/')
 
 
271
        for i in cwd_parts + rel_parts:
 
 
274
                    raise ValueError("illegal relpath %r under %r"
 
 
275
                        % (relpath, self._cwd))
 
 
277
            elif i == '.' or i == '':
 
 
281
        return '/' + '/'.join(r)
 
 
284
class _MemoryLock(object):
 
 
285
    """This makes a lock."""
 
 
287
    def __init__(self, path, transport):
 
 
288
        assert isinstance(transport, MemoryTransport)
 
 
290
        self.transport = transport
 
 
291
        if self.path in self.transport._locks:
 
 
292
            raise LockError('File %r already locked' % (self.path,))
 
 
293
        self.transport._locks[self.path] = self
 
 
296
        # Should this warn, or actually try to cleanup?
 
 
298
            warnings.warn("MemoryLock %r not explicitly unlocked" % (self.path,))
 
 
302
        del self.transport._locks[self.path]
 
 
303
        self.transport = None
 
 
306
class MemoryServer(Server):
 
 
307
    """Server for the MemoryTransport for testing with."""
 
 
310
        """See bzrlib.transport.Server.setUp."""
 
 
311
        self._dirs = {'/':None}
 
 
314
        self._scheme = "memory+%s:///" % id(self)
 
 
315
        def memory_factory(url):
 
 
316
            result = MemoryTransport(url)
 
 
317
            result._dirs = self._dirs
 
 
318
            result._files = self._files
 
 
319
            result._locks = self._locks
 
 
321
        register_transport(self._scheme, memory_factory)
 
 
324
        """See bzrlib.transport.Server.tearDown."""
 
 
325
        # unregister this server
 
 
328
        """See bzrlib.transport.Server.get_url."""
 
 
332
def get_test_permutations():
 
 
333
    """Return the permutations to be used in testing."""
 
 
334
    return [(MemoryTransport, MemoryServer),