Commit 0ffdf940 by rfkelly0

MountFS: support mounting at root dir, and mounting over mounted dirs.

This changes MountFS.mount_tree from an ObjectTree to a PathMap, since
ObjectTree didn't like mounting things at the root dir.
parent db8630d9
...@@ -47,4 +47,6 @@ ...@@ -47,4 +47,6 @@
fs.wrapfs.subfs. fs.wrapfs.subfs.
* Fix OSFS.add_watcher on linux platforms; previous version would cancel * Fix OSFS.add_watcher on linux platforms; previous version would cancel
any watchers when a new one was added. any watchers when a new one was added.
* MountFS: added support for mounting at the root directory, and for
mounting over an existing mount.
...@@ -45,7 +45,6 @@ Now both filesystems can be accessed with the same path structure:: ...@@ -45,7 +45,6 @@ Now both filesystems can be accessed with the same path structure::
from fs.base import * from fs.base import *
from fs.errors import * from fs.errors import *
from fs.path import * from fs.path import *
from fs.objecttree import ObjectTree
from fs import _thread_synchronize_default from fs import _thread_synchronize_default
...@@ -74,7 +73,7 @@ class MountFS(FS): ...@@ -74,7 +73,7 @@ class MountFS(FS):
def __init__(self, thread_synchronize=_thread_synchronize_default): def __init__(self, thread_synchronize=_thread_synchronize_default):
super(MountFS, self).__init__(thread_synchronize=thread_synchronize) super(MountFS, self).__init__(thread_synchronize=thread_synchronize)
self.mount_tree = ObjectTree() self.mount_tree = PathMap()
def __str__(self): def __str__(self):
return "<MountFS>" return "<MountFS>"
...@@ -85,17 +84,32 @@ class MountFS(FS): ...@@ -85,17 +84,32 @@ class MountFS(FS):
return unicode(self.__str__()) return unicode(self.__str__())
def _delegate(self, path): def _delegate(self, path):
path = normpath(path) path = abspath(normpath(path))
head_path, object, tail_path = self.mount_tree.partialget(path) object = None
head_path = "/"
tail_path = path
for prefix in recursepath(path):
try:
object = self.mount_tree[prefix]
except KeyError:
pass
else:
head_path = prefix
tail_path = path[len(head_path):]
if type(object) is MountFS.DirMount: if type(object) is MountFS.DirMount:
dirmount = object return object.fs, head_path, tail_path
return dirmount.fs, head_path, tail_path
if object is None: if type(object) is MountFS.FileMount:
return None, None, None return self, "/", path
return self, head_path, tail_path try:
self.mount_tree.iternames(path).next()
except StopIteration:
return None, None, None
else:
return self, "/", path
def getsyspath(self, path, allow_none=False): def getsyspath(self, path, allow_none=False):
fs, mount_path, delegate_path = self._delegate(path) fs, mount_path, delegate_path = self._delegate(path)
...@@ -123,9 +137,8 @@ class MountFS(FS): ...@@ -123,9 +137,8 @@ class MountFS(FS):
return False return False
if fs is self: if fs is self:
object = self.mount_tree.get(path, None) object = self.mount_tree.get(path, None)
return isinstance(object, dict) return not isinstance(object,MountFS.FileMount)
else: return fs.isdir(delegate_path)
return fs.isdir(delegate_path)
@synchronize @synchronize
def isfile(self, path): def isfile(self, path):
...@@ -134,23 +147,27 @@ class MountFS(FS): ...@@ -134,23 +147,27 @@ class MountFS(FS):
return False return False
if fs is self: if fs is self:
object = self.mount_tree.get(path, None) object = self.mount_tree.get(path, None)
return type(object) is MountFS.FileMount return isinstance(object,MountFS.FileMount)
else: return fs.isfile(delegate_path)
return fs.isfile(delegate_path)
@synchronize
def exists(self, path):
fs, mount_path, delegate_path = self._delegate(path)
if fs is None:
return False
if fs is self:
return True
return fs.exists(delegate_path)
@synchronize @synchronize
def listdir(self, path="/", wildcard=None, full=False, absolute=False, dirs_only=False, files_only=False): def listdir(self, path="/", wildcard=None, full=False, absolute=False, dirs_only=False, files_only=False):
path = normpath(path)
fs, mount_path, delegate_path = self._delegate(path) fs, mount_path, delegate_path = self._delegate(path)
if fs is None: if fs is None:
raise ResourceNotFoundError(path) raise ResourceNotFoundError(path)
if fs is self: if fs is self:
if files_only: paths = self.mount_tree.names(path)
return []
paths = self.mount_tree[path].keys()
return self._listdir_helper(path, return self._listdir_helper(path,
paths, paths,
wildcard, wildcard,
...@@ -167,26 +184,22 @@ class MountFS(FS): ...@@ -167,26 +184,22 @@ class MountFS(FS):
files_only=files_only) files_only=files_only)
if full or absolute: if full or absolute:
if full: if full:
path = abspath(normpath(path))
else:
path = relpath(normpath(path)) path = relpath(normpath(path))
else:
path = abspath(normpath(path))
paths = [pathjoin(path, p) for p in paths] paths = [pathjoin(path, p) for p in paths]
return paths return paths
@synchronize @synchronize
def makedir(self, path, recursive=False, allow_recreate=False): def makedir(self, path, recursive=False, allow_recreate=False):
path = normpath(path)
fs, mount_path, delegate_path = self._delegate(path) fs, mount_path, delegate_path = self._delegate(path)
if fs is self: if fs is self or fs is None:
raise UnsupportedError("make directory", msg="Can only makedir for mounted paths" ) raise UnsupportedError("make directory", msg="Can only makedir for mounted paths" )
if not delegate_path:
return True
return fs.makedir(delegate_path, recursive=recursive, allow_recreate=allow_recreate) return fs.makedir(delegate_path, recursive=recursive, allow_recreate=allow_recreate)
@synchronize @synchronize
def open(self, path, mode="r", **kwargs): def open(self, path, mode="r", **kwargs):
path = normpath(path)
object = self.mount_tree.get(path, None) object = self.mount_tree.get(path, None)
if type(object) is MountFS.FileMount: if type(object) is MountFS.FileMount:
callable = object.open_callable callable = object.open_callable
...@@ -194,39 +207,25 @@ class MountFS(FS): ...@@ -194,39 +207,25 @@ class MountFS(FS):
fs, mount_path, delegate_path = self._delegate(path) fs, mount_path, delegate_path = self._delegate(path)
if fs is None: if fs is self or fs is None:
raise ResourceNotFoundError(path) raise ResourceNotFoundError(path)
return fs.open(delegate_path, mode, **kwargs) return fs.open(delegate_path, mode, **kwargs)
@synchronize @synchronize
def setcontents(self, path, contents): def setcontents(self, path, contents):
path = normpath(path)
object = self.mount_tree.get(path, None) object = self.mount_tree.get(path, None)
if type(object) is MountFS.FileMount: if type(object) is MountFS.FileMount:
return super(MountFS,self).setcontents(path,contents) return super(MountFS,self).setcontents(path,contents)
fs, mount_path, delegate_path = self._delegate(path) fs, mount_path, delegate_path = self._delegate(path)
if fs is None: if fs is self or fs is None:
raise ParentDirectoryMissingError(path) raise ParentDirectoryMissingError(path)
return fs.setcontents(delegate_path,contents) return fs.setcontents(delegate_path,contents)
@synchronize @synchronize
def exists(self, path):
path = normpath(path)
fs, mount_path, delegate_path = self._delegate(path)
if fs is None:
return False
if fs is self:
return path in self.mount_tree
return fs.exists(delegate_path)
@synchronize
def remove(self, path): def remove(self, path):
path = normpath(path)
fs, mount_path, delegate_path = self._delegate(path) fs, mount_path, delegate_path = self._delegate(path)
if fs is None: if fs is self or fs is None:
raise ResourceNotFoundError(path)
if fs is self:
raise UnsupportedError("remove file", msg="Can only remove paths within a mounted dir") raise UnsupportedError("remove file", msg="Can only remove paths within a mounted dir")
return fs.remove(delegate_path) return fs.remove(delegate_path)
...@@ -234,13 +233,8 @@ class MountFS(FS): ...@@ -234,13 +233,8 @@ class MountFS(FS):
def removedir(self, path, recursive=False, force=False): def removedir(self, path, recursive=False, force=False):
path = normpath(path) path = normpath(path)
fs, mount_path, delegate_path = self._delegate(path) fs, mount_path, delegate_path = self._delegate(path)
if fs is self or fs is None:
if fs is None or fs is self:
raise ResourceInvalidError(path, msg="Can not removedir for an un-mounted path") raise ResourceInvalidError(path, msg="Can not removedir for an un-mounted path")
if not force and not fs.isdirempty(delegate_path):
raise DirectoryNotEmptyError("Directory is not empty: %(path)s")
return fs.removedir(delegate_path, recursive, force) return fs.removedir(delegate_path, recursive, force)
@synchronize @synchronize
...@@ -254,9 +248,6 @@ class MountFS(FS): ...@@ -254,9 +248,6 @@ class MountFS(FS):
if fs1 is not self: if fs1 is not self:
return fs1.rename(delegate_path1, delegate_path2) return fs1.rename(delegate_path1, delegate_path2)
path_src = normpath(src)
path_dst = normpath(dst)
object = self.mount_tree.get(path_src, None) object = self.mount_tree.get(path_src, None)
object2 = self.mount_tree.get(path_dst, None) object2 = self.mount_tree.get(path_dst, None)
...@@ -310,7 +301,6 @@ class MountFS(FS): ...@@ -310,7 +301,6 @@ class MountFS(FS):
:param fs: A filesystem object to mount :param fs: A filesystem object to mount
""" """
path = normpath(path)
self.mount_tree[path] = MountFS.DirMount(path, fs) self.mount_tree[path] = MountFS.DirMount(path, fs)
mount = mountdir mount = mountdir
...@@ -323,7 +313,6 @@ class MountFS(FS): ...@@ -323,7 +313,6 @@ class MountFS(FS):
:param info_callable: A callable that returns a dictionary with information regarding the file-like object :param info_callable: A callable that returns a dictionary with information regarding the file-like object
""" """
path = normpath(path)
self.mount_tree[path] = MountFS.FileMount(path, callable, info_callable) self.mount_tree[path] = MountFS.FileMount(path, callable, info_callable)
@synchronize @synchronize
...@@ -333,15 +322,16 @@ class MountFS(FS): ...@@ -333,15 +322,16 @@ class MountFS(FS):
:param path: Path to unmount :param path: Path to unmount
""" """
path = normpath(path)
del self.mount_tree[path] del self.mount_tree[path]
@synchronize @synchronize
def settimes(self, path, accessed_time=None, modified_time=None): def settimes(self, path, accessed_time=None, modified_time=None):
path = normpath(path) path = normpath(path)
fs, mount_path, delegate_path = self._delegate(path) fs, mount_path, delegate_path = self._delegate(path)
if fs is None: if fs is None:
raise ResourceNotFoundError(path) raise ResourceNotFoundError(path)
if fs is self: if fs is self:
raise UnsupportedError("settimes") raise UnsupportedError("settimes")
fs.settimes(delegate_path, accessed_time, modified_time) fs.settimes(delegate_path, accessed_time, modified_time)
...@@ -372,10 +362,12 @@ class MountFS(FS): ...@@ -372,10 +362,12 @@ class MountFS(FS):
if fs is self: if fs is self:
object = self.mount_tree.get(path, None) object = self.mount_tree.get(path, None)
if object is None or isinstance(object, dict): if object is None:
raise ResourceNotFoundError(path) raise ResourceNotFoundError(path)
if not isinstance(object,MountFS.FileMount):
raise ResourceInvalidError(path)
size = self.mount_tree[path].info_callable(path).get("size", None) size = object.info_callable(path).get("size", None)
return size return size
return fs.getinfo(delegate_path).get("size", None) return fs.getinfo(delegate_path).get("size", None)
...@@ -419,3 +411,5 @@ class MountFS(FS): ...@@ -419,3 +411,5 @@ class MountFS(FS):
if fs is self: if fs is self:
return [] return []
return fs.listxattrs(delegate_path) return fs.listxattrs(delegate_path)
...@@ -66,6 +66,29 @@ class TestMountFS(unittest.TestCase,FSTestCases,ThreadingTestCases): ...@@ -66,6 +66,29 @@ class TestMountFS(unittest.TestCase,FSTestCases,ThreadingTestCases):
def check(self, p): def check(self, p):
return self.mount_fs.exists(os.path.join("mounted/memfs", relpath(p))) return self.mount_fs.exists(os.path.join("mounted/memfs", relpath(p)))
class TestMountFS_atroot(unittest.TestCase,FSTestCases,ThreadingTestCases):
def setUp(self):
self.mem_fs = memoryfs.MemoryFS()
self.fs = mountfs.MountFS()
self.fs.mountdir("", self.mem_fs)
def check(self, p):
return self.mem_fs.exists(p)
class TestMountFS_stacked(unittest.TestCase,FSTestCases,ThreadingTestCases):
def setUp(self):
self.mem_fs1 = memoryfs.MemoryFS()
self.mem_fs2 = memoryfs.MemoryFS()
self.mount_fs = mountfs.MountFS()
self.mount_fs.mountdir("mem", self.mem_fs1)
self.mount_fs.mountdir("mem/two", self.mem_fs2)
self.fs = self.mount_fs.opendir("/mem/two")
def check(self, p):
return self.mount_fs.exists(os.path.join("mem/two", relpath(p)))
from fs import tempfs from fs import tempfs
class TestTempFS(unittest.TestCase,FSTestCases,ThreadingTestCases): class TestTempFS(unittest.TestCase,FSTestCases,ThreadingTestCases):
......
...@@ -76,6 +76,9 @@ class DisconnectingFS(WrapFS): ...@@ -76,6 +76,9 @@ class DisconnectingFS(WrapFS):
time.sleep(random.random()*0.1) time.sleep(random.random()*0.1)
self._connected = not self._connected self._connected = not self._connected
def setcontents(self, path, contents):
return self.wrapped_fs.setcontents(path, contents)
def close(self): def close(self):
if not self.closed: if not self.closed:
self._continue = False self._continue = False
...@@ -84,6 +87,7 @@ class DisconnectingFS(WrapFS): ...@@ -84,6 +87,7 @@ class DisconnectingFS(WrapFS):
self._connected = True self._connected = True
super(DisconnectingFS,self).close() super(DisconnectingFS,self).close()
def disconnecting_wrapper(func): def disconnecting_wrapper(func):
"""Method wrapper to raise RemoteConnectionError if not connected.""" """Method wrapper to raise RemoteConnectionError if not connected."""
@wraps(func) @wraps(func)
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment