Commit 62e77b6a by willmcgugan

Optimized FTP fs by caching directory structure

parent 72f3d49f
...@@ -26,4 +26,4 @@ ...@@ -26,4 +26,4 @@
* New FS implementation: * New FS implementation:
* FTPFS: access a plain old FTP server * FTPFS: access a plain old FTP server
* ReadOnlyFS: a WrapFS that makes an fs read-only * ReadOnlyFS: a WrapFS that makes an fs read-only
* Added cache_hint method to base.py
...@@ -107,7 +107,7 @@ try: ...@@ -107,7 +107,7 @@ try:
from functools import wraps from functools import wraps
except ImportError: except ImportError:
wraps = lambda f:f wraps = lambda f:f
def synchronize(func): def synchronize(func):
"""Decorator to synchronize a method on self._lock.""" """Decorator to synchronize a method on self._lock."""
@wraps(func) @wraps(func)
...@@ -132,7 +132,7 @@ class FS(object): ...@@ -132,7 +132,7 @@ class FS(object):
"""The base class for Filesystem objects. """The base class for Filesystem objects.
:param thread_synconize: If True, a lock object will be created for the object, otherwise a dummy lock will be used. :param thread_synconize: If True, a lock object will be created for the object, otherwise a dummy lock will be used.
:type thread_syncronize: bool :type thread_syncronize: bool
""" """
self.closed = False self.closed = False
if thread_synchronize: if thread_synchronize:
...@@ -144,7 +144,17 @@ class FS(object): ...@@ -144,7 +144,17 @@ class FS(object):
if not getattr(self, 'closed', True): if not getattr(self, 'closed', True):
self.close() self.close()
def close(self): def cache_hint(self, enabled):
"""Recommends the use of caching. Implementations are free to use or
ignore this value.
:param enabled: If True the implementation is permitted to cache directory
structure / file info.
"""
pass
def close(self):
self.closed = True self.closed = True
def __getstate__(self): def __getstate__(self):
...@@ -178,12 +188,12 @@ class FS(object): ...@@ -178,12 +188,12 @@ class FS(object):
then a NoSysPathError exception is thrown. Otherwise, the system then a NoSysPathError exception is thrown. Otherwise, the system
path will be returned as a unicode string. path will be returned as a unicode string.
:param path: A path within the filesystem :param path: A path within the filesystem
:param allow_none: If True, this method will return None when there is no system path, :param allow_none: If True, this method will return None when there is no system path,
rather than raising NoSysPathError rather than raising NoSysPathError
:type allow_none: bool :type allow_none: bool
:raises NoSysPathError: If the path does not map on to a system path, and allow_none is set to False (default) :raises NoSysPathError: If the path does not map on to a system path, and allow_none is set to False (default)
:rtype: unicode :rtype: unicode
""" """
if not allow_none: if not allow_none:
raise NoSysPathError(path=path) raise NoSysPathError(path=path)
...@@ -191,8 +201,8 @@ class FS(object): ...@@ -191,8 +201,8 @@ class FS(object):
def hassyspath(self, path): def hassyspath(self, path):
"""Return True if the path maps to a system path (a path recognised by the OS). """Return True if the path maps to a system path (a path recognised by the OS).
:param path: -- Path to check :param path: -- Path to check
:rtype: bool :rtype: bool
""" """
return self.getsyspath(path, allow_none=True) is not None return self.getsyspath(path, allow_none=True) is not None
...@@ -201,7 +211,7 @@ class FS(object): ...@@ -201,7 +211,7 @@ class FS(object):
def open(self, path, mode="r", **kwargs): def open(self, path, mode="r", **kwargs):
"""Open a the given path as a file-like object. """Open a the given path as a file-like object.
:param path: A path to file that should be opened :param path: A path to file that should be opened
:param mode: Mode of file to open, identical to the mode string used :param mode: Mode of file to open, identical to the mode string used
in 'file' and 'open' builtins in 'file' and 'open' builtins
:param kwargs: Additional (optional) keyword parameters that may :param kwargs: Additional (optional) keyword parameters that may
...@@ -212,12 +222,12 @@ class FS(object): ...@@ -212,12 +222,12 @@ class FS(object):
def safeopen(self, *args, **kwargs): def safeopen(self, *args, **kwargs):
"""Like 'open', but returns a NullFile if the file could not be opened. """Like 'open', but returns a NullFile if the file could not be opened.
A NullFile is a dummy file which has all the methods of a file-like object, A NullFile is a dummy file which has all the methods of a file-like object,
but contains no data. but contains no data.
:rtype: file-like object :rtype: file-like object
""" """
try: try:
f = self.open(*args, **kwargs) f = self.open(*args, **kwargs)
...@@ -228,27 +238,27 @@ class FS(object): ...@@ -228,27 +238,27 @@ class FS(object):
def exists(self, path): def exists(self, path):
"""Returns True if the path references a valid resource. """Returns True if the path references a valid resource.
:param path: A path in the filessystem :param path: A path in the filessystem
:rtype: bool :rtype: bool
""" """
return self.isfile(path) or self.isdir(path) return self.isfile(path) or self.isdir(path)
def isdir(self, path): def isdir(self, path):
"""Returns True if a given path references a directory. """Returns True if a given path references a directory.
:param path: A path in the filessystem :param path: A path in the filessystem
:rtype: bool :rtype: bool
""" """
raise UnsupportedError("check for directory") raise UnsupportedError("check for directory")
def isfile(self, path): def isfile(self, path):
"""Returns True if a given path references a file. """Returns True if a given path references a file.
:param path: A path in the filessystem :param path: A path in the filessystem
:rtype: bool :rtype: bool
""" """
raise UnsupportedError("check for file") raise UnsupportedError("check for file")
...@@ -272,23 +282,56 @@ class FS(object): ...@@ -272,23 +282,56 @@ class FS(object):
:param path: Root of the path to list :param path: Root of the path to list
:type path: str :type path: str
:param wildcard: Only returns paths that match this wildcard :param wildcard: Only returns paths that match this wildcard
:type wildcard: str :type wildcard: str
:param full: Returns full paths (relative to the root) :param full: Returns full paths (relative to the root)
:type full: bool :type full: bool
:param absolute: Returns absolute paths (paths begining with /) :param absolute: Returns absolute paths (paths begining with /)
:type absolute: bool :type absolute: bool
:param dirs_only: If True, only return directories :param dirs_only: If True, only return directories
:type dirs_only: bool :type dirs_only: bool
:param files_only: If True, only return files :param files_only: If True, only return files
:type files_only: bool :type files_only: bool
:rtype: iterable of paths :rtype: iterable of paths
:raises ResourceNotFoundError: If the path is not found :raises ResourceNotFoundError: If the path is not found
:raises ResourceInvalidError: If the path exists, but is not a directory :raises ResourceInvalidError: If the path exists, but is not a directory
""" """
raise UnsupportedError("list directory") raise UnsupportedError("list directory")
def listdirinfo(self, path="./",
wildcard=None,
full=False,
absolute=False,
dirs_only=False,
files_only=False):
"""Retrieves an iterable of paths and path info (as returned by getinfo) under
a given path.
:param path: Root of the path to list
:param wildcard: Filter paths that mach this wildcard
:dirs_only: Return only directory paths
:files_only: Return only files
:raises ResourceNotFoundError: If the path is not found
:raises ResourceInvalidError: If the path exists, but is not a directory
"""
def get_path(p):
if not full:
return pathjoin(path, p)
return [(p, self.getinfo(get_path(p)))
for p in self._listdir( path,
widcard=wildcard,
full=full,
absolute=absolute,
dirs_only=dirs_only,
files_only=files_only )]
def _listdir_helper(self, path, entries, def _listdir_helper(self, path, entries,
wildcard=None, wildcard=None,
...@@ -328,14 +371,14 @@ class FS(object): ...@@ -328,14 +371,14 @@ class FS(object):
:param path: Path of directory :param path: Path of directory
:param recursive: If True, any intermediate directories will also be created :param recursive: If True, any intermediate directories will also be created
:type recursive: bool :type recursive: bool
:param allow_recreate: If True, re-creating a directory wont be an error :param allow_recreate: If True, re-creating a directory wont be an error
:type allow_create: bool :type allow_create: bool
:raises DestinationExistsError: If the path is already a directory, and allow_recreate is False :raises DestinationExistsError: If the path is already a directory, and allow_recreate is False
:raises ParentDirectoryMissingError: If a containing directory is missing and recursive is False :raises ParentDirectoryMissingError: If a containing directory is missing and recursive is False
:raises ResourceInvalidError: If a path is an existing file :raises ResourceInvalidError: If a path is an existing file
""" """
raise UnsupportedError("make directory") raise UnsupportedError("make directory")
...@@ -346,7 +389,7 @@ class FS(object): ...@@ -346,7 +389,7 @@ class FS(object):
:raises ResourceNotFoundError: If the path does not exist :raises ResourceNotFoundError: If the path does not exist
:raises ResourceInvalidError: If the path is a directory :raises ResourceInvalidError: If the path is a directory
""" """
raise UnsupportedError("remove resource") raise UnsupportedError("remove resource")
...@@ -358,11 +401,11 @@ class FS(object): ...@@ -358,11 +401,11 @@ class FS(object):
:type recursive: bool :type recursive: bool
:param force: If True, any directory contents will be removed :param force: If True, any directory contents will be removed
:type force: bool :type force: bool
:raises ResourceNotFoundError: If the path does not exist :raises ResourceNotFoundError: If the path does not exist
:raises ResourceInvalidError: If the path is not a directory :raises ResourceInvalidError: If the path is not a directory
:raises DirectoryNotEmptyError: If the directory is not empty and force is False :raises DirectoryNotEmptyError: If the directory is not empty and force is False
""" """
raise UnsupportedError("remove directory") raise UnsupportedError("remove directory")
...@@ -463,7 +506,7 @@ class FS(object): ...@@ -463,7 +506,7 @@ class FS(object):
:param search: -- A string dentifying the method used to walk the directories. There are two such methods: :param search: -- A string dentifying the method used to walk the directories. There are two such methods:
* 'breadth' Yields paths in the top directories first * 'breadth' Yields paths in the top directories first
* 'depth' Yields the deepest paths first * 'depth' Yields the deepest paths first
""" """
if search == "breadth": if search == "breadth":
dirs = [path] dirs = [path]
...@@ -772,7 +815,7 @@ class SubFS(FS): ...@@ -772,7 +815,7 @@ class SubFS(FS):
def __str__(self): def __str__(self):
return "<SubFS: %s in %s>" % (self.sub_dir, self.parent) return "<SubFS: %s in %s>" % (self.sub_dir, self.parent)
def __unicode__(self): def __unicode__(self):
return u"<SubFS: %s in %s>" % (self.sub_dir, self.parent) return u"<SubFS: %s in %s>" % (self.sub_dir, self.parent)
......
...@@ -35,7 +35,7 @@ class InfoFrame(wx.Frame): ...@@ -35,7 +35,7 @@ class InfoFrame(wx.Frame):
self.list_ctrl.SetColumnWidth(1, 300) self.list_ctrl.SetColumnWidth(1, 300)
for key in keys: for key in keys:
self.list_ctrl.Append((key, repr(info.get(key)))) self.list_ctrl.Append((key, str(info.get(key))))
...@@ -99,7 +99,7 @@ class BrowseFrame(wx.Frame): ...@@ -99,7 +99,7 @@ class BrowseFrame(wx.Frame):
return return
paths = [(self.fs.isdir(p), p) for p in self.fs.listdir(path, absolute=True)] paths = [(self.fs.isdir(p), p) for p in self.fs.listdir(path, absolute=True)]
if not paths: if not paths:
#self.tree.SetItemHasChildren(item_id, False) #self.tree.SetItemHasChildren(item_id, False)
#self.tree.Collapse(item_id) #self.tree.Collapse(item_id)
......
...@@ -22,57 +22,53 @@ from fs import ftpfs ...@@ -22,57 +22,53 @@ from fs import ftpfs
ftp_port = 30000 ftp_port = 30000
class TestFTPFS(unittest.TestCase, FSTestCases, ThreadingTestCases): class TestFTPFS(unittest.TestCase, FSTestCases, ThreadingTestCases):
def setUp(self): def setUp(self):
global ftp_port global ftp_port
#ftp_port += 1 #ftp_port += 1
use_port = str(ftp_port) use_port = str(ftp_port)
#ftp_port = 10000 #ftp_port = 10000
sys.setcheckinterval(1) sys.setcheckinterval(1)
self.temp_dir = tempfile.mkdtemp(u"ftpfstests") self.temp_dir = tempfile.mkdtemp(u"ftpfstests")
self.ftp_server = subprocess.Popen(['python', abspath(__file__), self.temp_dir, str(use_port)]) self.ftp_server = subprocess.Popen(['python', abspath(__file__), self.temp_dir, str(use_port)])
# Need to sleep to allow ftp server to start # Need to sleep to allow ftp server to start
time.sleep(.2) time.sleep(.2)
self.fs = ftpfs.FTPFS('127.0.0.1', 'user', '12345', port=use_port, timeout=5.0) self.fs = ftpfs.FTPFS('127.0.0.1', 'user', '12345', port=use_port, timeout=5.0)
def tearDown(self): def tearDown(self):
if sys.platform == 'win32': if sys.platform == 'win32':
import win32api import win32api
win32api.TerminateProcess(int(process._handle), -1) win32api.TerminateProcess(int(process._handle), -1)
else: else:
os.system('kill '+str(self.ftp_server.pid)) os.system('kill '+str(self.ftp_server.pid))
shutil.rmtree(self.temp_dir) shutil.rmtree(self.temp_dir)
def check(self, p): def check(self, p):
return os.path.exists(os.path.join(self.temp_dir, relpath(p))) return os.path.exists(os.path.join(self.temp_dir, relpath(p)))
if __name__ == "__main__": if __name__ == "__main__":
# Run an ftp server that exposes a given directory # Run an ftp server that exposes a given directory
import sys import sys
authorizer = ftpserver.DummyAuthorizer() authorizer = ftpserver.DummyAuthorizer()
authorizer.add_user("user", "12345", sys.argv[1], perm="elradfmw") authorizer.add_user("user", "12345", sys.argv[1], perm="elradfmw")
authorizer.add_anonymous(sys.argv[1]) authorizer.add_anonymous(sys.argv[1])
def nolog(*args): def nolog(*args):
pass pass
ftpserver.log = nolog ftpserver.log = nolog
ftpserver.logline = nolog ftpserver.logline = nolog
handler = ftpserver.FTPHandler handler = ftpserver.FTPHandler
handler.authorizer = authorizer handler.authorizer = authorizer
address = ("127.0.0.1", int(sys.argv[2])) address = ("127.0.0.1", int(sys.argv[2]))
#print address #print address
ftpd = ftpserver.FTPServer(address, handler)
ftpd.serve_forever()
ftpd = ftpserver.FTPServer(address, handler)
ftpd.serve_forever()
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