Commit 4dff3320 by willmcgugan

Changed syntax for commands to be more url like, optimized sftps to use fewer queries for listdir

parent f1f224c1
from fs.opener import opener from fs.opener import opener
from fs.utils import copyfile, copystructure from fs.utils import copyfile, copystructure
from fs.path import pathjoin from fs.path import pathjoin, iswildcard
from fs.errors import FSError from fs.errors import FSError
from fs.commands.runner import Command from fs.commands.runner import Command
import sys import sys
...@@ -92,7 +92,7 @@ Copy SOURCE to DESTINATION""" ...@@ -92,7 +92,7 @@ Copy SOURCE to DESTINATION"""
if src_path is None: if src_path is None:
src_path = '/' src_path = '/'
if self.is_wildcard(src_path): if iswildcard(src_path):
for file_path in src_fs.listdir(wildcard=src_path, full=True): for file_path in src_fs.listdir(wildcard=src_path, full=True):
copy_fs_paths.append((self.FILE, src_fs, file_path, file_path)) copy_fs_paths.append((self.FILE, src_fs, file_path, file_path))
......
#!/usr/bin/env python #!/usr/bin/env python
from fs.opener import opener from fs.opener import opener
from fs.path import pathsplit, abspath, isdotfile from fs.path import pathsplit, abspath, isdotfile, iswildcard
from fs.commands.runner import Command from fs.commands.runner import Command
from collections import defaultdict from collections import defaultdict
import sys import sys
...@@ -43,10 +43,10 @@ List contents of [PATH]""" ...@@ -43,10 +43,10 @@ List contents of [PATH]"""
path = path or '.' path = path or '.'
wildcard = None wildcard = None
if self.is_wildcard(path): if iswildcard(path):
path, wildcard = pathsplit(path) path, wildcard = pathsplit(path)
if fs.isfile(path): if path != '.' and fs.isfile(path):
if not options.dirsonly: if not options.dirsonly:
file_paths.append(path) file_paths.append(path)
else: else:
......
...@@ -14,7 +14,9 @@ Recursively display the contents of PATH in an ascii tree""" ...@@ -14,7 +14,9 @@ Recursively display the contents of PATH in an ascii tree"""
def get_optparse(self): def get_optparse(self):
optparse = super(FSTree, self).get_optparse() optparse = super(FSTree, self).get_optparse()
optparse.add_option('-d', '--depth', dest='depth', type="int", default=5, optparse.add_option('-d', '--depth', dest='depth', type="int", default=5,
help="Maximum depth to display", metavar="DEPTH") help="Maximum depth to display", metavar="DEPTH")
optparse.add_option('-a', '--all', dest='all', action='store_true', default=False,
help="do not hide dot files")
return optparse return optparse
def do_run(self, options, args): def do_run(self, options, args):
...@@ -31,7 +33,8 @@ Recursively display the contents of PATH in an ascii tree""" ...@@ -31,7 +33,8 @@ Recursively display the contents of PATH in an ascii tree"""
print_fs(fs, path or '', print_fs(fs, path or '',
file_out=self.output_file, file_out=self.output_file,
max_levels=options.depth, max_levels=options.depth,
terminal_colors=self.is_terminal()) terminal_colors=self.is_terminal(),
hide_dotfiles=not options.all)
def run(): def run():
return FSTree().run() return FSTree().run()
......
...@@ -2,7 +2,7 @@ import sys ...@@ -2,7 +2,7 @@ import sys
from optparse import OptionParser from optparse import OptionParser
from fs.opener import opener, OpenerError from fs.opener import opener, OpenerError
from fs.errors import FSError from fs.errors import FSError
from fs.path import splitext, pathsplit, isdotfile from fs.path import splitext, pathsplit, isdotfile, iswildcard
import platform import platform
from collections import defaultdict from collections import defaultdict
...@@ -55,11 +55,6 @@ class Command(object): ...@@ -55,11 +55,6 @@ class Command(object):
self.terminal_width = w self.terminal_width = w
self.name = self.__class__.__name__.lower() self.name = self.__class__.__name__.lower()
def is_wildcard(self, path):
if path is None:
return False
return '*' in path or '?' in path
def is_terminal(self): def is_terminal(self):
try: try:
return self.output_file.isatty() return self.output_file.isatty()
...@@ -111,7 +106,7 @@ class Command(object): ...@@ -111,7 +106,7 @@ class Command(object):
if path is None: if path is None:
return [], [] return [], []
pathname, resourcename = pathsplit(path) pathname, resourcename = pathsplit(path)
if self.is_wildcard(resourcename): if iswildcard(resourcename):
dir_paths = fs.listdir(pathname, dir_paths = fs.listdir(pathname,
wildcard=resourcename, wildcard=resourcename,
absolute=True, absolute=True,
...@@ -137,7 +132,7 @@ class Command(object): ...@@ -137,7 +132,7 @@ class Command(object):
resources = [] resources = []
for fs, path in fs_paths: for fs, path in fs_paths:
if self.is_wildcard(path): if path and iswildcard(path):
if not files_only: if not files_only:
dir_paths = fs.listdir(wildcard=path, dirs_only=True) dir_paths = fs.listdir(wildcard=path, dirs_only=True)
for path in dir_paths: for path in dir_paths:
...@@ -227,8 +222,8 @@ class Command(object): ...@@ -227,8 +222,8 @@ class Command(object):
if self.is_terminal(): if self.is_terminal():
self.output("\n") self.output("\n")
return 0 return 0
except ValueError: #except ValueError:
pass # pass
except SystemExit: except SystemExit:
return 0 return 0
except IOError: except IOError:
......
...@@ -751,8 +751,7 @@ class FTPFS(FS): ...@@ -751,8 +751,7 @@ class FTPFS(FS):
def __init__(self, host='', user='', passwd='', acct='', timeout=_GLOBAL_DEFAULT_TIMEOUT, def __init__(self, host='', user='', passwd='', acct='', timeout=_GLOBAL_DEFAULT_TIMEOUT,
port=21, port=21,
dircache=True, dircache=True):
max_buffer_size=128*1024*1024):
""" Connect to a FTP server. """ Connect to a FTP server.
:param host: Host to connect to :param host: Host to connect to
...@@ -765,8 +764,7 @@ class FTPFS(FS): ...@@ -765,8 +764,7 @@ class FTPFS(FS):
which will speed up operations such as getinfo, isdi, isfile, but which will speed up operations such as getinfo, isdi, isfile, but
changes to the ftp file structure will not be visible until changes to the ftp file structure will not be visible until
`~fs.ftpfs.FTPFS.clear_dircache` is called `~fs.ftpfs.FTPFS.clear_dircache` is called
:param dircache: If True directory information will be cached for fast access :param dircache: If True directory information will be cached for fast access
:param max_buffer_size: Number of bytes to hold before blocking write operations
""" """
...@@ -780,9 +778,7 @@ class FTPFS(FS): ...@@ -780,9 +778,7 @@ class FTPFS(FS):
self.timeout = timeout self.timeout = timeout
self.use_dircache = dircache self.use_dircache = dircache
self.get_dircache() self.get_dircache()
self.max_buffer_size = max_buffer_size
self._cache_hint = False self._cache_hint = False
self._locals._ftp = None self._locals._ftp = None
......
...@@ -156,7 +156,7 @@ class DirEntry(object): ...@@ -156,7 +156,7 @@ class DirEntry(object):
self.locks += 1 self.locks += 1
def unlock(self): def unlock(self):
self.locks -=1 self.locks -= 1
assert self.locks >=0, "Lock / Unlock mismatch!" assert self.locks >=0, "Lock / Unlock mismatch!"
def desc_contents(self): def desc_contents(self):
...@@ -494,7 +494,7 @@ class MemoryFS(FS): ...@@ -494,7 +494,7 @@ class MemoryFS(FS):
if dir_entry is None: if dir_entry is None:
raise ResourceNotFoundError(path) raise ResourceNotFoundError(path)
if dir_entry.isfile(): if dir_entry.isfile():
raise ResourceInvalidError(path,msg="that's a file, not a directory: %(path)s") raise ResourceInvalidError(path, msg="not a directory: %(path)s")
paths = dir_entry.contents.keys() paths = dir_entry.contents.keys()
for (i,p) in enumerate(paths): for (i,p) in enumerate(paths):
if not isinstance(p,unicode): if not isinstance(p,unicode):
...@@ -522,7 +522,7 @@ class MemoryFS(FS): ...@@ -522,7 +522,7 @@ class MemoryFS(FS):
return info return info
@synchronize @synchronize
def copydir(self, src, dst, overwrite=False, ignore_errors=False, chunk_size=16384): def copydir(self, src, dst, overwrite=False, ignore_errors=False, chunk_size=1024*64):
src_dir_entry = self._get_dir_entry(src) src_dir_entry = self._get_dir_entry(src)
if src_dir_entry is None: if src_dir_entry is None:
raise ResourceNotFoundError(src) raise ResourceNotFoundError(src)
...@@ -533,7 +533,7 @@ class MemoryFS(FS): ...@@ -533,7 +533,7 @@ class MemoryFS(FS):
dst_dir_entry.xattrs.update(src_xattrs) dst_dir_entry.xattrs.update(src_xattrs)
@synchronize @synchronize
def movedir(self, src, dst, overwrite=False, ignore_errors=False, chunk_size=16384): def movedir(self, src, dst, overwrite=False, ignore_errors=False, chunk_size=1024*64):
src_dir_entry = self._get_dir_entry(src) src_dir_entry = self._get_dir_entry(src)
if src_dir_entry is None: if src_dir_entry is None:
raise ResourceNotFoundError(src) raise ResourceNotFoundError(src)
...@@ -544,7 +544,7 @@ class MemoryFS(FS): ...@@ -544,7 +544,7 @@ class MemoryFS(FS):
dst_dir_entry.xattrs.update(src_xattrs) dst_dir_entry.xattrs.update(src_xattrs)
@synchronize @synchronize
def copy(self, src, dst, overwrite=False, chunk_size=16384): def copy(self, src, dst, overwrite=False, chunk_size=1024*64):
src_dir_entry = self._get_dir_entry(src) src_dir_entry = self._get_dir_entry(src)
if src_dir_entry is None: if src_dir_entry is None:
raise ResourceNotFoundError(src) raise ResourceNotFoundError(src)
...@@ -555,7 +555,7 @@ class MemoryFS(FS): ...@@ -555,7 +555,7 @@ class MemoryFS(FS):
dst_dir_entry.xattrs.update(src_xattrs) dst_dir_entry.xattrs.update(src_xattrs)
@synchronize @synchronize
def move(self, src, dst, overwrite=False, chunk_size=16384): def move(self, src, dst, overwrite=False, chunk_size=1024*64):
src_dir_entry = self._get_dir_entry(src) src_dir_entry = self._get_dir_entry(src)
if src_dir_entry is None: if src_dir_entry is None:
raise ResourceNotFoundError(src) raise ResourceNotFoundError(src)
...@@ -566,6 +566,27 @@ class MemoryFS(FS): ...@@ -566,6 +566,27 @@ class MemoryFS(FS):
dst_dir_entry.xattrs.update(src_xattrs) dst_dir_entry.xattrs.update(src_xattrs)
@synchronize @synchronize
def getcontents(self, path):
dir_entry = self._get_dir_entry(path)
if dir_entry is None:
raise ResourceNotFoundError(path)
if not dir_entry.isfile():
raise ResourceInvalidError(path, msg="not a directory: %(path)s")
return dir_entry.data or ''
@synchronize
def setcontents(self, path, data, chunk_size=1024*64):
if not isinstance(data, str):
return super(MemoryFS, self).setcontents(path, data, chunk_size)
if not self.exists(path):
self.open(path, 'w').close()
dir_entry = self._get_dir_entry(path)
if not dir_entry.isfile():
raise ResourceInvalidError('Not a directory %(path)s', path)
dir_entry.data = data
@synchronize
def setxattr(self, path, key, value): def setxattr(self, path, key, value):
dir_entry = self._dir_entry(path) dir_entry = self._dir_entry(path)
key = unicode(key) key = unicode(key)
......
import sys import sys
from fs.osfs import OSFS from fs.osfs import OSFS
from fs.path import pathsplit from fs.path import pathsplit, basename, join, iswildcard
import os
import os.path import os.path
import re import re
from urlparse import urlparse
class OpenerError(Exception): class OpenerError(Exception):
pass pass
...@@ -31,11 +33,15 @@ def _expand_syspath(path): ...@@ -31,11 +33,15 @@ def _expand_syspath(path):
return path return path
class OpenerRegistry(object): class OpenerRegistry(object):
re_fs_url = re.compile(r''' re_fs_url = re.compile(r'''
^ ^
(?:\[(.*?)\])* (.*?)
:\/\/
(?: (?:
\((.*?)\) \((.*?)\)
...@@ -46,6 +52,8 @@ class OpenerRegistry(object): ...@@ -46,6 +52,8 @@ class OpenerRegistry(object):
\+(.*?)$ \+(.*?)$
)*$ )*$
''', re.VERBOSE) ''', re.VERBOSE)
def __init__(self, openers=[]): def __init__(self, openers=[]):
self.registry = {} self.registry = {}
...@@ -56,13 +64,12 @@ class OpenerRegistry(object): ...@@ -56,13 +64,12 @@ class OpenerRegistry(object):
@classmethod @classmethod
def split_segments(self, fs_url): def split_segments(self, fs_url):
match = self.re_fs_url.match(fs_url) match = self.re_fs_url.match(fs_url)
assert match is not None, "broken re?" return match
return match.groups()
def get_opener(self, name): def get_opener(self, name):
if name not in self.registry: if name not in self.registry:
raise NoOpenerError("No opener for [%s]" % name) raise NoOpenerError("No opener for %s" % name)
index = self.registry[name] index = self.registry[name]
return self.openers[index] return self.openers[index]
...@@ -73,34 +80,56 @@ class OpenerRegistry(object): ...@@ -73,34 +80,56 @@ class OpenerRegistry(object):
self.registry[name] = index self.registry[name] = index
def parse(self, fs_url, default_fs_name=None, writeable=False, create=False): def parse(self, fs_url, default_fs_name=None, writeable=False, create=False):
fs_name, paren_url, fs_url, path = self.split_segments(fs_url) orig_url = fs_url
match = self.split_segments(fs_url)
fs_url = fs_url or paren_url if match:
if fs_name is None and path is None: fs_name, paren_url, fs_url, path = match.groups()
fs_url = os.path.expanduser(os.path.expandvars(fs_url)) fs_url = fs_url or paren_url or ''
fs_url = os.path.normpath(os.path.abspath(fs_url)) if ':' in fs_name:
fs_url, path = pathsplit(fs_url) fs_name, sub_protocol = fs_name.split(':', 1)
if not fs_url: fs_url = '%s://%s' % (sub_protocol, fs_url)
fs_url = '/'
fs_name = fs_name or self.default_opener
else:
fs_name = default_fs_name or self.default_opener
fs_url = _expand_syspath(fs_url)
path = ''
fs_name, fs_name_params = self.parse_name(fs_name)
opener = self.get_opener(fs_name)
fs_name = fs_name or self.default_opener if fs_url is None:
raise OpenerError("Unable to parse '%s'" % orig_url)
if fs_name is None: wildcard = None
fs_name = fs_default_name if iswildcard(fs_url):
fs_url, wildcard = pathsplit(fs_url)
fs_name, fs_name_params = self.parse_name(fs_name) fs, fs_path = opener.get_fs(self, fs_name, fs_name_params, fs_url, writeable, create)
opener = self.get_opener(fs_name)
if wildcard:
fs = opener.get_fs(self, fs_name, fs_name_params, fs_url, writeable, create) fs_path = join(fs_path or '', wildcard)
else:
path = join(fs_path or '', path)
if path: if path:
pathname, resourcename = pathsplit(path) pathname, resourcename = pathsplit(path)
if pathname: if pathname:
fs = fs.opendir(pathname) fs = fs.opendir(pathname)
path = resourcename path = resourcename
if not iswildcard(path):
if fs.isdir(path):
fs = fs.opendir(path)
fs_path = ''
else:
fs_path = path
return fs, path return fs, fs_path
def parse_credentials(self, url): def parse_credentials(self, url):
...@@ -145,11 +174,21 @@ class OSFSOpener(Opener): ...@@ -145,11 +174,21 @@ class OSFSOpener(Opener):
names = ['osfs', 'file'] names = ['osfs', 'file']
@classmethod @classmethod
def get_fs(cls, registry, fs_name, fs_name_params, fs_path, writeable, create): def get_fs(cls, registry, fs_name, fs_name_params, fs_path, writeable, create):
username, password, fs_path = registry.parse_credentials(fs_path) from fs.osfs import OSFS
from fs.osfs import OSFS username, password, fs_path = registry.parse_credentials(fs_path)
osfs = OSFS(fs_path, create=create)
return osfs
path = _expand_syspath(fs_path)
if create:
sys.makedirs(fs_path)
if os.path.isdir(path):
osfs = OSFS(path)
filepath = None
else:
path, filepath = pathsplit(path)
osfs = OSFS(path, create=create)
return osfs, filepath
class ZipOpener(Opener): class ZipOpener(Opener):
...@@ -157,22 +196,21 @@ class ZipOpener(Opener): ...@@ -157,22 +196,21 @@ class ZipOpener(Opener):
@classmethod @classmethod
def get_fs(cls, registry, fs_name, fs_name_params, fs_path, writeable, create): def get_fs(cls, registry, fs_name, fs_name_params, fs_path, writeable, create):
create_zip = fs_name_params == 'new' append_zip = fs_name_params == 'add'
append_zip = fs_name_params == 'append'
zip_file = None
if fs_path.startswith('['):
container_fs, container_path = registry.parse(fs_path)
if not container_path:
raise OpenerError("Not a file")
container_mode = 'r+b'
if create_zip:
container_mode = 'w+b'
elif writeable:
container_mode = 'w+b'
zip_file = container_fs.open(container_path, mode=container_mode) zip_fs, zip_path = registry.parse(fs_path)
if zip_path is None:
raise OpenerError('File required for zip opener')
if create:
open_mode = 'wb'
if append_zip:
open_mode = 'r+b'
else:
open_mode = 'rb'
zip_file = zip_fs.open(zip_path, mode=open_mode)
username, password, fs_path = registry.parse_credentials(fs_path) username, password, fs_path = registry.parse_credentials(fs_path)
...@@ -182,21 +220,18 @@ class ZipOpener(Opener): ...@@ -182,21 +220,18 @@ class ZipOpener(Opener):
if append_zip: if append_zip:
mode = 'a' mode = 'a'
elif create_zip or create: elif create:
mode = 'w' mode = 'w'
else: else:
if writeable: if writeable:
mode = 'w' mode = 'w'
else: else:
mode = 'a' mode = 'a'
if fs_name == 'zip64': allow_zip_64 = fs_name == 'zip64'
allow_zip_64 = True
else:
allow_zip_64 = False
zipfs = ZipFS(zip_file, mode=mode, allow_zip_64=allow_zip_64) zipfs = ZipFS(zip_file, mode=mode, allow_zip_64=allow_zip_64)
return zipfs return zipfs, None
class RPCOpener(Opener): class RPCOpener(Opener):
names = ['rpc'] names = ['rpc']
...@@ -206,9 +241,16 @@ class RPCOpener(Opener): ...@@ -206,9 +241,16 @@ class RPCOpener(Opener):
from fs.rpcfs import RPCFS from fs.rpcfs import RPCFS
username, password, fs_path = registry.parse_credentials(fs_path) username, password, fs_path = registry.parse_credentials(fs_path)
if not fs_path.startswith('http://'): if not fs_path.startswith('http://'):
fs_path = 'http://' + fs_path fs_path = 'http://' + fs_path
rpcfs = RPCFS(fs_path)
return rpcfs scheme, netloc, path, params, query, fragment = urlparse(fs_path)
rpcfs = RPCFS('%s://%s' % (scheme, netloc))
if create and path:
rpcfs.makedir(path, recursive=True, allow_recreate=True)
return rpcfs, path or None
class FTPOpener(Opener): class FTPOpener(Opener):
names = ['ftp'] names = ['ftp']
...@@ -217,22 +259,28 @@ class FTPOpener(Opener): ...@@ -217,22 +259,28 @@ class FTPOpener(Opener):
def get_fs(cls, registry, fs_name, fs_name_params, fs_path, writeable, create): def get_fs(cls, registry, fs_name, fs_name_params, fs_path, writeable, create):
from fs.ftpfs import FTPFS from fs.ftpfs import FTPFS
username, password, fs_path = registry.parse_credentials(fs_path) username, password, fs_path = registry.parse_credentials(fs_path)
if '/' in fs_path: scheme, netloc, path, params, query, fragment = urlparse(fs_path)
url, root_path = fs_path.split('/', 1) if not scheme:
else: fs_path = 'ftp://' + fs_path
url = fs_path scheme, netloc, path, params, query, fragment = urlparse(fs_path)
root_path = ''
dirpath, resourcepath = pathsplit(path)
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)
if root_path not in ('', '/'): if create and path:
if not ftpfs.isdir(root_path): ftpfs.makedir(path, recursive=True, allow_recreate=True)
raise OpenerError("'%s' is not a directory on the server" % root_path)
return ftpfs.opendir(root_path)
return ftpfs if dirpath:
ftpfs = ftpfs.opendir(dirpath)
if not resourcepath:
return ftpfs, None
else:
return ftpfs, resourcepath
class SFTPOpener(Opener): class SFTPOpener(Opener):
...@@ -256,6 +304,8 @@ class SFTPOpener(Opener): ...@@ -256,6 +304,8 @@ class SFTPOpener(Opener):
addr = fs_path addr = fs_path
fs_path = '/' fs_path = '/'
fs_path, resourcename = pathsplit(fs_path)
host = addr host = addr
port = None port = None
if ':' in host: if ':' in host:
...@@ -265,10 +315,25 @@ class SFTPOpener(Opener): ...@@ -265,10 +315,25 @@ class SFTPOpener(Opener):
except ValueError: except ValueError:
pass pass
else: else:
host = (addr, port) host = (addr, port)
#if not username or not password:
# raise OpenerError('SFTP requires authentication')
if create:
sftpfs = SFTPFS(host, root_path='/', **credentials)
if not sftpfs._transport.is_authenticated():
sftpfs.close()
raise OpenerError('SFTP requires authentication')
sftpfs = sfspfs.makeopendir(fs_path)
return sftpfs, None
sftpfs = SFTPFS(host, root_path=fs_path, **credentials) sftpfs = SFTPFS(host, root_path=fs_path, **credentials)
return sftpfs if not sftpfs._transport.is_authenticated():
sftpfs.close()
raise OpenerError('SFTP requires authentication')
return sftpfs, resourcename
class MemOpener(Opener): class MemOpener(Opener):
...@@ -277,7 +342,10 @@ class MemOpener(Opener): ...@@ -277,7 +342,10 @@ class MemOpener(Opener):
@classmethod @classmethod
def get_fs(cls, registry, fs_name, fs_name_params, fs_path, writeable, create): def get_fs(cls, registry, fs_name, fs_name_params, fs_path, writeable, create):
from fs.memoryfs import MemoryFS from fs.memoryfs import MemoryFS
return MemoryFS() memfs = MemoryFS()
if create:
memfs = memfs.makeopendir(fs_path)
return memfs, None
class DebugOpener(Opener): class DebugOpener(Opener):
names = ['debug'] names = ['debug']
...@@ -287,13 +355,13 @@ class DebugOpener(Opener): ...@@ -287,13 +355,13 @@ class DebugOpener(Opener):
from fs.wrapfs.debugfs import DebugFS from fs.wrapfs.debugfs import DebugFS
if fs_path: if fs_path:
fs, path = registry.parse(fs_path, writeable=writeable, create=create) fs, path = registry.parse(fs_path, writeable=writeable, create=create)
return DebugFS(fs, verbose=False) return DebugFS(fs, verbose=False), None
if fs_name_params == 'ram': if fs_name_params == 'ram':
from fs.memoryfs import MemoryFS from fs.memoryfs import MemoryFS
return DebugFS(MemoryFS(), identifier=fs_name_params, verbose=False) return DebugFS(MemoryFS(), identifier=fs_name_params, verbose=False), None
else: else:
from fs.tempfs import TempFS from fs.tempfs import TempFS
return DebugFS(TempFS(), identifier=fs_name_params, verbose=False) return DebugFS(TempFS(), identifier=fs_name_params, verbose=False), None
class TempOpener(Opener): class TempOpener(Opener):
names = ['temp'] names = ['temp']
...@@ -301,7 +369,7 @@ class TempOpener(Opener): ...@@ -301,7 +369,7 @@ class TempOpener(Opener):
@classmethod @classmethod
def get_fs(cls, registry, fs_name, fs_name_params, fs_path, writeable, create): def get_fs(cls, registry, fs_name, fs_name_params, fs_path, writeable, create):
from fs.tempfs import TempFS from fs.tempfs import TempFS
return TempFS(identifier=fs_name_params, temp_dir=fs_path) return TempFS(identifier=fs_name_params, temp_dir=fs_path), None
opener = OpenerRegistry([OSFSOpener, opener = OpenerRegistry([OSFSOpener,
...@@ -317,7 +385,7 @@ opener = OpenerRegistry([OSFSOpener, ...@@ -317,7 +385,7 @@ opener = OpenerRegistry([OSFSOpener,
def main(): def main():
fs, path = opener.parse('galleries.zip') fs, path = opener.parse('sftp://willmcgugan.com')
print fs, path print fs, path
if __name__ == "__main__": if __name__ == "__main__":
......
...@@ -515,3 +515,16 @@ class PathMap(object): ...@@ -515,3 +515,16 @@ class PathMap(object):
def names(self,root="/"): def names(self,root="/"):
return list(self.iternames(root)) return list(self.iternames(root))
_wild_chars = frozenset('*?[]!{}')
def iswildcard(path):
"""Check if a path ends with a wildcard
>>> is_wildcard('foo/bar/baz.*')
True
>>> is_wildcard('foo/bar')
False
"""
assert path is not None
base_chars = frozenset(basename(path))
return not base_chars.isdisjoint(_wild_chars)
...@@ -9,12 +9,15 @@ Filesystem accessing an SFTP server (via paramiko) ...@@ -9,12 +9,15 @@ Filesystem accessing an SFTP server (via paramiko)
import datetime import datetime
import stat as statinfo import stat as statinfo
import threading import threading
import os
import paramiko import paramiko
from getpass import getuser
from binascii import hexlify
from fs.base import * from fs.base import *
from fs.path import * from fs.path import *
from fs.errors import * from fs.errors import *
from fs.utils import isdir, isfile
# SFTPClient appears to not be thread-safe, so we use an instance per thread # SFTPClient appears to not be thread-safe, so we use an instance per thread
if hasattr(threading, "local"): if hasattr(threading, "local"):
...@@ -58,6 +61,7 @@ class SFTPFS(FS): ...@@ -58,6 +61,7 @@ class SFTPFS(FS):
'atomic.setcontents' : False 'atomic.setcontents' : False
} }
def __init__(self, connection, root_path="/", encoding=None, **credentials): def __init__(self, connection, root_path="/", encoding=None, **credentials):
"""SFTPFS constructor. """SFTPFS constructor.
...@@ -88,18 +92,81 @@ class SFTPFS(FS): ...@@ -88,18 +92,81 @@ class SFTPFS(FS):
self._tlocal = thread_local() self._tlocal = thread_local()
self._transport = None self._transport = None
self._client = None self._client = None
hostname = None
if isinstance(connection, basestring):
hostname = connection
else:
try:
hostname, port = connection
except ValueError:
pass
hostkeytype = None
hostkey = None
if hostname is not None:
try:
host_keys = paramiko.util.load_host_keys(os.path.expanduser('~/.ssh/known_hosts'))
except IOError:
try:
# try ~/ssh/ too, because windows can't have a folder named ~/.ssh/
host_keys = paramiko.util.load_host_keys(os.path.expanduser('~/ssh/known_hosts'))
except IOError:
host_keys = {}
if host_keys.has_key(hostname):
hostkeytype = host_keys[hostname].keys()[0]
hostkey = host_keys[hostname][hostkeytype]
credentials['hostkey'] = hostkey
if not credentials.get('username'):
credentials['username'] = getuser()
super(SFTPFS, self).__init__()
if isinstance(connection,paramiko.Channel): if isinstance(connection,paramiko.Channel):
self._transport = None self._transport = None
self._client = paramiko.SFTPClient(connection) self._client = paramiko.SFTPClient(connection)
else: else:
if not isinstance(connection,paramiko.Transport): if not isinstance(connection, paramiko.Transport):
connection = paramiko.Transport(connection) connection = paramiko.Transport(connection)
self._owns_transport = True self._owns_transport = True
if not connection.is_authenticated(): try:
connection.connect(**credentials) if not connection.is_authenticated():
connection.connect(**credentials)
if not connection.is_authenticated():
self._agent_auth(connection, credentials.get('username'))
if not connection.is_authenticated():
connection.close()
raise RemoteConnectionError('No auth')
except paramiko.AuthenticationException:
raise RemoteConnectionError('Auth rejected')
self._transport = connection self._transport = connection
self.root_path = abspath(normpath(root_path)) self.root_path = abspath(normpath(root_path))
super(SFTPFS, self).__init__()
@classmethod
def _agent_auth(cls, transport, username):
"""
Attempt to authenticate to the given transport using any of the private
keys available from an SSH agent.
"""
agent = paramiko.Agent()
agent_keys = agent.get_keys()
if len(agent_keys) == 0:
return False
for key in agent_keys:
try:
transport.auth_publickey(username, key)
return key
except paramiko.SSHException:
pass
return None
def __del__(self): def __del__(self):
self.close() self.close()
...@@ -184,6 +251,8 @@ class SFTPFS(FS): ...@@ -184,6 +251,8 @@ class SFTPFS(FS):
@convert_os_errors @convert_os_errors
def isdir(self,path): def isdir(self,path):
if path == '/':
return True
npath = self._normpath(path) npath = self._normpath(path)
try: try:
stat = self.client.stat(npath) stat = self.client.stat(npath)
...@@ -209,6 +278,10 @@ class SFTPFS(FS): ...@@ -209,6 +278,10 @@ class SFTPFS(FS):
npath = self._normpath(path) npath = self._normpath(path)
try: try:
paths = self.client.listdir(npath) paths = self.client.listdir(npath)
if dirs_only or files_only:
path_attrs = self.client.listdir_attr(npath)
else:
path_attrs = None
except IOError, e: except IOError, e:
if getattr(e,"errno",None) == 2: if getattr(e,"errno",None) == 2:
if self.isfile(path): if self.isfile(path):
...@@ -217,10 +290,72 @@ class SFTPFS(FS): ...@@ -217,10 +290,72 @@ class SFTPFS(FS):
elif self.isfile(path): elif self.isfile(path):
raise ResourceInvalidError(path,msg="Can't list directory contents of a file: %(path)s") raise ResourceInvalidError(path,msg="Can't list directory contents of a file: %(path)s")
raise raise
if path_attrs is not None:
if dirs_only:
filter_paths = []
for path, attr in zip(paths, path_attrs):
if isdir(self, path, attr.__dict__):
filter_paths.append(path)
paths = filter_paths
elif files_only:
filter_paths = []
for path, attr in zip(paths, path_attrs):
if isfile(self, path, attr.__dict__):
filter_paths.append(path)
paths = filter_paths
for (i,p) in enumerate(paths): for (i,p) in enumerate(paths):
if not isinstance(p,unicode): if not isinstance(p,unicode):
paths[i] = p.decode(self.encoding) paths[i] = p.decode(self.encoding)
return self._listdir_helper(path, paths, wildcard, full, absolute, dirs_only, files_only) return self._listdir_helper(path, paths, wildcard, full, absolute, False, False)
@convert_os_errors
def listdirinfo(self,path="./",wildcard=None,full=False,absolute=False,dirs_only=False,files_only=False):
npath = self._normpath(path)
try:
paths = self.client.listdir(npath)
attrs = self.client.listdir_attr(npath)
attrs_map = dict(zip(paths, attrs))
except IOError, e:
if getattr(e,"errno",None) == 2:
if self.isfile(path):
raise ResourceInvalidError(path,msg="Can't list directory contents of a file: %(path)s")
raise ResourceNotFoundError(path)
elif self.isfile(path):
raise ResourceInvalidError(path,msg="Can't list directory contents of a file: %(path)s")
raise
if dirs_only:
filter_paths = []
for path, attr in zip(paths, attrs):
if isdir(self, path, attr.__dict__):
filter_paths.append(path)
paths = filter_paths
elif files_only:
filter_paths = []
for path, attr in zip(paths, attrs):
if isfile(self, path, attr.__dict__):
filter_paths.append(path)
paths = filter_paths
for (i, p) in enumerate(paths):
if not isinstance(p, unicode):
paths[i] = p.decode(self.encoding)
def getinfo(p):
resourcename = basename(p)
info = attrs_map.get(resourcename)
if info is None:
return self.getinfo(pathjoin(path, p))
return self._extract_info(info.__dict__)
return [(p, getinfo(p)) for p in
self._listdir_helper(path, paths, wildcard, full, absolute, False, False)]
@convert_os_errors @convert_os_errors
def makedir(self,path,recursive=False,allow_recreate=False): def makedir(self,path,recursive=False,allow_recreate=False):
...@@ -335,6 +470,23 @@ class SFTPFS(FS): ...@@ -335,6 +470,23 @@ class SFTPFS(FS):
raise ParentDirectoryMissingError(dst,msg="Destination directory does not exist: %(path)s") raise ParentDirectoryMissingError(dst,msg="Destination directory does not exist: %(path)s")
raise raise
_info_vars = frozenset('st_size st_uid st_gid st_mode st_atime st_mtime'.split())
@classmethod
def _extract_info(cls, stats):
fromtimestamp = datetime.datetime.fromtimestamp
info = dict((k, v) for k, v in stats.iteritems() if k in cls._info_vars)
info['size'] = info['st_size']
ct = info.get('st_ctime')
if ct is not None:
info['created_time'] = fromtimestamp(ct)
at = info.get('st_atime')
if at is not None:
info['accessed_time'] = fromtimestamp(at)
mt = info.get('st_mtime')
if mt is not None:
info['modified_time'] = fromtimestamp(mt)
return info
@convert_os_errors @convert_os_errors
def getinfo(self, path): def getinfo(self, path):
npath = self._normpath(path) npath = self._normpath(path)
......
...@@ -56,10 +56,13 @@ def copyfile(src_fs, src_path, dst_fs, dst_path, overwrite=True, chunk_size=64*1 ...@@ -56,10 +56,13 @@ def copyfile(src_fs, src_path, dst_fs, dst_path, overwrite=True, chunk_size=64*1
src = None src = None
try: try:
# Chunk copy # Chunk copy
src = src_fs.open(src_path, 'rb') if src_fs.getsize(src_path) < chunk_size:
src = src_fs.getcontents(src_path)
else:
src = src_fs.open(src_path, 'rb')
dst_fs.setcontents(dst_path, src, chunk_size=chunk_size) dst_fs.setcontents(dst_path, src, chunk_size=chunk_size)
finally: finally:
if src is not None: if src is not None and hasattr(src, 'close'):
src.close() src.close()
...@@ -89,14 +92,18 @@ def movefile(src_fs, src_path, dst_fs, dst_path, overwrite=True, chunk_size=64*1 ...@@ -89,14 +92,18 @@ def movefile(src_fs, src_path, dst_fs, dst_path, overwrite=True, chunk_size=64*1
FS._shutil_movefile(src_syspath, dst_syspath) FS._shutil_movefile(src_syspath, dst_syspath)
return return
src = None
try:
src = src_fs.open(src_path, 'rb')
dst_fs.setcontents(dst_path, src, chunk_size=chunk_size)
src_fs.remove(src_path)
src = None
try:
# Chunk copy
if src_fs.getsize(src_path) < chunk_size:
src = src_fs.getcontents(src_path)
else:
src = src_fs.open(src_path, 'rb')
dst_fs.setcontents(dst_path, src, chunk_size=chunk_size)
src_fs.remove(src_path)
finally: finally:
if src is not None: if src is not None and hasattr(src, 'close'):
src.close() src.close()
def movedir(fs1, fs2, overwrite=False, ignore_errors=False, chunk_size=64*1024): def movedir(fs1, fs2, overwrite=False, ignore_errors=False, chunk_size=64*1024):
...@@ -324,7 +331,7 @@ def find_duplicates(fs, ...@@ -324,7 +331,7 @@ def find_duplicates(fs,
paths = list(set(paths).difference(dups)) paths = list(set(paths).difference(dups))
def print_fs(fs, path='/', max_levels=5, file_out=None, terminal_colors=None): def print_fs(fs, path='/', max_levels=5, file_out=None, terminal_colors=None, hide_dotfiles=False):
"""Prints a filesystem listing to stdout (including sub dirs). Useful as a debugging aid. """Prints a filesystem listing to stdout (including sub dirs). Useful as a debugging aid.
Be careful about printing a OSFS, or any other large filesystem. Be careful about printing a OSFS, or any other large filesystem.
Without max_levels set, this function will traverse the entire directory tree. Without max_levels set, this function will traverse the entire directory tree.
...@@ -343,13 +350,14 @@ def print_fs(fs, path='/', max_levels=5, file_out=None, terminal_colors=None): ...@@ -343,13 +350,14 @@ def print_fs(fs, path='/', max_levels=5, file_out=None, terminal_colors=None):
:param file_out: File object to write output to (defaults to sys.stdout) :param file_out: File object to write output to (defaults to sys.stdout)
:param terminal_colors: If True, terminal color codes will be written, set to False for non-console output. :param terminal_colors: If True, terminal color codes will be written, set to False for non-console output.
The default (None) will select an appropriate setting for the platform. The default (None) will select an appropriate setting for the platform.
:param hide_dotfiles: if True, files or directories begining with '.' will be removed
""" """
if file_out is None: if file_out is None:
file_out = sys.stdout file_out = sys.stdout
file_encoding = getattr(file_out, 'encoding', 'utf-8') file_encoding = getattr(file_out, 'encoding', 'utf-8') or 'utf-8'
if terminal_colors is None: if terminal_colors is None:
if sys.platform.startswith('win'): if sys.platform.startswith('win'):
...@@ -388,12 +396,16 @@ def print_fs(fs, path='/', max_levels=5, file_out=None, terminal_colors=None): ...@@ -388,12 +396,16 @@ def print_fs(fs, path='/', max_levels=5, file_out=None, terminal_colors=None):
def print_dir(fs, path, levels=[]): def print_dir(fs, path, levels=[]):
try: try:
dir_listing = [(fs.isdir(pathjoin(path,p)), p) for p in fs.listdir(path)] dir_listing = ( [(True, p) for p in fs.listdir(path, dirs_only=True)] +
[(False, p) for p in fs.listdir(path, files_only=True)] )
except Exception, e: except Exception, e:
prefix = ''.join([('| ', ' ')[last] for last in levels]) + ' ' prefix = ''.join([('| ', ' ')[last] for last in levels]) + ' '
write(wrap_prefix(prefix[:-1] + ' ') + wrap_error("unabled to retrieve directory list (%s) ..." % str(e))) write(wrap_prefix(prefix[:-1] + ' ') + wrap_error("unabled to retrieve directory list (%s) ..." % str(e)))
return 0 return 0
if hide_dotfiles:
dir_listing = [(isdir, p) for isdir, p in dir_listing if not p.startswith('.')]
dir_listing.sort(key = lambda (isdir, p):(not isdir, p.lower())) dir_listing.sort(key = lambda (isdir, p):(not isdir, p.lower()))
for i, (is_dir, item) in enumerate(dir_listing): for i, (is_dir, item) in enumerate(dir_listing):
......
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