#!/usr/bin/env python # # pip install fusepy # # This implementation could improve a /lot/, but the core library is missing # some functionality (#213) to make that easy. Additionally it needs a set of # Python bindings for FUSE that stupidly require use of a thread pool. from __future__ import absolute_import, division from __future__ import unicode_literals import errno import logging import threading import sys import time import fuse import mitogen.master import mitogen.utils import __main__ import os LOG = logging.getLogger(__name__) def to_text(p): """ On 3.x, fusepy returns paths as bytes. """ if isinstance(p, bytes): return p.decode('utf-8') return p def errno_wrap(modname, func, *args): try: return getattr(globals()[modname], func)(*args), None except (IOError, OSError): e = sys.exc_info()[1] if e.args[0] == errno.ENOENT: LOG.error('%r(**%r): %s', func, args, e) else: LOG.exception('While running %r(**%r)', func, args) return None, to_text(errno.errorcode[e.args[0]]) def errno_call(context, func, *args): result, errname = context.call( errno_wrap, func.__module__, func.__name__, *args ) if errname: raise fuse.FuseOSError(getattr(errno, errname)) return result def _create(path, mode): fd = os.open(path, os.O_WRONLY) try: os.fchmod(fd, mode) finally: os.close(fd) def _stat(path): st = os.lstat(path) keys = ('st_atime', 'st_gid', 'st_mode', 'st_mtime', 'st_size', 'st_uid') dct = dict((key, getattr(st, key)) for key in keys) dct['has_contents'] = os.path.exists(os.path.join(path, 'Contents')) return dct def _listdir(path): return [ (name, _stat(os.path.join(path, name)), 0) for name in os.listdir(path) ] def _read(path, size, offset): fd = os.open(path, os.O_RDONLY) try: os.lseek(fd, offset, os.SEEK_SET) return os.read(fd, size) finally: os.close(fd) def _truncate(path, length): fd = os.open(path, os.O_RDWR) try: os.truncate(fd, length) finally: os.close(fd) def _write(path, data, offset): fd = os.open(path, os.O_RDWR) try: os.lseek(fd, offset, os.SEEK_SET) return os.write(fd, data) finally: os.close(fd) def _evil_name(path): if not (os.path.basename(path).startswith('._') or path.endswith('.DS_Store')): return raise fuse.FuseOSError(errno.ENOENT) def _chroot(path): os.chroot(path) class Operations(fuse.Operations): # fuse.LoggingMixIn, def __init__(self, host, path='.'): self.host = host self.root = path self.ready = threading.Event() if not hasattr(self, 'encoding'): self.encoding = 'utf-8' def init(self, path): self.broker = mitogen.master.Broker(install_watcher=False) self.router = mitogen.master.Router(self.broker) self.host = self.router.ssh(hostname=self.host) self._context = self.router.sudo(via=self.host) #self._context.call(_chroot , '/home/dmw') self._stat_cache = {} self.ready.set() def destroy(self, path): self.broker.shutdown() @property def context(self): self.ready.wait() return self._context def chmod(self, path, mode): path = path.decode(self.encoding) _evil_name(path) return errno_call(self._context, os.chmod, path, mode) def chown(self, path, uid, gid): path = path.decode(self.encoding) _evil_name(path) return errno_call(self._context, os.chown, path, uid, gid) def create(self, path, mode): path = path.decode(self.encoding) _evil_name(path) return errno_call(self._context, _create, path, mode) or 0 def getattr(self, path, fh=None): _evil_name(path) if path in self._stat_cache: now = time.time() then, st = self._stat_cache[path] if now < (then + 2.0): return st basedir = os.path.dirname(path) if path.endswith('/Contents') and basedir in self._stat_cache: now = time.time() then, st = self._stat_cache[basedir] if now < (then + 2.0) and not st['has_contents']: raise fuse.FuseOSError(errno.ENOENT) return errno_call(self._context, _stat, path) def mkdir(self, path, mode): path = path.decode(self.encoding) _evil_name(path) return errno_call(self._context, os.mkdir, path, mode) def read(self, path, size, offset, fh): path = path.decode(self.encoding) _evil_name(path) return errno_call(self._context, _read, path, size, offset) def readdir(self, path, fh): _evil_name(path) lst = errno_call(self._context, _listdir, path) now = time.time() for name, stat, _ in lst: self._stat_cache[os.path.join(path, name)] = (now, stat) return lst def readlink(self, path): _evil_name(path) return errno_call(self._context, os.readlink, path) def rename(self, old, new): old = old.decode(self.encoding) new = new.decode(self.encoding) return errno_call(self._context, os.rename, old, new) # TODO return self.sftp.rename(old, self.root + new) def rmdir(self, path): path = path.decode(self.encoding) _evil_name(path) return errno_call(self._context, os.rmdir, path) def symlink(self, target, source): target = target.decode(self.encoding) source = source.decode(self.encoding) _evil_name(path) return errno_call(self._context, os.symlink, source, target) def truncate(self, path, length, fh=None): path = path.decode(self.encoding) _evil_name(path) return errno_call(self._context, _truncate, path, length) def unlink(self, path): path = path.decode(self.encoding) _evil_name(path) return errno_call(self._context, os.unlink, path) def utimens(self, path, times=None): path = path.decode(self.encoding) _evil_name(path) return errno_call(self._context, os.utime, path, times) def write(self, path, data, offset, fh): path = path.decode(self.encoding) _evil_name(path) return errno_call(self._context, _write, path, data, offset) @mitogen.main(log_level='DEBUG') def main(router): if len(sys.argv) != 3: print('usage: %s ' % sys.argv[0]) sys.exit(1) kwargs = {} if sys.platform == 'darwin': kwargs['volname'] = '%s (Mitogen)' % (sys.argv[1],) fuse.FUSE( operations=Operations(sys.argv[1]), mountpoint=sys.argv[2], foreground=True, **kwargs )