1. Etienne Perot
  2. crushfs

Commits

Etienne Perot  committed 31d9c3e

Initial commit.

  • Participants
  • Branches master

Comments (0)

Files changed (5)

File .gitignore

View file
+__pycache__
+backing
+mount
+*.jpg
+*.png
+

File callbackfs.py

View file
+#!/usr/bin/env python3
+
+import os
+import re
+import loopbackfs
+
+class callback:
+	fileHandle = 1337
+	def getFileHandle():
+		callback.fileHandle += 1
+		return callback.fileHandle
+	def __init__(self, fs, path):
+		self.fs = fs
+		self.path = path
+		self.dirname = os.path.dirname(path)
+		if self.dirname[-1] != os.sep:
+			self.dirname += os.sep
+	def getPath(self):
+		return self.path
+	def getPlainPath(self):
+		return loopbackfs.getPlainPath(self.fs, self.path)
+	def getDirname(self):
+		return self.dirname
+	def clear(self):
+		self.fs.clearCallback(self.path)
+	def create(self):
+		return None
+	def open(self):
+		return None
+	def read(self, size, offset=0):
+		return None
+	def truncate(self, size):
+		return None
+	def close(self):
+		return None
+	def delete(self):
+		return None
+	def write(self, data, offset):
+		return None
+
+class callbackfs(loopbackfs.Loopback):
+	def __init__(self, *args, **kwargs):
+		super().__init__(*args, **kwargs)
+		self.callbacks = {}
+		self.compiledRegexes = {}
+		self.definedCallbacks = {}
+	def addCallback(self, regex, callbackClass, regexFlags=re.IGNORECASE):
+		self.compiledRegexes[regex] = re.compile(regex, regexFlags)
+		self.callbacks[regex] = callbackClass
+	def getCallback(self, path):
+		if os.path.exists(self.root + os.sep + path):
+			return None
+		if path in self.definedCallbacks:
+			return self.definedCallbacks[path]
+		for r in self.callbacks:
+			if self.compiledRegexes[r].search(path):
+				callback = self.callbacks[r](self, path)
+				self.definedCallbacks[path] = callback
+				return callback
+		return None
+	def clearCallback(self, path):
+		if path in self.definedCallbacks:
+			del self.definedCallbacks[path]
+	def create(self, path, mode):
+		callback = self.getCallback(path)
+		if callback:
+			result = callback.create()
+			if result is not None:
+				return result
+		return super().create(path, mode)
+	def open(self, path, flags, mode=None):
+		callback = self.getCallback(path)
+		if callback:
+			result = callback.open()
+			if result is not None:
+				return result
+		if mode is None:
+			return super().open(path, flags)
+		return super().open(path, flags, mode)
+	def read(self, path, size, offset, fh):
+		callback = self.getCallback(path)
+		if callback:
+			result = callback.read(size, offset)
+			if result is not None:
+				return result
+		return super().read(path, size, offset, fh)
+	def readdir(self, path, fh):
+		allFiles = super().readdir(path, fh)
+		for p in self.definedCallbacks.values():
+			if p.getDirname() == path and p.getPlainPath() not in allFiles:
+				allFiles.append(p.getPlainPath())
+		return allFiles
+	def truncate(self, path, length, fh=None):
+		callback = self.getCallback(path)
+		if callback:
+			result = callback.truncate(length)
+			if result is not None:
+				return result
+		return super().truncate(path, length, fh)
+	def release(self, path, fh):
+		callback = self.getCallback(path)
+		if callback:
+			result = callback.close()
+			if result is not None:
+				return result
+		return super().release(path, fh)
+	def unlink(self, path):
+		callback = self.getCallback(path)
+		if callback:
+			result = callback.delete()
+			if result is not None:
+				return result
+		return super().unlink(path)
+	def write(self, path, data, offset, fh):
+		callback = self.getCallback(path)
+		if callback:
+			result = callback.write(data, offset)
+			if result is not None:
+				return result
+		return super().write(path, data, offset, fh)

File crushfs.py

View file
+#!/usr/bin/env python3
+
+import os
+import sys
+import shutil
+import threading
+import subprocess
+from fuse import FUSE
+import callbackfs
+
+class Crusher(callbackfs.callback):
+	def getCrushPath(self):
+		return self.getPath() + '.crush'
+	def getArguments(self):
+		return None
+	def crush(self, attempt=0):
+		if attempt > 5:
+			try:
+				os.remove(self.getCrushPath())
+			except:
+				pass
+			return False
+		p = subprocess.Popen(self.getArguments())
+		result = p.wait()
+		if result:
+			try:
+				os.remove(self.getCrushPath())
+			except:
+				pass
+			return self.crush(attempt + 1)
+		os.remove(self.getPath())
+		shutil.move(self.getCrushPath(), self.getPath())
+		return True
+	def close(self):
+		threading.Thread(target=self.crush).start()
+		return None
+
+class PNGCrusher(Crusher):
+	arguments = ['pngcrush', '-q', '-l', '9', '-reduce', '-rem', 'gAMA', '-rem', 'cHRM', '-rem', 'iCCP', '-rem', 'sRGB'] + [i for l in [('-m', str(i)) for i in range(138)] for i in l]
+	def getArguments(self):
+		return PNGCrusher.arguments + [self.getPath(), self.getCrushPath()]
+
+class JPEGCrusher(Crusher):
+	arguments = ['jpegtran', '-optimize', '-copy', 'none', '-progressive', '-outfile']
+	def getArguments(self):
+		return JPEGCrusher.arguments + [self.getCrushPath(), self.getPath()]
+
+class crushfs(callbackfs.callbackfs):
+	def __init__(self, *args, **kwargs):
+		super().__init__(*args, **kwargs)
+		self.addCallback(r'\.png$', PNGCrusher)
+		self.addCallback(r'\.jpe?g$', JPEGCrusher)
+
+if __name__ == '__main__':
+	if len(sys.argv) != 3:
+		print('usage: %s <backing directory> <mountpoint>' % sys.argv[0])
+		sys.exit(1)
+	FUSE(crushfs(sys.argv[1]), sys.argv[2], foreground=True)

File fuse.py

View file
+# Copyright (c) 2008 Giorgos Verigakis <verigak@gmail.com>
+# 
+# Permission to use, copy, modify, and distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+# 
+# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+from ctypes import *
+from ctypes.util import find_library
+from errno import *
+from functools import partial
+from platform import machine, system
+from stat import S_IFDIR
+from traceback import print_exc
+
+import logging
+
+def toDecode(s):
+    if type(s) is type(b''):
+        return s.decode('utf8')
+    return s
+
+def toEncode(s):
+    if type(s) is type(b''):
+        return s
+    return s.encode('utf8')
+
+class c_timespec(Structure):
+    _fields_ = [('tv_sec', c_long), ('tv_nsec', c_long)]
+
+class c_utimbuf(Structure):
+    _fields_ = [('actime', c_timespec), ('modtime', c_timespec)]
+
+class c_stat(Structure):
+    pass    # Platform dependent
+
+_system = system()
+if _system in ('Darwin', 'FreeBSD'):
+    _libiconv = CDLL(find_library("iconv"), RTLD_GLOBAL)     # libfuse dependency
+    ENOTSUP = 45
+    c_dev_t = c_int32
+    c_fsblkcnt_t = c_ulong
+    c_fsfilcnt_t = c_ulong
+    c_gid_t = c_uint32
+    c_mode_t = c_uint16
+    c_off_t = c_int64
+    c_pid_t = c_int32
+    c_uid_t = c_uint32
+    setxattr_t = CFUNCTYPE(c_int, c_char_p, c_char_p, POINTER(c_byte),
+        c_size_t, c_int, c_uint32)
+    getxattr_t = CFUNCTYPE(c_int, c_char_p, c_char_p, POINTER(c_byte),
+        c_size_t, c_uint32)
+    c_stat._fields_ = [
+        ('st_dev', c_dev_t),
+        ('st_ino', c_uint32),
+        ('st_mode', c_mode_t),
+        ('st_nlink', c_uint16),
+        ('st_uid', c_uid_t),
+        ('st_gid', c_gid_t),
+        ('st_rdev', c_dev_t),
+        ('st_atimespec', c_timespec),
+        ('st_mtimespec', c_timespec),
+        ('st_ctimespec', c_timespec),
+        ('st_size', c_off_t),
+        ('st_blocks', c_int64),
+        ('st_blksize', c_int32)]
+elif _system == 'Linux':
+    ENOTSUP = 95
+    c_dev_t = c_ulonglong
+    c_fsblkcnt_t = c_ulonglong
+    c_fsfilcnt_t = c_ulonglong
+    c_gid_t = c_uint
+    c_mode_t = c_uint
+    c_off_t = c_longlong
+    c_pid_t = c_int
+    c_uid_t = c_uint
+    setxattr_t = CFUNCTYPE(c_int, c_char_p, c_char_p, POINTER(c_byte), c_size_t, c_int)
+    getxattr_t = CFUNCTYPE(c_int, c_char_p, c_char_p, POINTER(c_byte), c_size_t)
+    
+    _machine = machine()
+    if _machine == 'x86_64':
+        c_stat._fields_ = [
+            ('st_dev', c_dev_t),
+            ('st_ino', c_ulong),
+            ('st_nlink', c_ulong),
+            ('st_mode', c_mode_t),
+            ('st_uid', c_uid_t),
+            ('st_gid', c_gid_t),
+            ('__pad0', c_int),
+            ('st_rdev', c_dev_t),
+            ('st_size', c_off_t),
+            ('st_blksize', c_long),
+            ('st_blocks', c_long),
+            ('st_atimespec', c_timespec),
+            ('st_mtimespec', c_timespec),
+            ('st_ctimespec', c_timespec)]
+    elif _machine == 'ppc':
+        c_stat._fields_ = [
+            ('st_dev', c_dev_t),
+            ('st_ino', c_ulonglong),
+            ('st_mode', c_mode_t),
+            ('st_nlink', c_uint),
+            ('st_uid', c_uid_t),
+            ('st_gid', c_gid_t),
+            ('st_rdev', c_dev_t),
+            ('__pad2', c_ushort),
+            ('st_size', c_off_t),
+            ('st_blksize', c_long),
+            ('st_blocks', c_longlong),
+            ('st_atimespec', c_timespec),
+            ('st_mtimespec', c_timespec),
+            ('st_ctimespec', c_timespec)]
+    else:
+        # i686, use as fallback for everything else
+        c_stat._fields_ = [
+            ('st_dev', c_dev_t),
+            ('__pad1', c_ushort),
+            ('__st_ino', c_ulong),
+            ('st_mode', c_mode_t),
+            ('st_nlink', c_uint),
+            ('st_uid', c_uid_t),
+            ('st_gid', c_gid_t),
+            ('st_rdev', c_dev_t),
+            ('__pad2', c_ushort),
+            ('st_size', c_off_t),
+            ('st_blksize', c_long),
+            ('st_blocks', c_longlong),
+            ('st_atimespec', c_timespec),
+            ('st_mtimespec', c_timespec),
+            ('st_ctimespec', c_timespec),
+            ('st_ino', c_ulonglong)]
+else:
+    raise NotImplementedError('%s is not supported.' % _system)
+
+
+class c_statvfs(Structure):
+    _fields_ = [
+        ('f_bsize', c_ulong),
+        ('f_frsize', c_ulong),
+        ('f_blocks', c_fsblkcnt_t),
+        ('f_bfree', c_fsblkcnt_t),
+        ('f_bavail', c_fsblkcnt_t),
+        ('f_files', c_fsfilcnt_t),
+        ('f_ffree', c_fsfilcnt_t),
+        ('f_favail', c_fsfilcnt_t)]
+
+if _system == 'FreeBSD':
+    c_fsblkcnt_t = c_uint64
+    c_fsfilcnt_t = c_uint64
+    setxattr_t = CFUNCTYPE(c_int, c_char_p, c_char_p, POINTER(c_byte), c_size_t, c_int)
+    getxattr_t = CFUNCTYPE(c_int, c_char_p, c_char_p, POINTER(c_byte), c_size_t)
+    class c_statvfs(Structure):
+        _fields_ = [
+            ('f_bavail', c_fsblkcnt_t),
+            ('f_bfree', c_fsblkcnt_t),
+            ('f_blocks', c_fsblkcnt_t),
+            ('f_favail', c_fsfilcnt_t),
+            ('f_ffree', c_fsfilcnt_t),
+            ('f_files', c_fsfilcnt_t),
+            ('f_bsize', c_ulong),
+            ('f_flag', c_ulong),
+            ('f_frsize', c_ulong)]
+
+class fuse_file_info(Structure):
+    _fields_ = [
+        ('flags', c_int),
+        ('fh_old', c_ulong),
+        ('writepage', c_int),
+        ('direct_io', c_uint, 1),
+        ('keep_cache', c_uint, 1),
+        ('flush', c_uint, 1),
+        ('padding', c_uint, 29),
+        ('fh', c_uint64),
+        ('lock_owner', c_uint64)]
+
+class fuse_context(Structure):
+    _fields_ = [
+        ('fuse', c_voidp),
+        ('uid', c_uid_t),
+        ('gid', c_gid_t),
+        ('pid', c_pid_t),
+        ('private_data', c_voidp)]
+
+class fuse_operations(Structure):
+    _fields_ = [
+        ('getattr', CFUNCTYPE(c_int, c_char_p, POINTER(c_stat))),
+        ('readlink', CFUNCTYPE(c_int, c_char_p, POINTER(c_byte), c_size_t)),
+        ('getdir', c_voidp),    # Deprecated, use readdir
+        ('mknod', CFUNCTYPE(c_int, c_char_p, c_mode_t, c_dev_t)),
+        ('mkdir', CFUNCTYPE(c_int, c_char_p, c_mode_t)),
+        ('unlink', CFUNCTYPE(c_int, c_char_p)),
+        ('rmdir', CFUNCTYPE(c_int, c_char_p)),
+        ('symlink', CFUNCTYPE(c_int, c_char_p, c_char_p)),
+        ('rename', CFUNCTYPE(c_int, c_char_p, c_char_p)),
+        ('link', CFUNCTYPE(c_int, c_char_p, c_char_p)),
+        ('chmod', CFUNCTYPE(c_int, c_char_p, c_mode_t)),
+        ('chown', CFUNCTYPE(c_int, c_char_p, c_uid_t, c_gid_t)),
+        ('truncate', CFUNCTYPE(c_int, c_char_p, c_off_t)),
+        ('utime', c_voidp),     # Deprecated, use utimens
+        ('open', CFUNCTYPE(c_int, c_char_p, POINTER(fuse_file_info))),
+        ('read', CFUNCTYPE(c_int, c_char_p, POINTER(c_byte), c_size_t, c_off_t,
+            POINTER(fuse_file_info))),
+        ('write', CFUNCTYPE(c_int, c_char_p, POINTER(c_byte), c_size_t, c_off_t,
+            POINTER(fuse_file_info))),
+        ('statfs', CFUNCTYPE(c_int, c_char_p, POINTER(c_statvfs))),
+        ('flush', CFUNCTYPE(c_int, c_char_p, POINTER(fuse_file_info))),
+        ('release', CFUNCTYPE(c_int, c_char_p, POINTER(fuse_file_info))),
+        ('fsync', CFUNCTYPE(c_int, c_char_p, c_int, POINTER(fuse_file_info))),
+        ('setxattr', setxattr_t),
+        ('getxattr', getxattr_t),
+        ('listxattr', CFUNCTYPE(c_int, c_char_p, POINTER(c_byte), c_size_t)),
+        ('removexattr', CFUNCTYPE(c_int, c_char_p, c_char_p)),
+        ('opendir', CFUNCTYPE(c_int, c_char_p, POINTER(fuse_file_info))),
+        ('readdir', CFUNCTYPE(c_int, c_char_p, c_voidp, CFUNCTYPE(c_int, c_voidp,
+            c_char_p, POINTER(c_stat), c_off_t), c_off_t, POINTER(fuse_file_info))),
+        ('releasedir', CFUNCTYPE(c_int, c_char_p, POINTER(fuse_file_info))),
+        ('fsyncdir', CFUNCTYPE(c_int, c_char_p, c_int, POINTER(fuse_file_info))),
+        ('init', CFUNCTYPE(c_voidp, c_voidp)),
+        ('destroy', CFUNCTYPE(c_voidp, c_voidp)),
+        ('access', CFUNCTYPE(c_int, c_char_p, c_int)),
+        ('create', CFUNCTYPE(c_int, c_char_p, c_mode_t, POINTER(fuse_file_info))),
+        ('ftruncate', CFUNCTYPE(c_int, c_char_p, c_off_t, POINTER(fuse_file_info))),
+        ('fgetattr', CFUNCTYPE(c_int, c_char_p, POINTER(c_stat),
+            POINTER(fuse_file_info))),
+        ('lock', CFUNCTYPE(c_int, c_char_p, POINTER(fuse_file_info), c_int, c_voidp)),
+        ('utimens', CFUNCTYPE(c_int, c_char_p, POINTER(c_utimbuf))),
+        ('bmap', CFUNCTYPE(c_int, c_char_p, c_size_t, POINTER(c_ulonglong)))]
+
+
+def time_of_timespec(ts):
+    return ts.tv_sec + ts.tv_nsec / 10 ** 9
+
+def set_st_attrs(st, attrs):
+    for key, val in attrs.items():
+        if key in ('st_atime', 'st_mtime', 'st_ctime'):
+            timespec = getattr(st, key + 'spec')
+            timespec.tv_sec = int(val)
+            timespec.tv_nsec = int((val - timespec.tv_sec) * 10 ** 9)
+        elif hasattr(st, key):
+            setattr(st, key, val)
+
+
+_libfuse_path = find_library('fuse')
+if not _libfuse_path:
+    raise EnvironmentError('Unable to find libfuse')
+_libfuse = CDLL(_libfuse_path)
+_libfuse.fuse_get_context.restype = POINTER(fuse_context)
+
+
+def fuse_get_context():
+    """Returns a (uid, gid, pid) tuple"""
+    ctxp = _libfuse.fuse_get_context()
+    ctx = ctxp.contents
+    return ctx.uid, ctx.gid, ctx.pid
+
+
+class FUSE(object):
+    """This class is the lower level interface and should not be subclassed
+       under normal use. Its methods are called by fuse.
+       Assumes API version 2.6 or later."""
+    
+    def __init__(self, operations, mountpoint, raw_fi=False, **kwargs):
+        """Setting raw_fi to True will cause FUSE to pass the fuse_file_info
+           class as is to Operations, instead of just the fh field.
+           This gives you access to direct_io, keep_cache, etc."""
+        
+        self.operations = operations
+        self.raw_fi = raw_fi
+        args = ['fuse']
+        if kwargs.pop('foreground', False):
+            args.append('-f')
+        if kwargs.pop('debug', False):
+            args.append('-d')
+        if kwargs.pop('nothreads', False):
+            args.append('-s')
+        kwargs.setdefault('fsname', operations.__class__.__name__)
+        args.append('-o')
+        args.append(','.join(key if val == True else '%s=%s' % (key, val)
+            for key, val in kwargs.items()))
+        args.append(mountpoint)
+        args = [toEncode(x) for x in args]
+        argv = (c_char_p * len(args))(*args)
+        fuse_ops = fuse_operations()
+        for name, prototype in fuse_operations._fields_:
+            if prototype != c_voidp and getattr(operations, name, None):
+                op = partial(self._wrapper_, getattr(self, name))
+                setattr(fuse_ops, name, prototype(op))
+        _libfuse.fuse_main_real(len(args), argv, pointer(fuse_ops),
+            sizeof(fuse_ops), None)
+        del self.operations     # Invoke the destructor
+    
+    def _wrapper_(self, func, *args, **kwargs):
+        """Decorator for the methods that follow"""
+        try:
+            return func(*args, **kwargs) or 0
+        except OSError as e:
+            return -(e.errno or EFAULT)
+        except:
+            print_exc()
+            return -EFAULT
+    
+    def getattr(self, path, buf):
+        return self.fgetattr(path, buf, None)
+    
+    def readlink(self, path, buf, bufsize):
+        ret = toEncode(self.operations('readlink', path))
+        data = create_string_buffer(ret[:bufsize - 1])
+        memmove(buf, data, len(data))
+        return 0
+    
+    def mknod(self, path, mode, dev):
+        return self.operations('mknod', toDecode(path), mode, dev)
+    
+    def mkdir(self, path, mode):
+        return self.operations('mkdir', toDecode(path), mode)
+    
+    def unlink(self, path):
+        return self.operations('unlink', toDecode(path))
+    
+    def rmdir(self, path):
+        return self.operations('rmdir', toDecode(path))
+    
+    def symlink(self, source, target):
+        return self.operations('symlink', toDecode(target), toDecode(source))
+    
+    def rename(self, old, new):
+        return self.operations('rename', toDecode(old), toDecode(new))
+    
+    def link(self, source, target):
+        return self.operations('link', toDecode(target), toDecode(source))
+    
+    def chmod(self, path, mode):
+        return self.operations('chmod', toDecode(path), toDecode(mode))
+    
+    def chown(self, path, uid, gid):
+        return self.operations('chown', toDecode(path), uid, gid)
+    
+    def truncate(self, path, length):
+        return self.operations('truncate', toDecode(path), length)
+    
+    def open(self, path, fip):
+        fi = fip.contents
+        if self.raw_fi:
+            return self.operations('open', toDecode(path), fi)
+        else:
+            fi.fh = self.operations('open', toDecode(path), fi.flags)
+            return 0
+    
+    def read(self, path, buf, size, offset, fip):
+        fh = fip.contents if self.raw_fi else fip.contents.fh
+        ret = self.operations('read', toDecode(path), size, offset, fh)
+        if not ret:
+            return 0
+        data = create_string_buffer(ret[:size], size)
+        memmove(buf, data, size)
+        return size
+    
+    def write(self, path, buf, size, offset, fip):
+        data = string_at(buf, size)
+        fh = fip.contents if self.raw_fi else fip.contents.fh
+        return self.operations('write', toDecode(path), data, offset, fh)
+    
+    def statfs(self, path, buf):
+        stv = buf.contents
+        attrs = self.operations('statfs', toDecode(path))
+        for key, val in attrs.items():
+            if hasattr(stv, key):
+                setattr(stv, key, val)
+        return 0
+    
+    def flush(self, path, fip):
+        fh = fip.contents if self.raw_fi else fip.contents.fh
+        return self.operations('flush', toDecode(path), fh)
+    
+    def release(self, path, fip):
+        fh = fip.contents if self.raw_fi else fip.contents.fh
+        return self.operations('release', toDecode(path), fh)
+    
+    def fsync(self, path, datasync, fip):
+        fh = fip.contents if self.raw_fi else fip.contents.fh
+        return self.operations('fsync', toDecode(path), datasync, fh)
+    
+    def setxattr(self, path, name, value, size, options, *args):
+        data = string_at(value, size)
+        return self.operations('setxattr', toDecode(path), toDecode(name), data, options, *args)
+    
+    def getxattr(self, path, name, value, size, *args):
+        ret = self.operations('getxattr', toDecode(path), toDecode(name), *args)
+        retsize = len(ret)
+        buf = create_string_buffer(ret, retsize)    # Does not add trailing 0
+        if bool(value):
+            if retsize > size:
+                return -ERANGE
+            memmove(value, buf, retsize)
+        return retsize
+    
+    def listxattr(self, path, namebuf, size):
+        ret = self.operations('listxattr', toDecode(path))
+        buf = create_string_buffer('\x00'.join(ret)) if ret else ''
+        bufsize = len(buf)
+        if bool(namebuf):
+            if bufsize > size:
+                return -ERANGE
+            memmove(namebuf, buf, bufsize)
+        return bufsize
+    
+    def removexattr(self, path, name):
+        return self.operations('removexattr', toDecode(path), name)
+    
+    def opendir(self, path, fip):
+        # Ignore raw_fi
+        fip.contents.fh = self.operations('opendir', toDecode(path))
+        return 0
+    
+    def readdir(self, path, buf, filler, offset, fip):
+        # Ignore raw_fi
+        for item in self.operations('readdir', toDecode(path), fip.contents.fh):
+            if isinstance(item, str):
+                name, st, offset = item, None, 0
+            else:
+                name, attrs, offset = item
+                if attrs:
+                    st = c_stat()
+                    set_st_attrs(st, attrs)
+                else:
+                    st = None
+            if filler(buf, toEncode(name), st, offset) != 0:
+                break
+        return 0
+    
+    def releasedir(self, path, fip):
+        # Ignore raw_fi
+        return self.operations('releasedir', toDecode(path), fip.contents.fh)
+    
+    def fsyncdir(self, path, datasync, fip):
+        # Ignore raw_fi
+        return self.operations('fsyncdir', toDecode(path), datasync, fip.contents.fh)
+    
+    def init(self, conn):
+        return self.operations('init', '/')
+    
+    def destroy(self, private_data):
+        return self.operations('destroy', '/')
+    
+    def access(self, path, amode):
+        return self.operations('access', toDecode(path), amode)
+    
+    def create(self, path, mode, fip):
+        fi = fip.contents
+        if self.raw_fi:
+            return self.operations('create', toDecode(path), mode, fi)
+        else:
+            fi.fh = self.operations('create', toDecode(path), mode)
+            return 0
+    
+    def ftruncate(self, path, length, fip):
+        fh = fip.contents if self.raw_fi else fip.contents.fh
+        return self.operations('truncate', toDecode(path), length, fh)
+    
+    def fgetattr(self, path, buf, fip):
+        memset(buf, 0, sizeof(c_stat))
+        st = buf.contents
+        fh = fip and (fip.contents if self.raw_fi else fip.contents.fh)
+        attrs = self.operations('getattr', toDecode(path), fh)
+        set_st_attrs(st, attrs)
+        return 0
+    
+    def lock(self, path, fip, cmd, lock):
+        fh = fip.contents if self.raw_fi else fip.contents.fh
+        return self.operations('lock', toDecode(path), fh, cmd, lock)
+    
+    def utimens(self, path, buf):
+        if buf:
+            atime = time_of_timespec(buf.contents.actime)
+            mtime = time_of_timespec(buf.contents.modtime)
+            times = (atime, mtime)
+        else:
+            times = None
+        return self.operations('utimens', toDecode(path), times)
+    
+    def bmap(self, path, blocksize, idx):
+        return self.operations('bmap', toDecode(path), blocksize, idx)
+
+
+class Operations(object):
+    """This class should be subclassed and passed as an argument to FUSE on
+       initialization. All operations should raise an OSError exception on
+       error.
+       
+       When in doubt of what an operation should do, check the FUSE header
+       file or the corresponding system call man page."""
+    
+    def __call__(self, op, *args):
+        if not hasattr(self, op):
+            raise OSError(EFAULT, '')
+        return getattr(self, op)(*args)
+        
+    def access(self, path, amode):
+        return 0
+    
+    bmap = None
+    
+    def chmod(self, path, mode):
+        raise OSError(EROFS, '')
+    
+    def chown(self, path, uid, gid):
+        raise OSError(EROFS, '')
+    
+    def create(self, path, mode, fi=None):
+        """When raw_fi is False (default case), fi is None and create should
+           return a numerical file handle.
+           When raw_fi is True the file handle should be set directly by create
+           and return 0."""
+        raise OSError(EROFS, '')
+    
+    def destroy(self, path):
+        """Called on filesystem destruction. Path is always /"""
+        pass
+    
+    def flush(self, path, fh):
+        return 0
+    
+    def fsync(self, path, datasync, fh):
+        return 0
+    
+    def fsyncdir(self, path, datasync, fh):
+        return 0
+    
+    def getattr(self, path, fh=None):
+        """Returns a dictionary with keys identical to the stat C structure
+           of stat(2).
+           st_atime, st_mtime and st_ctime should be floats.
+           NOTE: There is an incombatibility between Linux and Mac OS X concerning
+           st_nlink of directories. Mac OS X counts all files inside the directory,
+           while Linux counts only the subdirectories."""
+        
+        if path != '/':
+            raise OSError(ENOENT, '')
+        return dict(st_mode=(S_IFDIR | 0o755), st_nlink=2)
+    
+    def getxattr(self, path, name, position=0):
+        raise OSError(ENOTSUP, '')
+    
+    def init(self, path):
+        """Called on filesystem initialization. Path is always /
+           Use it instead of __init__ if you start threads on initialization."""
+        pass
+    
+    def link(self, target, source):
+        raise OSError(EROFS, '')
+    
+    def listxattr(self, path):
+        return []
+        
+    lock = None
+    
+    def mkdir(self, path, mode):
+        raise OSError(EROFS, '')
+    
+    def mknod(self, path, mode, dev):
+        raise OSError(EROFS, '')
+    
+    def open(self, path, flags):
+        """When raw_fi is False (default case), open should return a numerical
+           file handle.
+           When raw_fi is True the signature of open becomes:
+               open(self, path, fi)
+           and the file handle should be set directly."""
+        return 0
+    
+    def opendir(self, path):
+        """Returns a numerical file handle."""
+        return 0
+    
+    def read(self, path, size, offset, fh):
+        """Returns a string containing the data requested."""
+        raise OSError(ENOENT, '')
+    
+    def readdir(self, path, fh):
+        """Can return either a list of names, or a list of (name, attrs, offset)
+           tuples. attrs is a dict as in getattr."""
+        return ['.', '..']
+    
+    def readlink(self, path):
+        raise OSError(ENOENT, '')
+    
+    def release(self, path, fh):
+        return 0
+    
+    def releasedir(self, path, fh):
+        return 0
+    
+    def removexattr(self, path, name):
+        raise OSError(ENOTSUP, '')
+    
+    def rename(self, old, new):
+        raise OSError(EROFS, '')
+    
+    def rmdir(self, path):
+        raise OSError(EROFS, '')
+    
+    def setxattr(self, path, name, value, options, position=0):
+        raise OSError(ENOTSUP, '')
+    
+    def statfs(self, path):
+        """Returns a dictionary with keys identical to the statvfs C structure
+           of statvfs(3).
+           On Mac OS X f_bsize and f_frsize must be a power of 2 (minimum 512)."""
+        return {}
+    
+    def symlink(self, target, source):
+        raise OSError(EROFS, '')
+    
+    def truncate(self, path, length, fh=None):
+        raise OSError(EROFS, '')
+    
+    def unlink(self, path):
+        raise OSError(EROFS, '')
+    
+    def utimens(self, path, times=None):
+        """Times is a (atime, mtime) tuple. If None use current time."""
+        return 0
+    
+    def write(self, path, data, offset, fh):
+        raise OSError(EROFS, '')
+
+
+class LoggingMixIn:
+    def __call__(self, op, path, *args):
+        logging.debug('-> %s %s %s', op, path, repr(args))
+        ret = '[Unknown Error]'
+        try:
+            ret = getattr(self, op)(path, *args)
+            return ret
+        except OSError as e:
+            ret = str(e)
+            raise
+        finally:
+            logging.debug('<- %s %s', op, repr(ret))

File loopbackfs.py

View file
+#!/usr/bin/env python3
+
+from __future__ import with_statement
+
+from errno import EACCES
+from os.path import realpath
+from sys import argv, exit
+from threading import Lock
+
+import os
+
+from fuse import FUSE, Operations, LoggingMixIn
+
+def getPlainPath(loopbackFS, path):
+    return path[len(loopbackFS.root+os.sep):]
+
+class Loopback(LoggingMixIn, Operations):    
+    def __init__(self, root):
+        self.root = realpath(root)
+        while self.root[-1] == os.sep:
+             self.root = self.root[:-1]
+        self.rwlock = Lock()
+    
+    def __call__(self, op, path, *args):
+        return super(Loopback, self).__call__(op, self.root + path, *args)
+
+    def access(self, path, mode):
+        if not os.access(path, mode):
+            raise Exception(EACCES)
+    
+    chmod = os.chmod
+    chown = os.chown
+    
+    def create(self, path, mode):
+        return os.open(path, os.O_WRONLY | os.O_CREAT, mode)
+    
+    def flush(self, path, fh):
+        return os.fsync(fh)
+
+    def fsync(self, path, datasync, fh):
+        return os.fsync(fh)
+                
+    def getattr(self, path, fh=None):
+        st = os.lstat(path)
+        return dict((key, getattr(st, key)) for key in ('st_atime', 'st_ctime',
+            'st_gid', 'st_mode', 'st_mtime', 'st_nlink', 'st_size', 'st_uid'))
+    
+    getxattr = None
+    
+    def link(self, target, source):
+        return os.link(source, target)
+    
+    listxattr = None
+    mkdir = os.mkdir
+    mknod = os.mknod
+    open = os.open
+        
+    def read(self, path, size, offset, fh):
+        with self.rwlock:
+            os.lseek(fh, offset, 0)
+            return os.read(fh, size)
+    
+    def readdir(self, path, fh):
+        return ['.', '..'] + os.listdir(path)
+
+    readlink = os.readlink
+    
+    def release(self, path, fh):
+        return os.close(fh)
+        
+    def rename(self, old, new):
+        return os.rename(old, self.root + new)
+    
+    rmdir = os.rmdir
+    
+    def statfs(self, path):
+        stv = os.statvfs(path)
+        return dict((key, getattr(stv, key)) for key in ('f_bavail', 'f_bfree',
+            'f_blocks', 'f_bsize', 'f_favail', 'f_ffree', 'f_files', 'f_flag',
+            'f_frsize', 'f_namemax'))
+    
+    def symlink(self, target, source):
+        return os.symlink(source, target)
+    
+    def truncate(self, path, length, fh=None):
+        with open(path, 'r+') as f:
+            f.truncate(length)
+    
+    unlink = os.unlink
+    utimens = os.utime
+    
+    def write(self, path, data, offset, fh):
+        with self.rwlock:
+            os.lseek(fh, offset, 0)
+            return os.write(fh, data)
+    
+
+if __name__ == "__main__":
+    if len(argv) != 3:
+        print('usage: %s <root> <mountpoint>' % argv[0])
+        exit(1)
+    fuse = FUSE(Loopback(argv[1]), argv[2], foreground=True)