summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorwillmcgugan <willmcgugan@67cdc799-7952-0410-af00-57a81ceafa0f>2011-08-07 15:12:10 +0000
committerwillmcgugan <willmcgugan@67cdc799-7952-0410-af00-57a81ceafa0f>2011-08-07 15:12:10 +0000
commit19a25b721353cc4fc2ed7adcbdb1709dc83b621d (patch)
treeba26b1383d532e357fb9167533a74ddbcf343a90
parent038c0022c0d07069815ca46af0215ffa8ca2cf52 (diff)
downloadpyfilesystem-19a25b721353cc4fc2ed7adcbdb1709dc83b621d.tar.gz
Fixes and documentation
git-svn-id: http://pyfilesystem.googlecode.com/svn/trunk@718 67cdc799-7952-0410-af00-57a81ceafa0f
-rw-r--r--docs/concepts.rst8
-rw-r--r--docs/getting_started.rst12
-rw-r--r--fs/base.py343
-rw-r--r--fs/commands/fsls.py4
-rw-r--r--fs/commands/runner.py2
-rw-r--r--fs/expose/serve/server.py72
-rw-r--r--fs/expose/sftp.py6
-rw-r--r--fs/ftpfs.py2
-rw-r--r--fs/opener.py7
-rw-r--r--fs/path.py21
-rw-r--r--fs/remotefs.py51
-rw-r--r--fs/sftpfs.py13
-rw-r--r--fs/tests/test_expose.py55
-rw-r--r--fs/tests/test_path.py5
14 files changed, 415 insertions, 186 deletions
diff --git a/docs/concepts.rst b/docs/concepts.rst
index e30758d..313ed04 100644
--- a/docs/concepts.rst
+++ b/docs/concepts.rst
@@ -1,7 +1,7 @@
Concepts
========
-It is generally quite easy to get in to the mind-set of using PyFilesystem interface over lower level interfaces (since the code tends to be simpler) but there are a few concepts which you will need to keep in mind.
+Working with PyFilesystem is generally easier than working with lower level interfaces, as long as you are aware these simple concepts.
Sandboxing
----------
@@ -25,7 +25,9 @@ We can open the `foo` directory with the following code::
from fs.osfs import OSFS
foo_fs = OSFS('foo')
-The `foo_fs` object can work with any of the contents of `bar` and `baz`, which may not be desirable, especially if we are passing `foo_fs` to an untrusted function or one that could potentially delete files. Fortunately we can isolate a single sub-directory with then :meth:`~fs.base.FS.opendir` method::
+The `foo_fs` object can work with any of the contents of `bar` and `baz`, which may not be desirable,
+especially if we are passing `foo_fs` to an untrusted function or to a function that has the potential to delete files.
+Fortunately we can isolate a single sub-directory with then :meth:`~fs.base.FS.opendir` method::
bar_fs = foo_fs.opendir('bar')
@@ -54,7 +56,7 @@ When working with paths in FS objects, keep in mind the following:
* A double dot means 'previous directory'
Note that paths used by the FS interface will use this format, but the constructor or additional methods may not.
-Notably the :mod:`~fs.osfs.OSFS` constructor which requires an OS path -- the format of which can be platform-dependent.
+Notably the :mod:`~fs.osfs.OSFS` constructor which requires an OS path -- the format of which is platform-dependent.
There are many helpful functions for working with paths in the :mod:`fs.path` module.
diff --git a/docs/getting_started.rst b/docs/getting_started.rst
index dab09ef..66887e1 100644
--- a/docs/getting_started.rst
+++ b/docs/getting_started.rst
@@ -13,14 +13,22 @@ The easiest way to install PyFilesystem is with `easy_install <http://peak.telec
Add the -U switch if you want to upgrade a previous installation::
easy_install -U fs
+
+If you prefer to use Pip (http://pypi.python.org/pypi/pip) to install Python packages, the procedure is much the same::
-This will install the latest stable release. If you would prefer to install the cutting edge release then you can get the latest copy of the source via SVN::
+ pip install fs
+
+Or to upgrade::
+
+ pip install fs --upgrade
+
+You can also install the cutting edge release by checking out the source via SVN::
svn checkout http://pyfilesystem.googlecode.com/svn/trunk/ pyfilesystem-read-only
cd pyfilesystem-read-only
python setup.py install
-You should now have the `fs` module on your path:
+Whichever method you use, you should now have the `fs` module on your path (version number may vary)::
>>> import fs
>>> fs.__version__
diff --git a/fs/base.py b/fs/base.py
index 67693b3..9b4a0fa 100644
--- a/fs/base.py
+++ b/fs/base.py
@@ -52,10 +52,10 @@ class DummyLock(object):
def release(self):
"""Releasing a DummyLock always succeeds."""
pass
-
+
def __enter__(self):
pass
-
+
def __exit__(self, *args):
pass
@@ -95,7 +95,7 @@ class NullFile(object):
def __enter__(self):
return self
-
+
def __exit__(self, exc_type, exc_value, traceback):
self.closed = True
@@ -147,7 +147,7 @@ class FS(object):
An instance of a class derived from FS is an abstraction on some kind of filesystem, such as the OS filesystem or a zip file.
"""
-
+
_meta = {}
def __init__(self, thread_synchronize=False):
@@ -157,7 +157,7 @@ class FS(object):
:type thread_synchronize: bool
"""
-
+
super(FS, self).__init__()
self.closed = False
self.thread_synchronize = thread_synchronize
@@ -169,11 +169,11 @@ class FS(object):
def __del__(self):
if not getattr(self, 'closed', True):
self.close()
-
+
def __enter__(self):
return self
-
- def __exit__(self, type, value, traceback):
+
+ def __exit__(self, type, value, traceback):
self.close()
def cachehint(self, enabled):
@@ -188,7 +188,7 @@ class FS(object):
"""
pass
# Deprecating cache_hint in favour of no underscore version, for consistency
- cache_hint = cachehint
+ cache_hint = cachehint
def close(self):
"""Close the filesystem. This will perform any shutdown related
@@ -205,16 +205,16 @@ class FS(object):
# type of lock that should be there. None == no lock,
# True == a proper lock, False == a dummy lock.
state = self.__dict__.copy()
- lock = state.get("_lock",None)
+ lock = state.get("_lock", None)
if lock is not None:
- if isinstance(lock,threading._RLock):
+ if isinstance(lock, threading._RLock):
state["_lock"] = True
else:
state["_lock"] = False
return state
- def __setstate__(self,state):
- self.__dict__.update(state)
+ def __setstate__(self, state):
+ self.__dict__.update(state)
lock = state.get("_lock")
if lock is not None:
if lock:
@@ -254,13 +254,13 @@ class FS(object):
:param default: An option default to return, if the meta value isn't present
:raises `fs.errors.NoMetaError`: If specified meta value is not present, and there is no default
- """
+ """
if meta_name not in self._meta:
if default is not NoDefaultMeta:
return default
- raise NoMetaError(meta_name=meta_name)
+ raise NoMetaError(meta_name=meta_name)
return self._meta[meta_name]
-
+
def hasmeta(self, meta_name):
"""Check that a meta value is supported
@@ -272,7 +272,7 @@ class FS(object):
self.getmeta(meta_name)
except NoMetaError:
return False
- return True
+ return True
def getsyspath(self, path, allow_none=False):
"""Returns the system path (a path recognized by the OS) if one is present.
@@ -302,7 +302,7 @@ class FS(object):
"""
return self.getsyspath(path, allow_none=True) is not None
-
+
def getpathurl(self, path, allow_none=False):
"""Returns a url that corresponds to the given path, if one exists.
@@ -321,7 +321,7 @@ class FS(object):
if not allow_none:
raise NoPathURLError(path=path)
return None
-
+
def haspathurl(self, path):
"""Check if the path has an equivalent URL form
@@ -336,26 +336,39 @@ class FS(object):
"""Open a the given path as a file-like object.
:param path: a path to file that should be opened
+ :type path: string
:param mode: mode of file to open, identical to the mode string used
in 'file' and 'open' builtins
+ :type mode: string
:param kwargs: additional (optional) keyword parameters that may
- be required to open the file
+ be required to open the file
+ :type kwargs: dict
+
:rtype: a file-like object
-
+
+ :raises `fs.errors.ParentDirectoryMissingError`: if an intermediate directory is missing
+ :raises `fs.errors.ResourceInvalidError`: if an intermediate directory is an file
+ :raises `fs.errors.ResourceNotFoundError`: if the path is not found
+
"""
raise UnsupportedError("open file")
def safeopen(self, path, mode="r", **kwargs):
- """Like :py:meth:`~fs.base.FS.open`, but returns a :py:class:`~fs.base.NullFile` if the file could not be opened.
+ """Like :py:meth:`~fs.base.FS.open`, but returns a
+ :py:class:`~fs.base.NullFile` if the file could not be opened.
A ``NullFile`` is a dummy file which has all the methods of a file-like object,
but contains no data.
:param path: a path to file that should be opened
+ :type path: string
:param mode: mode of file to open, identical to the mode string used
in 'file' and 'open' builtins
+ :type mode: string
:param kwargs: additional (optional) keyword parameters that may
- be required to open the file
+ be required to open the file
+ :type kwargs: dict
+
:rtype: a file-like object
"""
@@ -369,8 +382,10 @@ class FS(object):
"""Check if a path references a valid resource.
:param path: A path in the filesystem
+ :type path: string
+
:rtype: bool
-
+
"""
return self.isfile(path) or self.isdir(path)
@@ -378,6 +393,8 @@ class FS(object):
"""Check if a path references a directory.
:param path: a path in the filesystem
+ :type path: string
+
:rtype: bool
"""
@@ -387,6 +404,8 @@ class FS(object):
"""Check if a path references a file.
:param path: a path in the filesystem
+ :type path: string
+
:rtype: bool
"""
@@ -419,10 +438,12 @@ class FS(object):
:type dirs_only: bool
:param files_only: if True, only return files
:type files_only: bool
+
:rtype: iterable of paths
- :raises `fs.errors.ResourceNotFoundError`: if the path is not found
+ :raises `fs.errors.ParentDirectoryMissingError`: if an intermediate directory is missing
:raises `fs.errors.ResourceInvalidError`: if the path exists, but is not a directory
+ :raises `fs.errors.ResourceNotFoundError`: if the path is not found
"""
raise UnsupportedError("list directory")
@@ -460,7 +481,7 @@ class FS(object):
if full or absolute:
return self.getinfo(p)
else:
- return self.getinfo(pathjoin(path,p))
+ return self.getinfo(pathjoin(path, p))
except FSError:
return {}
@@ -493,7 +514,7 @@ class FS(object):
if wildcard is not None:
if not callable(wildcard):
wildcard_re = re.compile(fnmatch.translate(wildcard))
- wildcard = lambda fn:bool (wildcard_re.match(fn))
+ wildcard = lambda fn:bool (wildcard_re.match(fn))
entries = [p for p in entries if wildcard(p)]
if dirs_only:
@@ -553,14 +574,16 @@ class FS(object):
"""Make a directory on the filesystem.
:param path: path of directory
+ :type path: string
:param recursive: if True, any intermediate directories will also be created
:type recursive: bool
:param allow_recreate: if True, re-creating a directory wont be an error
:type allow_create: bool
-
+
:raises `fs.errors.DestinationExistsError`: if the path is already a directory, and allow_recreate is False
:raises `fs.errors.ParentDirectoryMissingError`: if a containing directory is missing and recursive is False
:raises `fs.errors.ResourceInvalidError`: if a path is an existing file
+ :raises `fs.errors.ResourceNotFoundError`: if the path is not found
"""
raise UnsupportedError("make directory")
@@ -569,9 +592,11 @@ class FS(object):
"""Remove a file from the filesystem.
:param path: Path of the resource to remove
+ :type path: string
- :raises `fs.errors.ResourceNotFoundError`: if the path does not exist
+ :raises `fs.errors.ParentDirectoryMissingError`: if an intermediate directory is missing
:raises `fs.errors.ResourceInvalidError`: if the path is a directory
+ :raises `fs.errors.ResourceNotFoundError`: if the path does not exist
"""
raise UnsupportedError("remove resource")
@@ -580,15 +605,17 @@ class FS(object):
"""Remove a directory from the filesystem
:param path: path of the directory to remove
+ :type path: string
:param recursive: if True, empty parent directories will be removed
:type recursive: bool
:param force: if True, any directory contents will be removed
:type force: bool
- :raises `fs.errors.ResourceNotFoundError`: if the path does not exist
- :raises `fs.errors.ResourceInvalidError`: if the path is not a directory
:raises `fs.errors.DirectoryNotEmptyError`: if the directory is not empty and force is False
-
+ :raises `fs.errors.ParentDirectoryMissingError`: if an intermediate directory is missing
+ :raises `fs.errors.ResourceInvalidError`: if the path is not a directory
+ :raises `fs.errors.ResourceNotFoundError`: if the path does not exist
+
"""
raise UnsupportedError("remove directory")
@@ -596,49 +623,66 @@ class FS(object):
"""Renames a file or directory
:param src: path to rename
+ :type src: string
:param dst: new name
-
+ :type dst: string
+
+ :raises ParentDirectoryMissingError: if a containing directory is missing
+ :raises ResourceInvalidError: if the path or a parent path is not a
+ directory or src is a parent of dst or one of src or dst is a dir
+ and the other don't
+ :raises ResourceNotFoundError: if the src path does not exist
+
"""
raise UnsupportedError("rename resource")
@convert_os_errors
def settimes(self, path, accessed_time=None, modified_time=None):
"""Set the accessed time and modified time of a file
-
+
:param path: path to a file
- :param accessed_time: a datetime object the file was accessed (defaults to current time)
- :param modified_time: a datetime object the file was modified (defaults to current time)
-
+ :type path: string
+ :param accessed_time: the datetime the file was accessed (defaults to current time)
+ :type accessed_time: datetime
+ :param modified_time: the datetime the file was modified (defaults to current time)
+ :type modified_time: datetime
+
"""
-
+
sys_path = self.getsyspath(path, allow_none=True)
- if sys_path is not None:
+ if sys_path is not None:
now = datetime.datetime.now()
if accessed_time is None:
accessed_time = now
if modified_time is None:
- modified_time = now
+ modified_time = now
accessed_time = int(time.mktime(accessed_time.timetuple()))
modified_time = int(time.mktime(modified_time.timetuple()))
os.utime(sys_path, (accessed_time, modified_time))
return True
else:
raise UnsupportedError("settimes")
-
+
def getinfo(self, path):
"""Returns information for a path as a dictionary. The exact content of
this dictionary will vary depending on the implementation, but will
likely include a few common values. The following values will be found
in info dictionaries for most implementations:
-
+
* "size" - Number of bytes used to store the file or directory
* "created_time" - A datetime object containing the time the resource was created
* "accessed_time" - A datetime object containing the time the resource was last accessed
* "modified_time" - A datetime object containing the time the resource was modified
:param path: a path to retrieve information for
+ :type path: string
+
:rtype: dict
-
+
+ :raises `fs.errors.ParentDirectoryMissingError`: if an intermediate directory is missing
+ :raises `fs.errors.ResourceInvalidError`: if the path is not a directory
+ :raises `fs.errors.ResourceNotFoundError`: if the path does not exist
+
"""
raise UnsupportedError("get resource info")
@@ -655,9 +699,9 @@ class FS(object):
try:
sys_path = self.getsyspath(path)
except NoSysPathError:
- return "No description available"
+ return "No description available"
return sys_path
-
+
def getcontents(self, path):
"""Returns the contents of a file as a string.
@@ -675,7 +719,7 @@ class FS(object):
if f is not None:
f.close()
- def setcontents(self, path, data, chunk_size=1024*64):
+ def setcontents(self, path, data, chunk_size=1024 * 64):
"""A convenience method to create a new file from a string or file-like object
:param path: a path of the file to create
@@ -683,7 +727,7 @@ class FS(object):
:param chunk_size: Number of bytes to read in a chunk, if the implementation has to resort to a read / copy loop
"""
-
+
if not data:
self.createfile(path)
else:
@@ -704,11 +748,11 @@ class FS(object):
finally:
if f is not None:
f.close()
-
+
def setcontents_async(self,
path,
data,
- chunk_size=1024*64,
+ chunk_size=1024 * 64,
progress_callback=None,
finished_callback=None,
error_callback=None):
@@ -728,18 +772,18 @@ class FS(object):
:returns: An event object that is set when the copy is complete, call
the `wait` method of this object to block until the data is written
- """
-
+ """
+
if progress_callback is None:
- progress_callback = lambda bytes_written:None
-
- def do_setcontents():
- try:
+ progress_callback = lambda bytes_written:None
+
+ def do_setcontents():
+ try:
f = None
try:
f = self.open(path, 'wb')
- progress_callback(0)
-
+ progress_callback(0)
+
if hasattr(data, "read"):
bytes_written = 0
read = data.read
@@ -749,30 +793,30 @@ class FS(object):
write(chunk)
bytes_written += len(chunk)
progress_callback(bytes_written)
- chunk = read(chunk_size)
- else:
- f.write(data)
+ chunk = read(chunk_size)
+ else:
+ f.write(data)
progress_callback(len(data))
-
+
if finished_callback is not None:
finished_callback()
-
+
finally:
if f is not None:
f.close()
-
+
except Exception, e:
if error_callback is not None:
error_callback(e)
-
+
finally:
finished_event.set()
-
- finished_event = threading.Event()
+
+ finished_event = threading.Event()
threading.Thread(target=do_setcontents).start()
return finished_event
-
-
+
+
def createfile(self, path, wipe=False):
"""Creates an empty file if it doesn't exist
@@ -782,23 +826,26 @@ class FS(object):
"""
if not wipe and self.isfile(path):
return
-
+
f = None
try:
f = self.open(path, 'w')
finally:
if f is not None:
f.close()
-
+
def opendir(self, path):
"""Opens a directory and returns a FS object representing its contents.
:param path: path to directory to open
+ :type path: string
+
+ :return: the opened dir
:rtype: an FS object
-
+
"""
-
+
from fs.wrapfs.subfs import SubFS
if not self.exists(path):
raise ResourceNotFoundError(path)
@@ -815,19 +862,23 @@ class FS(object):
contents.
:param path: root path to start walking
+ :type path: string
:param wildcard: if given, only return files that match this wildcard
:type wildcard: a string containing a wildcard (e.g. `*.txt`) or a callable that takes the file path and returns a boolean
:param dir_wildcard: if given, only walk directories that match the wildcard
:type dir_wildcard: a string containing a wildcard (e.g. `*.txt`) or a callable that takes the directory name and returns a boolean
:param search: a string identifying the method used to walk the directories. There are two such methods:
-
+
* ``"breadth"`` yields paths in the top directories first
* ``"depth"`` yields the deepest paths first
-
+
:param ignore_errors: ignore any errors reading the directory
+ :type ignore_errors: bool
+
+ :rtype: iterator of (current_path, paths)
+
+ """
- """
-
def listdir(path, *args, **kwargs):
if ignore_errors:
try:
@@ -836,21 +887,21 @@ class FS(object):
return []
else:
return self.listdir(path, *args, **kwargs)
-
+
if wildcard is None:
wildcard = lambda f:True
elif not callable(wildcard):
wildcard_re = re.compile(fnmatch.translate(wildcard))
wildcard = lambda fn:bool (wildcard_re.match(fn))
-
+
if dir_wildcard is None:
dir_wildcard = lambda f:True
elif not callable(dir_wildcard):
dir_wildcard_re = re.compile(fnmatch.translate(dir_wildcard))
- dir_wildcard = lambda fn:bool (dir_wildcard_re.match(fn))
-
+ dir_wildcard = lambda fn:bool (dir_wildcard_re.match(fn))
+
if search == "breadth":
-
+
dirs = [path]
while dirs:
current_path = dirs.pop()
@@ -858,16 +909,16 @@ class FS(object):
try:
for filename in listdir(current_path):
path = pathjoin(current_path, filename)
- if self.isdir(path):
+ if self.isdir(path):
if dir_wildcard(path):
- dirs.append(path)
- else:
+ dirs.append(path)
+ else:
if wildcard(filename):
paths.append(filename)
except ResourceNotFoundError:
# Could happen if another thread / process deletes something whilst we are walking
pass
-
+
yield (current_path, paths)
elif search == "depth":
@@ -884,7 +935,7 @@ class FS(object):
for p in recurse(path):
yield p
-
+
else:
raise ValueError("Search should be 'breadth' or 'depth'")
@@ -893,17 +944,25 @@ class FS(object):
wildcard=None,
dir_wildcard=None,
search="breadth",
- ignore_errors=False ):
+ ignore_errors=False):
"""Like the 'walk' method, but just yields file paths.
:param path: root path to start walking
+ :type path: string
:param wildcard: if given, only return files that match this wildcard
:type wildcard: A string containing a wildcard (e.g. `*.txt`) or a callable that takes the file path and returns a boolean
:param dir_wildcard: if given, only walk directories that match the wildcard
:type dir_wildcard: A string containing a wildcard (e.g. `*.txt`) or a callable that takes the directory name and returns a boolean
- :param search: same as walk method
+ :param search: a string identifying the method used to walk the directories. There are two such methods:
+
+ * ``"breadth"`` yields paths in the top directories first
+ * ``"depth"`` yields the deepest paths first
+
:param ignore_errors: ignore any errors reading the directory
-
+ :type ignore_errors: bool
+
+ :rtype: iterator of file paths
+
"""
for path, files in self.walk(path, wildcard=wildcard, dir_wildcard=dir_wildcard, search=search, ignore_errors=ignore_errors):
for f in files:
@@ -917,11 +976,19 @@ class FS(object):
"""Like the 'walk' method but yields directories.
:param path: root path to start walking
+ :type path: string
:param wildcard: if given, only return directories that match this wildcard
:type wildcard: A string containing a wildcard (e.g. `*.txt`) or a callable that takes the directory name and returns a boolean
- :param search: same as the walk method
+ :param search: a string identifying the method used to walk the directories. There are two such methods:
+
+ * ``"breadth"`` yields paths in the top directories first
+ * ``"depth"`` yields the deepest paths first
+
:param ignore_errors: ignore any errors reading the directory
-
+ :type ignore_errors: bool
+
+ :rtype: iterator of dir paths
+
"""
for p, _files in self.walk(path, dir_wildcard=wildcard, search=search, ignore_errors=ignore_errors):
yield p
@@ -931,32 +998,38 @@ class FS(object):
"""Returns the size (in bytes) of a resource.
:param path: a path to the resource
- :rtype: integer
+ :type path: string
+
:returns: the size of the file
-
+ :rtype: integer
+
"""
info = self.getinfo(path)
size = info.get('size', None)
if size is None:
raise OperationFailedError("get size of resource", path)
return size
-
- def copy(self, src, dst, overwrite=False, chunk_size=1024*64):
+
+ def copy(self, src, dst, overwrite=False, chunk_size=1024 * 64):
"""Copies a file from src to dst.
:param src: the source path
+ :type src: string
:param dst: the destination path
+ :type dst: string
:param overwrite: if True, then an existing file at the destination may
be overwritten; If False then DestinationExistsError
will be raised.
+ :type overwrite: bool
:param chunk_size: size of chunks to use if a simple copy is required
(defaults to 64K).
-
+ :type chunk_size: bool
+
"""
if not self.isfile(src):
if self.isdir(src):
- raise ResourceInvalidError(src,msg="Source is not a file: %(path)s")
+ raise ResourceInvalidError(src, msg="Source is not a file: %(path)s")
raise ResourceNotFoundError(src)
if not overwrite and self.exists(dst):
raise DestinationExistsError(dst)
@@ -967,50 +1040,51 @@ class FS(object):
if src_syspath is not None and dst_syspath is not None:
self._shutil_copyfile(src_syspath, dst_syspath)
else:
- src_file = None
+ src_file = None
try:
- src_file = self.open(src, "rb")
- self.setcontents(dst, src_file, chunk_size=chunk_size)
+ src_file = self.open(src, "rb")
+ self.setcontents(dst, src_file, chunk_size=chunk_size)
except ResourceNotFoundError:
if self.exists(src) and not self.exists(dirname(dst)):
raise ParentDirectoryMissingError(dst)
finally:
if src_file is not None:
- src_file.close()
-
+ src_file.close()
+
@classmethod
- @convert_os_errors
+ @convert_os_errors
def _shutil_copyfile(cls, src_syspath, dst_syspath):
try:
shutil.copyfile(src_syspath, dst_syspath)
except IOError, e:
# shutil reports ENOENT when a parent directory is missing
- if getattr(e,"errno",None) == 2:
+ if getattr(e, "errno", None) == 2:
if not os.path.exists(dirname(dst_syspath)):
raise ParentDirectoryMissingError(dst_syspath)
raise
-
+
@classmethod
@convert_os_errors
def _shutil_movefile(cls, src_syspath, dst_syspath):
shutil.move(src_syspath, dst_syspath)
-
-
+
+
def move(self, src, dst, overwrite=False, chunk_size=16384):
"""moves a file from one location to another.
:param src: source path
+ :type src: string
:param dst: destination path
- :param overwrite: if True, then an existing file at the destination path
- will be silently overwritten; if False then an exception
- will be raised in this case.
+ :type dst: string
:param overwrite: When True the destination will be overwritten (if it exists),
otherwise a DestinationExistsError will be thrown
:type overwrite: bool
:param chunk_size: Size of chunks to use when copying, if a simple copy
is required
:type chunk_size: integer
-
+
+ :raise `fs.errors.DestinationExistsError`: if destination exists and `overwrite` is False
+
"""
src_syspath = self.getsyspath(src, allow_none=True)
@@ -1032,19 +1106,26 @@ class FS(object):
pass
self.copy(src, dst, overwrite=overwrite, chunk_size=chunk_size)
self.remove(src)
-
+
def movedir(self, src, dst, overwrite=False, ignore_errors=False, chunk_size=16384):
"""moves a directory from one location to another.
:param src: source directory path
+ :type src: string
:param dst: destination directory path
+ :type dst: string
:param overwrite: if True then any existing files in the destination
directory will be overwritten
+ :type overwrite: bool
:param ignore_errors: if True then this method will ignore FSError
exceptions when moving files
+ :type ignore_errors: bool
:param chunk_size: size of chunks to use when copying, if a simple copy
is required
-
+ :type chunk_size: integer
+
+ :raise `fs.errors.DestinationExistsError`: if destination exists and `overwrite` is False
+
"""
if not self.isdir(src):
if self.isfile(src):
@@ -1058,7 +1139,7 @@ class FS(object):
if src_syspath is not None and dst_syspath is not None:
try:
- os.rename(src_syspath,dst_syspath)
+ os.rename(src_syspath, dst_syspath)
return
except OSError:
pass
@@ -1092,12 +1173,14 @@ class FS(object):
movefile(src_filename, dst_filename, overwrite=overwrite, chunk_size=chunk_size)
self.removedir(dirname)
-
+
def copydir(self, src, dst, overwrite=False, ignore_errors=False, chunk_size=16384):
"""copies a directory from one location to another.
:param src: source directory path
+ :type src: string
:param dst: destination directory path
+ :type dst: string
:param overwrite: if True then any existing files in the destination
directory will be overwritten
:type overwrite: bool
@@ -1105,7 +1188,7 @@ class FS(object):
:type ignore_errors: bool
:param chunk_size: size of chunks to use when copying, if a simple copy
is required (defaults to 16K)
-
+
"""
if not self.isdir(src):
raise ResourceInvalidError(src, msg="Source is not a directory: %(path)s")
@@ -1144,8 +1227,9 @@ class FS(object):
"""Check if a directory is empty (contains no files or sub-directories)
:param path: a directory path
+
:rtype: bool
-
+
"""
path = normpath(path)
iter_dir = iter(self.listdir(path))
@@ -1162,6 +1246,9 @@ class FS(object):
:param path: path to the new directory
:param recursive: if True any intermediate directories will be created
+ :return: the opened dir
+ :rtype: an FS object
+
"""
self.makedir(path, allow_recreate=True, recursive=recursive)
@@ -1175,10 +1262,10 @@ class FS(object):
5. Set to None for no limit
"""
- from fs.utils import print_fs
+ from fs.utils import print_fs
print_fs(self, max_levels=max_levels)
tree = printtree
-
+
def browse(self, hide_dotfiles=False):
"""Displays the FS tree in a graphical window (requires wxPython)
@@ -1198,16 +1285,16 @@ class FS(object):
:param copy: If False then changes wont be written back to the file
:raises `fs.errors.NoMMapError`: Only paths that have a syspath can be opened as a mmap
- """
+ """
syspath = self.getsyspath(path, allow_none=True)
if syspath is None:
raise NoMMapError(path)
-
+
try:
- import mmap
+ import mmap
except ImportError:
raise NoMMapError(msg="mmap not supported")
-
+
if read_only:
f = open(syspath, 'rb')
access = mmap.ACCESS_READ
@@ -1217,14 +1304,14 @@ class FS(object):
access = mmap.ACCESS_COPY
else:
f = open(syspath, 'r+b')
- access = mmap.ACCESS_WRITE
-
+ access = mmap.ACCESS_WRITE
+
m = mmap.mmap(f.fileno(), 0, access=access)
return m
def flags_to_mode(flags):
- """Convert an os.O_* flag bitmask into an FS mode string."""
+ """Convert an os.O_* flag bitmask into an FS mode string."""
if flags & os.O_WRONLY:
if flags & os.O_TRUNC:
mode = "w"
@@ -1240,9 +1327,9 @@ def flags_to_mode(flags):
else:
mode = "r+"
else:
- mode = "r"
+ mode = "r"
if flags & os.O_EXCL:
mode += "x"
return mode
-
+
diff --git a/fs/commands/fsls.py b/fs/commands/fsls.py
index b5c1532..a009e7d 100644
--- a/fs/commands/fsls.py
+++ b/fs/commands/fsls.py
@@ -42,8 +42,8 @@ List contents of [PATH]"""
dir_paths = []
file_paths = []
fs_used = set()
- for fs_url in args:
- fs, path = self.open_fs(fs_url)
+ for fs_url in args:
+ fs, path = self.open_fs(fs_url)
fs_used.add(fs)
path = path or '.'
wildcard = None
diff --git a/fs/commands/runner.py b/fs/commands/runner.py
index 736f86a..bf31d24 100644
--- a/fs/commands/runner.py
+++ b/fs/commands/runner.py
@@ -245,7 +245,7 @@ class Command(object):
for col_no, col in enumerate(row):
td = col.ljust(max_row_widths[col_no])
if col_no in col_process:
- td = col_process[col_no](td)
+ td = col_process[col_no](td)
out_col.append(td)
lines.append(self.text_encode('%s\n' % ' '.join(out_col).rstrip()))
self.output(''.join(lines))
diff --git a/fs/expose/serve/server.py b/fs/expose/serve/server.py
index c8082a1..7ac8c1f 100644
--- a/fs/expose/serve/server.py
+++ b/fs/expose/serve/server.py
@@ -19,10 +19,28 @@ class _SocketFile(object):
def write(self, data):
self.socket.sendall(data)
-class ConnectionThread(threading.Thread):
+
+def remote_call(method_name=None):
+ method = method_name
+ def deco(f):
+ if not hasattr(f, '_remote_call_names'):
+ f._remote_call_names = []
+ f._remote_call_names.append(method or f.__name__)
+ return f
+ return deco
+
+
+class RemoteResponse(Exception):
+ def __init__(self, header, payload):
+ self.header = header
+ self.payload = payload
+
+class ConnectionHandlerBase(threading.Thread):
+
+ _methods = {}
def __init__(self, server, connection_id, socket, address):
- super(ConnectionThread, self).__init__()
+ super(ConnectionHandlerBase, self).__init__()
self.server = server
self.connection_id = connection_id
self.socket = socket
@@ -33,6 +51,16 @@ class ConnectionThread(threading.Thread):
self._lock = threading.RLock()
self.socket_error = None
+
+ if not self._methods:
+ for method_name in dir(self):
+ method = getattr(self, method_name)
+ if callable(method) and hasattr(method, '_remote_call_names'):
+ for name in method._remote_call_names:
+
+ self._methods[name] = method
+
+ print self._methods
self.fs = None
@@ -70,17 +98,37 @@ class ConnectionThread(threading.Thread):
def on_packet(self, header, payload):
print '-' * 30
print repr(header)
- print repr(payload)
-
- if header['method'] == 'ping':
- self.encoder.write({'client_ref':header['client_ref']}, payload)
-
+ print repr(payload)
+ if header['type'] == 'rpc':
+ method = header['method']
+ args = header['args']
+ kwargs = header['kwargs']
+ method_callable = self._methods[method]
+ remote = dict(type='rpcresult',
+ client_ref = header['client_ref'])
+ try:
+ response = method_callable(*args, **kwargs)
+ remote['response'] = response
+ self.encoder.write(remote, '')
+ except RemoteResponse, response:
+ self.encoder.write(response.header, response.payload)
+class RemoteFSConnection(ConnectionHandlerBase):
+
+ @remote_call()
+ def auth(self, username, password, resource):
+ self.username = username
+ self.password = password
+ self.resource = resource
+ from fs.memoryfs import MemoryFS
+ self.fs = MemoryFS()
+
class Server(object):
- def __init__(self, addr='', port=3000):
+ def __init__(self, addr='', port=3000, connection_factory=RemoteFSConnection):
self.addr = addr
self.port = port
+ self.connection_factory = connection_factory
self.socket = None
self.connection_id = 0
self.threads = {}
@@ -124,10 +172,10 @@ class Server(object):
print "Connection from", address
with self._lock:
self.connection_id += 1
- thread = ConnectionThread(self,
- self.connection_id,
- clientsocket,
- address)
+ thread = self.connection_factory(self,
+ self.connection_id,
+ clientsocket,
+ address)
self.threads[self.connection_id] = thread
thread.start()
diff --git a/fs/expose/sftp.py b/fs/expose/sftp.py
index df4a806..7872c9c 100644
--- a/fs/expose/sftp.py
+++ b/fs/expose/sftp.py
@@ -245,7 +245,10 @@ class SFTPRequestHandler(sockserv.StreamRequestHandler):
t.start_server(server=self.server)
-class BaseSFTPServer(sockserv.TCPServer,paramiko.ServerInterface):
+class ThreadedTCPServer(sockserv.TCPServer, sockserv.ThreadingMixIn):
+ pass
+
+class BaseSFTPServer(ThreadedTCPServer, paramiko.ServerInterface):
"""SocketServer.TCPServer subclass exposing an FS via SFTP.
BaseSFTPServer combines a simple SocketServer.TCPServer subclass with an
@@ -318,6 +321,7 @@ if __name__ == "__main__":
from fs.tempfs import TempFS
server = BaseSFTPServer(("localhost",8022),TempFS())
try:
+ #import rpdb2; rpdb2.start_embedded_debugger('password')
server.serve_forever()
except (SystemExit,KeyboardInterrupt):
server.server_close()
diff --git a/fs/ftpfs.py b/fs/ftpfs.py
index 5743ad2..b5b53bb 100644
--- a/fs/ftpfs.py
+++ b/fs/ftpfs.py
@@ -1209,7 +1209,7 @@ class FTPFS(FS):
@ftperrors
def listdir(self, path="./", wildcard=None, full=False, absolute=False, dirs_only=False, files_only=False):
path = normpath(path)
- self.clear_dircache(path)
+ #self.clear_dircache(path)
if not self.exists(path):
raise ResourceNotFoundError(path)
if not self.isdir(path):
diff --git a/fs/opener.py b/fs/opener.py
index aec90d5..d8b4bbc 100644
--- a/fs/opener.py
+++ b/fs/opener.py
@@ -174,7 +174,7 @@ class OpenerRegistry(object):
for name in opener.names:
self.registry[name] = index
- def parse(self, fs_url, default_fs_name=None, writeable=False, create_dir=False):
+ def parse(self, fs_url, default_fs_name=None, writeable=False, create_dir=False, cache_hint=True):
"""Parses a FS url and returns an fs object a path within that FS object
(if indicated in the path). A tuple of (<FS instance>, <path>) is returned.
@@ -216,7 +216,8 @@ class OpenerRegistry(object):
if fs_url is None:
raise OpenerError("Unable to parse '%s'" % orig_url)
- fs, fs_path = opener.get_fs(self, fs_name, fs_name_params, fs_url, writeable, create_dir)
+ fs, fs_path = opener.get_fs(self, fs_name, fs_name_params, fs_url, writeable, create_dir)
+ fs.cache_hint(cache_hint)
if fs_path and iswildcard(fs_path):
pathname, resourcename = pathsplit(fs_path or '')
@@ -432,7 +433,7 @@ examples:
dirpath, resourcepath = pathsplit(path)
url = netloc
-
+
ftpfs = FTPFS(url, user=username or '', passwd=password or '')
ftpfs.cache_hint(True)
diff --git a/fs/path.py b/fs/path.py
index 88b2e24..dbe32ad 100644
--- a/fs/path.py
+++ b/fs/path.py
@@ -237,6 +237,7 @@ def splitext(path):
path = pathjoin(parent_path, pathname)
return path, '.' + ext
+
def isdotfile(path):
"""Detects if a path references a dot file, i.e. a resource who's name
starts with a '.'
@@ -255,6 +256,7 @@ def isdotfile(path):
"""
return basename(path).startswith('.')
+
def dirname(path):
"""Returns the parent directory of a path.
@@ -265,12 +267,16 @@ def dirname(path):
>>> dirname('foo/bar/baz')
'foo/bar'
+
+ >>> dirname('/foo/bar')
+ '/foo'
+
+ >>> dirname('/foo')
+ '/'
"""
- if '/' not in path:
- return ''
- return path.rsplit('/', 1)[0]
-
+ return pathsplit(path)[0]
+
def basename(path):
"""Returns the basename of the resource referenced by a path.
@@ -290,9 +296,7 @@ def basename(path):
''
"""
- if '/' not in path:
- return path
- return path.rsplit('/', 1)[-1]
+ return pathsplit(path)[1]
def issamedir(path1, path2):
@@ -309,11 +313,13 @@ def issamedir(path1, path2):
"""
return dirname(normpath(path1)) == dirname(normpath(path2))
+
def isbase(path1, path2):
p1 = forcedir(abspath(path1))
p2 = forcedir(abspath(path2))
return p1 == p2 or p1.startswith(p2)
+
def isprefix(path1, path2):
"""Return true is path1 is a prefix of path2.
@@ -341,6 +347,7 @@ def isprefix(path1, path2):
return False
return True
+
def forcedir(path):
"""Ensure the path ends with a trailing /
diff --git a/fs/remotefs.py b/fs/remotefs.py
index dedbb49..c44179b 100644
--- a/fs/remotefs.py
+++ b/fs/remotefs.py
@@ -1,5 +1,5 @@
# Work in Progress - Do not use
-
+from __future__ import with_statement
from fs.base import FS
from fs.expose.serve import packetstream
@@ -107,26 +107,60 @@ class _SocketFile(object):
def close(self):
self.socket.shutdown(socket.SHUT_RDWR)
self.socket.close()
-
+
+
+class _RemoteFile(object):
+
+ def __init__(self, path, connection):
+ self.path = path
+ self.connection = connection
class RemoteFS(FS):
- def __init__(self, addr='', port=3000, transport=None):
+ _meta = { 'thead_safe' : True,
+ 'network' : True,
+ 'virtual' : False,
+ 'read_only' : False,
+ 'unicode_paths' : True,
+ }
+
+ def __init__(self, addr='', port=3000, username=None, password=None, resource=None, transport=None):
self.addr = addr
self.port = port
+ self.username = None
+ self.password = None
+ self.resource = None
self.transport = transport
if self.transport is None:
self.transport = self._open_connection()
self.packet_handler = PacketHandler(self.transport)
self.packet_handler.start()
+ self._remote_call('auth',
+ username=username,
+ password=password,
+ resource=resource)
+
def _open_connection(self):
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect((self.addr, self.port))
socket_file = _SocketFile(sock)
socket_file.write('pyfs/0.1\n')
return socket_file
+
+ def _make_call(self, method_name, *args, **kwargs):
+ call = dict(type='rpc',
+ method=method_name,
+ args=args,
+ kwargs=kwargs)
+ return call
+ def _remote_call(self, method_name, *args, **kwargs):
+ call = self._make_call(method_name, *args, **kwargs)
+ call_id = self.packet_handler.send_packet(call)
+ header, payload = self.packet_handler.get_packet(call_id)
+ return header, payload
+
def ping(self, msg):
call_id = self.packet_handler.send_packet({'type':'rpc', 'method':'ping'}, msg)
header, payload = self.packet_handler.get_packet(call_id)
@@ -137,11 +171,18 @@ class RemoteFS(FS):
def close(self):
self.transport.close()
self.packet_handler.join()
+
+ def open(self, path, mode="r", **kwargs):
+ pass
+
+ def exists(self, path):
+ remote = self._remote_call('exists', path)
+ return remote.get('response')
+
if __name__ == "__main__":
- rfs = RemoteFS()
- rfs.ping("Hello, World!")
+ rfs = RemoteFS()
rfs.close()
\ No newline at end of file
diff --git a/fs/sftpfs.py b/fs/sftpfs.py
index 03f4b20..9bab5fc 100644
--- a/fs/sftpfs.py
+++ b/fs/sftpfs.py
@@ -139,10 +139,10 @@ class SFTPFS(FS):
if not connection.is_active():
raise RemoteConnectionError(msg='Unable to connect')
- if no_auth:
+ if no_auth:
try:
connection.auth_none('')
- except paramiko.SSHException:
+ except paramiko.SSHException:
pass
elif not connection.is_authenticated():
@@ -222,6 +222,8 @@ class SFTPFS(FS):
@property
@synchronize
def client(self):
+ if self.closed:
+ return None
client = getattr(self._tlocal, 'client', None)
if client is None:
if self._transport is None:
@@ -242,9 +244,10 @@ class SFTPFS(FS):
def close(self):
"""Close the connection to the remote server."""
if not self.closed:
- if self.client:
- self.client.close()
- if self._owns_transport and self._transport:
+ self._tlocal = None
+ #if self.client:
+ # self.client.close()
+ if self._owns_transport and self._transport and self._transport.is_active:
self._transport.close()
self.closed = True
diff --git a/fs/tests/test_expose.py b/fs/tests/test_expose.py
index 5d5085a..eb859be 100644
--- a/fs/tests/test_expose.py
+++ b/fs/tests/test_expose.py
@@ -29,6 +29,22 @@ class TestRPCFS(unittest.TestCase, FSTestCases, ThreadingTestCases):
port = 3000
self.temp_fs = TempFS()
self.server = None
+
+ self.serve_more_requests = True
+ self.server_thread = threading.Thread(target=self.runServer)
+ self.server_thread.setDaemon(True)
+
+ self.start_event = threading.Event()
+ self.end_event = threading.Event()
+
+ self.server_thread.start()
+
+ self.start_event.wait()
+
+ def runServer(self):
+ """Run the server, swallowing shutdown-related execptions."""
+
+ port = 3000
while not self.server:
try:
self.server = self.makeServer(self.temp_fs,("127.0.0.1",port))
@@ -37,24 +53,26 @@ class TestRPCFS(unittest.TestCase, FSTestCases, ThreadingTestCases):
port += 1
else:
raise
- self.server_addr = ("127.0.0.1",port)
- self.serve_more_requests = True
- self.server_thread = threading.Thread(target=self.runServer)
- self.server_thread.daemon = True
- self.server_thread.start()
-
- def runServer(self):
- """Run the server, swallowing shutdown-related execptions."""
- if sys.platform != "win32":
- try:
- self.server.socket.settimeout(0.1)
- except socket.error:
- pass
+ self.server_addr = ("127.0.0.1", port)
+
+ self.server.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+
+# if sys.platform != "win32":
+# try:
+# self.server.socket.settimeout(1)
+# except socket.error:
+# pass
+#
+ self.start_event.set()
+
try:
+ #self.server.serve_forever()
while self.serve_more_requests:
self.server.handle_request()
except Exception, e:
pass
+
+ self.end_event.set()
def setUp(self):
self.startServer()
@@ -62,12 +80,21 @@ class TestRPCFS(unittest.TestCase, FSTestCases, ThreadingTestCases):
def tearDown(self):
self.serve_more_requests = False
+ #self.server.socket.close()
+# self.server.socket.shutdown(socket.SHUT_RDWR)
+# self.server.socket.close()
+# self.temp_fs.close()
+ #self.server_thread.join()
+
+ #self.end_event.wait()
+ #return
+
try:
self.bump()
self.server.server_close()
except Exception:
pass
- self.server_thread.join()
+ #self.server_thread.join()
self.temp_fs.close()
def bump(self):
diff --git a/fs/tests/test_path.py b/fs/tests/test_path.py
index 75332e4..639a4cc 100644
--- a/fs/tests/test_path.py
+++ b/fs/tests/test_path.py
@@ -105,7 +105,6 @@ class TestPathFunctions(unittest.TestCase):
self.assertEquals(recursepath("hello",reverse=True),["/hello","/"])
self.assertEquals(recursepath("",reverse=True),["/"])
-
def test_isdotfile(self):
for path in ['.foo',
'.svn',
@@ -125,7 +124,9 @@ class TestPathFunctions(unittest.TestCase):
tests = [('foo', ''),
('foo/bar', 'foo'),
('foo/bar/baz', 'foo/bar'),
- ('/', '')]
+ ('/foo/bar', '/foo'),
+ ('/foo', '/'),
+ ('/', '/')]
for path, test_dirname in tests:
self.assertEqual(dirname(path), test_dirname)