1
# Copyright (C) 2005-2011, 2016 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 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.
23
from __future__ import absolute_import
32
from stat import S_IFREG, S_IFDIR, S_IFLNK, S_ISDIR
38
from ..errors import (
45
from ..transport import (
46
AppendBasedFileStream,
52
class MemoryStat(object):
54
def __init__(self, size, kind, perms=None):
59
self.st_mode = kind | perms
63
self.st_mode = kind | perms
66
class MemoryTransport(transport.Transport):
67
"""This is an in memory file system for transient data storage."""
69
def __init__(self, url=""):
70
"""Set the 'base' path where files will be stored."""
75
super(MemoryTransport, self).__init__(url)
76
split = url.find(':') + 3
77
self._scheme = url[:split]
78
self._cwd = url[split:]
79
# dictionaries from absolute path to file mode
80
self._dirs = {'/': None}
85
def clone(self, offset=None):
86
"""See Transport.clone()."""
87
path = urlutils.URL._combine_paths(self._cwd, offset)
88
if len(path) == 0 or path[-1] != '/':
90
url = self._scheme + path
91
result = self.__class__(url)
92
result._dirs = self._dirs
93
result._symlinks = self._symlinks
94
result._files = self._files
95
result._locks = self._locks
98
def abspath(self, relpath):
99
"""See Transport.abspath()."""
100
# while a little slow, this is sufficiently fast to not matter in our
101
# current environment - XXX RBC 20060404 move the clone '..' handling
102
# into here and call abspath from clone
103
temp_t = self.clone(relpath)
104
if temp_t.base.count('/') == 3:
107
return temp_t.base[:-1]
109
def append_file(self, relpath, f, mode=None):
110
"""See Transport.append_file()."""
111
_abspath = self._resolve_symlinks(relpath)
112
self._check_parent(_abspath)
113
orig_content, orig_mode = self._files.get(_abspath, (b"", None))
116
self._files[_abspath] = (orig_content + f.read(), mode)
117
return len(orig_content)
119
def _check_parent(self, _abspath):
120
dir = os.path.dirname(_abspath)
122
if dir not in self._dirs:
123
raise NoSuchFile(_abspath)
125
def has(self, relpath):
126
"""See Transport.has()."""
127
_abspath = self._abspath(relpath)
128
for container in (self._files, self._dirs, self._symlinks):
129
if _abspath in container.keys():
133
def delete(self, relpath):
134
"""See Transport.delete()."""
135
_abspath = self._abspath(relpath)
136
if _abspath in self._files:
137
del self._files[_abspath]
138
elif _abspath in self._symlinks:
139
del self._symlinks[_abspath]
141
raise NoSuchFile(relpath)
143
def external_url(self):
144
"""See breezy.transport.Transport.external_url."""
145
# MemoryTransport's are only accessible in-process
147
raise InProcessTransport(self)
149
def get(self, relpath):
150
"""See Transport.get()."""
151
_abspath = self._resolve_symlinks(relpath)
152
if _abspath not in self._files:
153
if _abspath in self._dirs:
154
return LateReadError(relpath)
156
raise NoSuchFile(relpath)
157
return BytesIO(self._files[_abspath][0])
159
def put_file(self, relpath, f, mode=None):
160
"""See Transport.put_file()."""
161
_abspath = self._resolve_symlinks(relpath)
162
self._check_parent(_abspath)
164
self._files[_abspath] = (raw_bytes, mode)
165
return len(raw_bytes)
167
def symlink(self, source, target):
168
_abspath = self._resolve_symlinks(target)
169
self._check_parent(_abspath)
170
self._symlinks[_abspath] = self._abspath(source)
172
def mkdir(self, relpath, mode=None):
173
"""See Transport.mkdir()."""
174
_abspath = self._resolve_symlinks(relpath)
175
self._check_parent(_abspath)
176
if _abspath in self._dirs:
177
raise FileExists(relpath)
178
self._dirs[_abspath] = mode
180
def open_write_stream(self, relpath, mode=None):
181
"""See Transport.open_write_stream."""
182
self.put_bytes(relpath, b"", mode)
183
result = AppendBasedFileStream(self, relpath)
184
_file_streams[self.abspath(relpath)] = result
188
"""See Transport.listable."""
191
def iter_files_recursive(self):
192
for file in itertools.chain(self._files, self._symlinks):
193
if file.startswith(self._cwd):
194
yield urlutils.escape(file[len(self._cwd):])
196
def list_dir(self, relpath):
197
"""See Transport.list_dir()."""
198
_abspath = self._resolve_symlinks(relpath)
199
if _abspath != '/' and _abspath not in self._dirs:
200
raise NoSuchFile(relpath)
203
if not _abspath.endswith('/'):
206
for path_group in self._files, self._dirs, self._symlinks:
207
for path in path_group:
208
if path.startswith(_abspath):
209
trailing = path[len(_abspath):]
210
if trailing and '/' not in trailing:
211
result.append(urlutils.escape(trailing))
214
def rename(self, rel_from, rel_to):
215
"""Rename a file or directory; fail if the destination exists"""
216
abs_from = self._resolve_symlinks(rel_from)
217
abs_to = self._resolve_symlinks(rel_to)
222
elif x.startswith(abs_from + '/'):
223
x = abs_to + x[len(abs_from):]
226
def do_renames(container):
228
for path in container:
229
new_path = replace(path)
231
if new_path in container:
232
raise FileExists(new_path)
233
renames.append((path, new_path))
234
for path, new_path in renames:
235
container[new_path] = container[path]
238
# If we modify the existing dicts, we're not atomic anymore and may
239
# fail differently depending on dict order. So work on copy, fail on
240
# error on only replace dicts if all goes well.
241
renamed_files = self._files.copy()
242
renamed_symlinks = self._symlinks.copy()
243
renamed_dirs = self._dirs.copy()
244
do_renames(renamed_files)
245
do_renames(renamed_symlinks)
246
do_renames(renamed_dirs)
247
# We may have been cloned so modify in place
249
self._files.update(renamed_files)
250
self._symlinks.clear()
251
self._symlinks.update(renamed_symlinks)
253
self._dirs.update(renamed_dirs)
255
def rmdir(self, relpath):
256
"""See Transport.rmdir."""
257
_abspath = self._resolve_symlinks(relpath)
258
if _abspath in self._files:
259
self._translate_error(IOError(errno.ENOTDIR, relpath), relpath)
260
for path in itertools.chain(self._files, self._symlinks):
261
if path.startswith(_abspath + '/'):
262
self._translate_error(IOError(errno.ENOTEMPTY, relpath),
264
for path in self._dirs:
265
if path.startswith(_abspath + '/') and path != _abspath:
266
self._translate_error(
267
IOError(errno.ENOTEMPTY, relpath), relpath)
268
if _abspath not in self._dirs:
269
raise NoSuchFile(relpath)
270
del self._dirs[_abspath]
272
def stat(self, relpath):
273
"""See Transport.stat()."""
274
_abspath = self._abspath(relpath)
275
if _abspath in self._files.keys():
276
return MemoryStat(len(self._files[_abspath][0]), S_IFREG,
277
self._files[_abspath][1])
278
elif _abspath in self._dirs.keys():
279
return MemoryStat(0, S_IFDIR, self._dirs[_abspath])
280
elif _abspath in self._symlinks.keys():
281
return MemoryStat(0, S_IFLNK)
283
raise NoSuchFile(_abspath)
285
def lock_read(self, relpath):
286
"""See Transport.lock_read()."""
287
return _MemoryLock(self._abspath(relpath), self)
289
def lock_write(self, relpath):
290
"""See Transport.lock_write()."""
291
return _MemoryLock(self._abspath(relpath), self)
293
def _resolve_symlinks(self, relpath):
294
path = self._abspath(relpath)
295
while path in self._symlinks.keys():
296
path = self._symlinks[path]
299
def _abspath(self, relpath):
300
"""Generate an internal absolute path."""
301
relpath = urlutils.unescape(relpath)
302
if relpath[:1] == '/':
304
cwd_parts = self._cwd.split('/')
305
rel_parts = relpath.split('/')
307
for i in cwd_parts + rel_parts:
310
raise ValueError("illegal relpath %r under %r"
311
% (relpath, self._cwd))
313
elif i == '.' or i == '':
317
r = self._symlinks.get('/'.join(r), r)
318
return '/' + '/'.join(r)
320
def symlink(self, source, link_name):
321
"""Create a symlink pointing to source named link_name."""
322
_abspath = self._abspath(link_name)
323
self._check_parent(_abspath)
324
self._symlinks[_abspath] = source.split('/')
326
def readlink(self, link_name):
327
_abspath = self._abspath(link_name)
329
return '/'.join(self._symlinks[_abspath])
331
raise NoSuchFile(link_name)
334
class _MemoryLock(object):
335
"""This makes a lock."""
337
def __init__(self, path, transport):
339
self.transport = transport
340
if self.path in self.transport._locks:
341
raise LockError('File %r already locked' % (self.path,))
342
self.transport._locks[self.path] = self
345
del self.transport._locks[self.path]
346
self.transport = None
349
class MemoryServer(transport.Server):
350
"""Server for the MemoryTransport for testing with."""
352
def start_server(self):
353
self._dirs = {'/': None}
357
self._scheme = "memory+%s:///" % id(self)
359
def memory_factory(url):
361
result = memory.MemoryTransport(url)
362
result._dirs = self._dirs
363
result._files = self._files
364
result._symlinks = self._symlinks
365
result._locks = self._locks
367
self._memory_factory = memory_factory
368
transport.register_transport(self._scheme, self._memory_factory)
370
def stop_server(self):
371
# unregister this server
372
transport.unregister_transport(self._scheme, self._memory_factory)
375
"""See breezy.transport.Server.get_url."""
378
def get_bogus_url(self):
379
raise NotImplementedError
382
def get_test_permutations():
383
"""Return the permutations to be used in testing."""
384
return [(MemoryTransport, MemoryServer),