Commit c56e8ae6 by willmcgugan

Fixes and documentation

parent 462723b6
Concepts 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 Sandboxing
---------- ----------
...@@ -25,7 +25,9 @@ We can open the `foo` directory with the following code:: ...@@ -25,7 +25,9 @@ We can open the `foo` directory with the following code::
from fs.osfs import OSFS from fs.osfs import OSFS
foo_fs = OSFS('foo') 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') bar_fs = foo_fs.opendir('bar')
...@@ -54,7 +56,7 @@ When working with paths in FS objects, keep in mind the following: ...@@ -54,7 +56,7 @@ When working with paths in FS objects, keep in mind the following:
* A double dot means 'previous directory' * 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. 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. There are many helpful functions for working with paths in the :mod:`fs.path` module.
......
...@@ -13,14 +13,22 @@ The easiest way to install PyFilesystem is with `easy_install <http://peak.telec ...@@ -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:: Add the -U switch if you want to upgrade a previous installation::
easy_install -U fs 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 svn checkout http://pyfilesystem.googlecode.com/svn/trunk/ pyfilesystem-read-only
cd pyfilesystem-read-only cd pyfilesystem-read-only
python setup.py install 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 >>> import fs
>>> fs.__version__ >>> fs.__version__
......
...@@ -42,8 +42,8 @@ List contents of [PATH]""" ...@@ -42,8 +42,8 @@ List contents of [PATH]"""
dir_paths = [] dir_paths = []
file_paths = [] file_paths = []
fs_used = set() fs_used = set()
for fs_url in args: for fs_url in args:
fs, path = self.open_fs(fs_url) fs, path = self.open_fs(fs_url)
fs_used.add(fs) fs_used.add(fs)
path = path or '.' path = path or '.'
wildcard = None wildcard = None
......
...@@ -245,7 +245,7 @@ class Command(object): ...@@ -245,7 +245,7 @@ class Command(object):
for col_no, col in enumerate(row): for col_no, col in enumerate(row):
td = col.ljust(max_row_widths[col_no]) td = col.ljust(max_row_widths[col_no])
if col_no in col_process: if col_no in col_process:
td = col_process[col_no](td) td = col_process[col_no](td)
out_col.append(td) out_col.append(td)
lines.append(self.text_encode('%s\n' % ' '.join(out_col).rstrip())) lines.append(self.text_encode('%s\n' % ' '.join(out_col).rstrip()))
self.output(''.join(lines)) self.output(''.join(lines))
......
...@@ -19,10 +19,28 @@ class _SocketFile(object): ...@@ -19,10 +19,28 @@ class _SocketFile(object):
def write(self, data): def write(self, data):
self.socket.sendall(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): def __init__(self, server, connection_id, socket, address):
super(ConnectionThread, self).__init__() super(ConnectionHandlerBase, self).__init__()
self.server = server self.server = server
self.connection_id = connection_id self.connection_id = connection_id
self.socket = socket self.socket = socket
...@@ -33,6 +51,16 @@ class ConnectionThread(threading.Thread): ...@@ -33,6 +51,16 @@ class ConnectionThread(threading.Thread):
self._lock = threading.RLock() self._lock = threading.RLock()
self.socket_error = None 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 self.fs = None
...@@ -70,17 +98,37 @@ class ConnectionThread(threading.Thread): ...@@ -70,17 +98,37 @@ class ConnectionThread(threading.Thread):
def on_packet(self, header, payload): def on_packet(self, header, payload):
print '-' * 30 print '-' * 30
print repr(header) print repr(header)
print repr(payload) print repr(payload)
if header['type'] == 'rpc':
if header['method'] == 'ping': method = header['method']
self.encoder.write({'client_ref':header['client_ref']}, payload) 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): class Server(object):
def __init__(self, addr='', port=3000): def __init__(self, addr='', port=3000, connection_factory=RemoteFSConnection):
self.addr = addr self.addr = addr
self.port = port self.port = port
self.connection_factory = connection_factory
self.socket = None self.socket = None
self.connection_id = 0 self.connection_id = 0
self.threads = {} self.threads = {}
...@@ -124,10 +172,10 @@ class Server(object): ...@@ -124,10 +172,10 @@ class Server(object):
print "Connection from", address print "Connection from", address
with self._lock: with self._lock:
self.connection_id += 1 self.connection_id += 1
thread = ConnectionThread(self, thread = self.connection_factory(self,
self.connection_id, self.connection_id,
clientsocket, clientsocket,
address) address)
self.threads[self.connection_id] = thread self.threads[self.connection_id] = thread
thread.start() thread.start()
......
...@@ -245,7 +245,10 @@ class SFTPRequestHandler(sockserv.StreamRequestHandler): ...@@ -245,7 +245,10 @@ class SFTPRequestHandler(sockserv.StreamRequestHandler):
t.start_server(server=self.server) 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. """SocketServer.TCPServer subclass exposing an FS via SFTP.
BaseSFTPServer combines a simple SocketServer.TCPServer subclass with an BaseSFTPServer combines a simple SocketServer.TCPServer subclass with an
...@@ -318,6 +321,7 @@ if __name__ == "__main__": ...@@ -318,6 +321,7 @@ if __name__ == "__main__":
from fs.tempfs import TempFS from fs.tempfs import TempFS
server = BaseSFTPServer(("localhost",8022),TempFS()) server = BaseSFTPServer(("localhost",8022),TempFS())
try: try:
#import rpdb2; rpdb2.start_embedded_debugger('password')
server.serve_forever() server.serve_forever()
except (SystemExit,KeyboardInterrupt): except (SystemExit,KeyboardInterrupt):
server.server_close() server.server_close()
......
...@@ -1209,7 +1209,7 @@ class FTPFS(FS): ...@@ -1209,7 +1209,7 @@ class FTPFS(FS):
@ftperrors @ftperrors
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) path = normpath(path)
self.clear_dircache(path) #self.clear_dircache(path)
if not self.exists(path): if not self.exists(path):
raise ResourceNotFoundError(path) raise ResourceNotFoundError(path)
if not self.isdir(path): if not self.isdir(path):
......
...@@ -174,7 +174,7 @@ class OpenerRegistry(object): ...@@ -174,7 +174,7 @@ class OpenerRegistry(object):
for name in opener.names: for name in opener.names:
self.registry[name] = index 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 """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. (if indicated in the path). A tuple of (<FS instance>, <path>) is returned.
...@@ -216,7 +216,8 @@ class OpenerRegistry(object): ...@@ -216,7 +216,8 @@ class OpenerRegistry(object):
if fs_url is None: if fs_url is None:
raise OpenerError("Unable to parse '%s'" % orig_url) 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): if fs_path and iswildcard(fs_path):
pathname, resourcename = pathsplit(fs_path or '') pathname, resourcename = pathsplit(fs_path or '')
...@@ -432,7 +433,7 @@ examples: ...@@ -432,7 +433,7 @@ examples:
dirpath, resourcepath = pathsplit(path) dirpath, resourcepath = pathsplit(path)
url = netloc url = netloc
ftpfs = FTPFS(url, user=username or '', passwd=password or '') ftpfs = FTPFS(url, user=username or '', passwd=password or '')
ftpfs.cache_hint(True) ftpfs.cache_hint(True)
......
...@@ -237,6 +237,7 @@ def splitext(path): ...@@ -237,6 +237,7 @@ def splitext(path):
path = pathjoin(parent_path, pathname) path = pathjoin(parent_path, pathname)
return path, '.' + ext return path, '.' + ext
def isdotfile(path): def isdotfile(path):
"""Detects if a path references a dot file, i.e. a resource who's name """Detects if a path references a dot file, i.e. a resource who's name
starts with a '.' starts with a '.'
...@@ -255,6 +256,7 @@ def isdotfile(path): ...@@ -255,6 +256,7 @@ def isdotfile(path):
""" """
return basename(path).startswith('.') return basename(path).startswith('.')
def dirname(path): def dirname(path):
"""Returns the parent directory of a path. """Returns the parent directory of a path.
...@@ -265,12 +267,16 @@ def dirname(path): ...@@ -265,12 +267,16 @@ def dirname(path):
>>> dirname('foo/bar/baz') >>> dirname('foo/bar/baz')
'foo/bar' 'foo/bar'
>>> dirname('/foo/bar')
'/foo'
>>> dirname('/foo')
'/'
""" """
if '/' not in path: return pathsplit(path)[0]
return ''
return path.rsplit('/', 1)[0]
def basename(path): def basename(path):
"""Returns the basename of the resource referenced by a path. """Returns the basename of the resource referenced by a path.
...@@ -290,9 +296,7 @@ def basename(path): ...@@ -290,9 +296,7 @@ def basename(path):
'' ''
""" """
if '/' not in path: return pathsplit(path)[1]
return path
return path.rsplit('/', 1)[-1]
def issamedir(path1, path2): def issamedir(path1, path2):
...@@ -309,11 +313,13 @@ def issamedir(path1, path2): ...@@ -309,11 +313,13 @@ def issamedir(path1, path2):
""" """
return dirname(normpath(path1)) == dirname(normpath(path2)) return dirname(normpath(path1)) == dirname(normpath(path2))
def isbase(path1, path2): def isbase(path1, path2):
p1 = forcedir(abspath(path1)) p1 = forcedir(abspath(path1))
p2 = forcedir(abspath(path2)) p2 = forcedir(abspath(path2))
return p1 == p2 or p1.startswith(p2) return p1 == p2 or p1.startswith(p2)
def isprefix(path1, path2): def isprefix(path1, path2):
"""Return true is path1 is a prefix of path2. """Return true is path1 is a prefix of path2.
...@@ -341,6 +347,7 @@ def isprefix(path1, path2): ...@@ -341,6 +347,7 @@ def isprefix(path1, path2):
return False return False
return True return True
def forcedir(path): def forcedir(path):
"""Ensure the path ends with a trailing / """Ensure the path ends with a trailing /
......
# Work in Progress - Do not use # Work in Progress - Do not use
from __future__ import with_statement
from fs.base import FS from fs.base import FS
from fs.expose.serve import packetstream from fs.expose.serve import packetstream
...@@ -107,26 +107,60 @@ class _SocketFile(object): ...@@ -107,26 +107,60 @@ class _SocketFile(object):
def close(self): def close(self):
self.socket.shutdown(socket.SHUT_RDWR) self.socket.shutdown(socket.SHUT_RDWR)
self.socket.close() self.socket.close()
class _RemoteFile(object):
def __init__(self, path, connection):
self.path = path
self.connection = connection
class RemoteFS(FS): 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.addr = addr
self.port = port self.port = port
self.username = None
self.password = None
self.resource = None
self.transport = transport self.transport = transport
if self.transport is None: if self.transport is None:
self.transport = self._open_connection() self.transport = self._open_connection()
self.packet_handler = PacketHandler(self.transport) self.packet_handler = PacketHandler(self.transport)
self.packet_handler.start() self.packet_handler.start()
self._remote_call('auth',
username=username,
password=password,
resource=resource)
def _open_connection(self): def _open_connection(self):
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect((self.addr, self.port)) sock.connect((self.addr, self.port))
socket_file = _SocketFile(sock) socket_file = _SocketFile(sock)
socket_file.write('pyfs/0.1\n') socket_file.write('pyfs/0.1\n')
return socket_file 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): def ping(self, msg):
call_id = self.packet_handler.send_packet({'type':'rpc', 'method':'ping'}, msg) call_id = self.packet_handler.send_packet({'type':'rpc', 'method':'ping'}, msg)
header, payload = self.packet_handler.get_packet(call_id) header, payload = self.packet_handler.get_packet(call_id)
...@@ -137,11 +171,18 @@ class RemoteFS(FS): ...@@ -137,11 +171,18 @@ class RemoteFS(FS):
def close(self): def close(self):
self.transport.close() self.transport.close()
self.packet_handler.join() 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__": if __name__ == "__main__":
rfs = RemoteFS() rfs = RemoteFS()
rfs.ping("Hello, World!")
rfs.close() rfs.close()
\ No newline at end of file
...@@ -139,10 +139,10 @@ class SFTPFS(FS): ...@@ -139,10 +139,10 @@ class SFTPFS(FS):
if not connection.is_active(): if not connection.is_active():
raise RemoteConnectionError(msg='Unable to connect') raise RemoteConnectionError(msg='Unable to connect')
if no_auth: if no_auth:
try: try:
connection.auth_none('') connection.auth_none('')
except paramiko.SSHException: except paramiko.SSHException:
pass pass
elif not connection.is_authenticated(): elif not connection.is_authenticated():
...@@ -222,6 +222,8 @@ class SFTPFS(FS): ...@@ -222,6 +222,8 @@ class SFTPFS(FS):
@property @property
@synchronize @synchronize
def client(self): def client(self):
if self.closed:
return None
client = getattr(self._tlocal, 'client', None) client = getattr(self._tlocal, 'client', None)
if client is None: if client is None:
if self._transport is None: if self._transport is None:
...@@ -242,9 +244,10 @@ class SFTPFS(FS): ...@@ -242,9 +244,10 @@ class SFTPFS(FS):
def close(self): def close(self):
"""Close the connection to the remote server.""" """Close the connection to the remote server."""
if not self.closed: if not self.closed:
if self.client: self._tlocal = None
self.client.close() #if self.client:
if self._owns_transport and self._transport: # self.client.close()
if self._owns_transport and self._transport and self._transport.is_active:
self._transport.close() self._transport.close()
self.closed = True self.closed = True
......
...@@ -29,6 +29,22 @@ class TestRPCFS(unittest.TestCase, FSTestCases, ThreadingTestCases): ...@@ -29,6 +29,22 @@ class TestRPCFS(unittest.TestCase, FSTestCases, ThreadingTestCases):
port = 3000 port = 3000
self.temp_fs = TempFS() self.temp_fs = TempFS()
self.server = None 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: while not self.server:
try: try:
self.server = self.makeServer(self.temp_fs,("127.0.0.1",port)) self.server = self.makeServer(self.temp_fs,("127.0.0.1",port))
...@@ -37,24 +53,26 @@ class TestRPCFS(unittest.TestCase, FSTestCases, ThreadingTestCases): ...@@ -37,24 +53,26 @@ class TestRPCFS(unittest.TestCase, FSTestCases, ThreadingTestCases):
port += 1 port += 1
else: else:
raise raise
self.server_addr = ("127.0.0.1",port) self.server_addr = ("127.0.0.1", port)
self.serve_more_requests = True
self.server_thread = threading.Thread(target=self.runServer) self.server.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
self.server_thread.daemon = True
self.server_thread.start() # if sys.platform != "win32":
# try:
def runServer(self): # self.server.socket.settimeout(1)
"""Run the server, swallowing shutdown-related execptions.""" # except socket.error:
if sys.platform != "win32": # pass
try: #
self.server.socket.settimeout(0.1) self.start_event.set()
except socket.error:
pass
try: try:
#self.server.serve_forever()
while self.serve_more_requests: while self.serve_more_requests:
self.server.handle_request() self.server.handle_request()
except Exception, e: except Exception, e:
pass pass
self.end_event.set()
def setUp(self): def setUp(self):
self.startServer() self.startServer()
...@@ -62,12 +80,21 @@ class TestRPCFS(unittest.TestCase, FSTestCases, ThreadingTestCases): ...@@ -62,12 +80,21 @@ class TestRPCFS(unittest.TestCase, FSTestCases, ThreadingTestCases):
def tearDown(self): def tearDown(self):
self.serve_more_requests = False 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: try:
self.bump() self.bump()
self.server.server_close() self.server.server_close()
except Exception: except Exception:
pass pass
self.server_thread.join() #self.server_thread.join()
self.temp_fs.close() self.temp_fs.close()
def bump(self): def bump(self):
......
...@@ -105,7 +105,6 @@ class TestPathFunctions(unittest.TestCase): ...@@ -105,7 +105,6 @@ class TestPathFunctions(unittest.TestCase):
self.assertEquals(recursepath("hello",reverse=True),["/hello","/"]) self.assertEquals(recursepath("hello",reverse=True),["/hello","/"])
self.assertEquals(recursepath("",reverse=True),["/"]) self.assertEquals(recursepath("",reverse=True),["/"])
def test_isdotfile(self): def test_isdotfile(self):
for path in ['.foo', for path in ['.foo',
'.svn', '.svn',
...@@ -125,7 +124,9 @@ class TestPathFunctions(unittest.TestCase): ...@@ -125,7 +124,9 @@ class TestPathFunctions(unittest.TestCase):
tests = [('foo', ''), tests = [('foo', ''),
('foo/bar', 'foo'), ('foo/bar', 'foo'),
('foo/bar/baz', 'foo/bar'), ('foo/bar/baz', 'foo/bar'),
('/', '')] ('/foo/bar', '/foo'),
('/foo', '/'),
('/', '/')]
for path, test_dirname in tests: for path, test_dirname in tests:
self.assertEqual(dirname(path), test_dirname) self.assertEqual(dirname(path), test_dirname)
......
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