From f028eb2988a2975b3e10ee073b5f6b683b569076 Mon Sep 17 00:00:00 2001 From: cki Date: Thu, 23 Nov 2023 18:55:18 +0100 Subject: [PATCH 1/2] passthrough example for ext fs --- examples/ext_passthroughfs.py | 496 ++++++++++++++++++++++++++++++++++ 1 file changed, 496 insertions(+) create mode 100755 examples/ext_passthroughfs.py diff --git a/examples/ext_passthroughfs.py b/examples/ext_passthroughfs.py new file mode 100755 index 0000000..cda3804 --- /dev/null +++ b/examples/ext_passthroughfs.py @@ -0,0 +1,496 @@ +#!/usr/bin/env python3 +''' +ext filesystem compatibility checked by https://github.com/zfsonlinux/fstest.git +passthroughfs.py - Example file system for pyfuse3 + +This file system mirrors the contents of a specified directory tree. + +Caveats: + + * Inode generation numbers are not passed through but set to zero. + + * Block size (st_blksize) and number of allocated blocks (st_blocks) are not + passed through. + + * Performance for large directories is not good, because the directory + is always read completely. + + * There may be a way to break-out of the directory tree. + + * The readdir implementation is not fully POSIX compliant. If a directory + contains hardlinks and is modified during a readdir call, readdir() + may return some of the hardlinked files twice or omit them completely. + + * If you delete or rename files in the underlying file system, the + passthrough file system will get confused. + +Copyright © Nikolaus Rath + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +''' + +import os +import sys + +# If we are running from the pyfuse3 source directory, try +# to load the module from there first. +basedir = os.path.abspath(os.path.join(os.path.dirname(sys.argv[0]), '..')) +if (os.path.exists(os.path.join(basedir, 'setup.py')) and + os.path.exists(os.path.join(basedir, 'src', 'pyfuse3.pyx'))): + sys.path.insert(0, os.path.join(basedir, 'src')) + +import pyfuse3 +from argparse import ArgumentParser +import errno +import logging +import stat as stat_m +from pyfuse3 import FUSEError +from os import fsencode, fsdecode +from collections import defaultdict +import trio + +import faulthandler +faulthandler.enable() + +log = logging.getLogger(__name__) + +class Operations(pyfuse3.Operations): + + enable_writeback_cache = True + + def __init__(self, source): + super().__init__() + self._inode_path_map = { pyfuse3.ROOT_INODE: source } + self._lookup_cnt = defaultdict(lambda : 0) + self._fd_inode_map = dict() + self._inode_fd_map = dict() + self._fd_open_count = dict() + + def _inode_to_path(self, inode): + try: + val = self._inode_path_map[inode] + except KeyError: + raise FUSEError(errno.ENOENT) + + if isinstance(val, set): + # In case of hardlinks, pick any path + val = next(iter(val)) + return val + + def _add_path(self, inode, path): + log.debug('_add_path for %d, %s', inode, path) + self._lookup_cnt[inode] += 1 + + # With hardlinks, one inode may map to multiple paths. + if inode not in self._inode_path_map: + self._inode_path_map[inode] = path + return + + val = self._inode_path_map[inode] + if isinstance(val, set): + val.add(path) + elif val != path: + self._inode_path_map[inode] = { path, val } + + async def forget(self, inode_list): + for (inode, nlookup) in inode_list: + if self._lookup_cnt[inode] > nlookup: + self._lookup_cnt[inode] -= nlookup + continue + log.debug('forgetting about inode %d', inode) + assert inode not in self._inode_fd_map + del self._lookup_cnt[inode] + try: + del self._inode_path_map[inode] + except KeyError: # may have been deleted + pass + + async def lookup(self, inode_p, name, ctx=None): + name = fsdecode(name) + log.debug('lookup for %s in %d', name, inode_p) + path = os.path.join(self._inode_to_path(inode_p), name) + attr = self._getattr(path=path) + if name != '.' and name != '..': + self._add_path(attr.st_ino, path) + return attr + + async def getattr(self, inode, ctx=None): + if inode in self._inode_fd_map: + return self._getattr(fd=self._inode_fd_map[inode]) + else: + return self._getattr(path=self._inode_to_path(inode)) + + def _getattr(self, path=None, fd=None): + assert fd is None or path is None + assert not(fd is None and path is None) + try: + if fd is None: + stat = os.lstat(path) + else: + stat = os.fstat(fd) + except OSError as exc: + raise FUSEError(exc.errno) + + entry = pyfuse3.EntryAttributes() + for attr in ('st_ino', 'st_mode', 'st_nlink', 'st_uid', 'st_gid', + 'st_rdev', 'st_size', 'st_atime_ns', 'st_mtime_ns', + 'st_ctime_ns'): + setattr(entry, attr, getattr(stat, attr)) + entry.generation = 0 + entry.entry_timeout = 0 + entry.attr_timeout = 0 + entry.st_blksize = 512 + entry.st_blocks = ((entry.st_size+entry.st_blksize-1) // entry.st_blksize) + + return entry + + async def readlink(self, inode, ctx): + path = self._inode_to_path(inode) + try: + target = os.readlink(path) + except OSError as exc: + raise FUSEError(exc.errno) + return fsencode(target) + + async def opendir(self, inode, ctx): + return inode + + async def readdir(self, inode, off, token): + path = self._inode_to_path(inode) + log.debug('reading %s', path) + entries = [] + for name in os.listdir(path): + if name == '.' or name == '..': + continue + attr = self._getattr(path=os.path.join(path, name)) + entries.append((attr.st_ino, name, attr)) + + log.debug('read %d entries, starting at %d', len(entries), off) + + # This is not fully posix compatible. If there are hardlinks + # (two names with the same inode), we don't have a unique + # offset to start in between them. Note that we cannot simply + # count entries, because then we would skip over entries + # (or return them more than once) if the number of directory + # entries changes between two calls to readdir(). + for (ino, name, attr) in sorted(entries): + if ino <= off: + continue + if not pyfuse3.readdir_reply( + token, fsencode(name), attr, ino): + break + self._add_path(attr.st_ino, os.path.join(path, name)) + + async def unlink(self, inode_p, name, ctx): + name = fsdecode(name) + parent = self._inode_to_path(inode_p) + path = os.path.join(parent, name) + try: + inode = os.lstat(path).st_ino + os.unlink(path) + except OSError as exc: + raise FUSEError(exc.errno) + if inode in self._lookup_cnt: + self._forget_path(inode, path) + + async def rmdir(self, inode_p, name, ctx): + name = fsdecode(name) + parent = self._inode_to_path(inode_p) + path = os.path.join(parent, name) + try: + inode = os.lstat(path).st_ino + os.rmdir(path) + except OSError as exc: + raise FUSEError(exc.errno) + if inode in self._lookup_cnt: + self._forget_path(inode, path) + + def _forget_path(self, inode, path): + log.debug('forget %s for %d', path, inode) + val = self._inode_path_map[inode] + if isinstance(val, set): + val.remove(path) + if len(val) == 1: + self._inode_path_map[inode] = next(iter(val)) + else: + del self._inode_path_map[inode] + + async def symlink(self, inode_p, name, target, ctx): + name = fsdecode(name) + target = fsdecode(target) + parent = self._inode_to_path(inode_p) + path = os.path.join(parent, name) + try: + os.symlink(target, path) + os.chown(path, ctx.uid, ctx.gid, follow_symlinks=False) + except OSError as exc: + raise FUSEError(exc.errno) + stat = os.lstat(path) + self._add_path(stat.st_ino, path) + return await self.getattr(stat.st_ino) + + async def rename(self, inode_p_old, name_old, inode_p_new, name_new, + flags, ctx): + if flags != 0: + raise FUSEError(errno.EINVAL) + + name_old = fsdecode(name_old) + name_new = fsdecode(name_new) + parent_old = self._inode_to_path(inode_p_old) + parent_new = self._inode_to_path(inode_p_new) + path_old = os.path.join(parent_old, name_old) + path_new = os.path.join(parent_new, name_new) + try: + os.rename(path_old, path_new) + inode = os.lstat(path_new).st_ino + except OSError as exc: + raise FUSEError(exc.errno) + if inode not in self._lookup_cnt: + return + + val = self._inode_path_map[inode] + if isinstance(val, set): + assert len(val) > 1 + val.add(path_new) + val.remove(path_old) + else: + assert val == path_old + self._inode_path_map[inode] = path_new + + async def link(self, inode, new_inode_p, new_name, ctx): + new_name = fsdecode(new_name) + parent = self._inode_to_path(new_inode_p) + path = os.path.join(parent, new_name) + try: + os.link(self._inode_to_path(inode), path, follow_symlinks=False) + except OSError as exc: + raise FUSEError(exc.errno) + self._add_path(inode, path) + return await self.getattr(inode) + + async def setattr(self, inode, attr, fields, fh, ctx): + # We use the f* functions if possible so that we can handle + # a setattr() call for an inode without associated directory + # handle. + if fh is None: + path_or_fh = self._inode_to_path(inode) + truncate = os.truncate + chmod = os.chmod + chown = os.chown + stat = os.lstat + else: + path_or_fh = fh + truncate = os.ftruncate + chmod = os.fchmod + chown = os.fchown + stat = os.fstat + + try: + if fields.update_size: + truncate(path_or_fh, attr.st_size) + + if fields.update_mode: + # Under Linux, chmod always resolves symlinks so we should + # actually never get a setattr() request for a symbolic + # link. + assert not stat_m.S_ISLNK(attr.st_mode) + chmod(path_or_fh, stat_m.S_IMODE(attr.st_mode)) + + if fields.update_uid: + chown(path_or_fh, attr.st_uid, -1, follow_symlinks=False) + + if fields.update_gid: + chown(path_or_fh, -1, attr.st_gid, follow_symlinks=False) + + if fields.update_mode is False and \ + fields.update_uid is False and \ + fields.update_gid is False: + if chown == os.chown: + chown(path_or_fh, -1, -1, follow_symlinks=False) + if chown == os.fchown: + chown(path_or_fh, -1, -1) + + if fields.update_atime and fields.update_mtime: + if fh is None: + os.utime(path_or_fh, None, follow_symlinks=False, + ns=(attr.st_atime_ns, attr.st_mtime_ns)) + else: + os.utime(path_or_fh, None, + ns=(attr.st_atime_ns, attr.st_mtime_ns)) + elif fields.update_atime or fields.update_mtime: + # We can only set both values, so we first need to retrieve the + # one that we shouldn't be changing. + oldstat = stat(path_or_fh) + if not fields.update_atime: + attr.st_atime_ns = oldstat.st_atime_ns + else: + attr.st_mtime_ns = oldstat.st_mtime_ns + if fh is None: + os.utime(path_or_fh, None, follow_symlinks=False, + ns=(attr.st_atime_ns, attr.st_mtime_ns)) + else: + os.utime(path_or_fh, None, + ns=(attr.st_atime_ns, attr.st_mtime_ns)) + + except OSError as exc: + raise FUSEError(exc.errno) + + return await self.getattr(inode) + + async def mknod(self, inode_p, name, mode, rdev, ctx): + path = os.path.join(self._inode_to_path(inode_p), fsdecode(name)) + try: + os.mknod(path, mode=(mode & ~ctx.umask), device=rdev) + os.chown(path, ctx.uid, ctx.gid) + except OSError as exc: + raise FUSEError(exc.errno) + attr = self._getattr(path=path) + self._add_path(attr.st_ino, path) + return attr + + async def mkdir(self, inode_p, name, mode, ctx): + path = os.path.join(self._inode_to_path(inode_p), fsdecode(name)) + try: + os.mkdir(path, mode=(mode & ~ctx.umask)) + os.chown(path, ctx.uid, ctx.gid) + except OSError as exc: + raise FUSEError(exc.errno) + attr = self._getattr(path=path) + self._add_path(attr.st_ino, path) + return attr + + async def statfs(self, ctx): + root = self._inode_path_map[pyfuse3.ROOT_INODE] + stat_ = pyfuse3.StatvfsData() + try: + statfs = os.statvfs(root) + except OSError as exc: + raise FUSEError(exc.errno) + for attr in ('f_bsize', 'f_frsize', 'f_blocks', 'f_bfree', 'f_bavail', + 'f_files', 'f_ffree', 'f_favail'): + setattr(stat_, attr, getattr(statfs, attr)) + stat_.f_namemax = statfs.f_namemax - (len(root)+1) + return stat_ + + async def open(self, inode, flags, ctx): + if inode in self._inode_fd_map: + fd = self._inode_fd_map[inode] + self._fd_open_count[fd] += 1 + return pyfuse3.FileInfo(fh=fd) + assert flags & os.O_CREAT == 0 + try: + fd = os.open(self._inode_to_path(inode), flags) + except OSError as exc: + raise FUSEError(exc.errno) + self._inode_fd_map[inode] = fd + self._fd_inode_map[fd] = inode + self._fd_open_count[fd] = 1 + return pyfuse3.FileInfo(fh=fd) + + async def create(self, inode_p, name, mode, flags, ctx): + path = os.path.join(self._inode_to_path(inode_p), fsdecode(name)) + try: + fd = os.open(path, flags | os.O_CREAT | os.O_TRUNC) + os.fchmod(fd, mode) + os.fchown(fd, ctx.uid, ctx.gid) + except OSError as exc: + raise FUSEError(exc.errno) + attr = self._getattr(fd=fd) + self._add_path(attr.st_ino, path) + self._inode_fd_map[attr.st_ino] = fd + self._fd_inode_map[fd] = attr.st_ino + self._fd_open_count[fd] = 1 + return (pyfuse3.FileInfo(fh=fd), attr) + + async def read(self, fd, offset, length): + os.lseek(fd, offset, os.SEEK_SET) + return os.read(fd, length) + + async def write(self, fd, offset, buf): + os.lseek(fd, offset, os.SEEK_SET) + return os.write(fd, buf) + + async def release(self, fd): + if self._fd_open_count[fd] > 1: + self._fd_open_count[fd] -= 1 + return + + del self._fd_open_count[fd] + inode = self._fd_inode_map[fd] + del self._inode_fd_map[inode] + del self._fd_inode_map[fd] + try: + os.close(fd) + except OSError as exc: + raise FUSEError(exc.errno) + +def init_logging(debug=False): + formatter = logging.Formatter('%(asctime)s.%(msecs)03d %(threadName)s: ' + '[%(name)s] %(message)s', datefmt="%Y-%m-%d %H:%M:%S") + handler = logging.StreamHandler() + handler.setFormatter(formatter) + root_logger = logging.getLogger() + if debug: + handler.setLevel(logging.DEBUG) + root_logger.setLevel(logging.DEBUG) + else: + handler.setLevel(logging.INFO) + root_logger.setLevel(logging.INFO) + root_logger.addHandler(handler) + + +def parse_args(args): + '''Parse command line''' + + parser = ArgumentParser() + + parser.add_argument('source', type=str, + help='Directory tree to mirror') + parser.add_argument('mountpoint', type=str, + help='Where to mount the file system') + parser.add_argument('--debug', action='store_true', default=False, + help='Enable debugging output') + parser.add_argument('--debug-fuse', action='store_true', default=False, + help='Enable FUSE debugging output') + + return parser.parse_args(args) + +def main(): + options = parse_args(sys.argv[1:]) + init_logging(options.debug) + operations = Operations(options.source) + + log.debug('Mounting...') + fuse_options = set(pyfuse3.default_options) + fuse_options.add('fsname=passthroughfs') + fuse_options.add('allow_other') + fuse_options.add('suid') + fuse_options.add('dev') + if options.debug_fuse: + fuse_options.add('debug') + pyfuse3.init(operations, options.mountpoint, fuse_options) + + try: + log.debug('Entering main loop..') + trio.run(pyfuse3.main) + except: + pyfuse3.close(unmount=False) + raise + + log.debug('Unmounting..') + pyfuse3.close() + +if __name__ == '__main__': + main() + From 0c57d793f706efed3037ecd35cfb63bf7271a8c8 Mon Sep 17 00:00:00 2001 From: cki Date: Fri, 1 Dec 2023 11:18:24 +0100 Subject: [PATCH 2/2] overriden passthroughfs.py --- examples/ext_passthroughfs.py | 496 ---------------------------------- examples/passthroughfs.py | 15 + 2 files changed, 15 insertions(+), 496 deletions(-) delete mode 100755 examples/ext_passthroughfs.py diff --git a/examples/ext_passthroughfs.py b/examples/ext_passthroughfs.py deleted file mode 100755 index cda3804..0000000 --- a/examples/ext_passthroughfs.py +++ /dev/null @@ -1,496 +0,0 @@ -#!/usr/bin/env python3 -''' -ext filesystem compatibility checked by https://github.com/zfsonlinux/fstest.git -passthroughfs.py - Example file system for pyfuse3 - -This file system mirrors the contents of a specified directory tree. - -Caveats: - - * Inode generation numbers are not passed through but set to zero. - - * Block size (st_blksize) and number of allocated blocks (st_blocks) are not - passed through. - - * Performance for large directories is not good, because the directory - is always read completely. - - * There may be a way to break-out of the directory tree. - - * The readdir implementation is not fully POSIX compliant. If a directory - contains hardlinks and is modified during a readdir call, readdir() - may return some of the hardlinked files twice or omit them completely. - - * If you delete or rename files in the underlying file system, the - passthrough file system will get confused. - -Copyright © Nikolaus Rath - -Permission is hereby granted, free of charge, to any person obtaining a copy of -this software and associated documentation files (the "Software"), to deal in -the Software without restriction, including without limitation the rights to -use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of -the Software, and to permit persons to whom the Software is furnished to do so. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS -FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR -COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER -IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -''' - -import os -import sys - -# If we are running from the pyfuse3 source directory, try -# to load the module from there first. -basedir = os.path.abspath(os.path.join(os.path.dirname(sys.argv[0]), '..')) -if (os.path.exists(os.path.join(basedir, 'setup.py')) and - os.path.exists(os.path.join(basedir, 'src', 'pyfuse3.pyx'))): - sys.path.insert(0, os.path.join(basedir, 'src')) - -import pyfuse3 -from argparse import ArgumentParser -import errno -import logging -import stat as stat_m -from pyfuse3 import FUSEError -from os import fsencode, fsdecode -from collections import defaultdict -import trio - -import faulthandler -faulthandler.enable() - -log = logging.getLogger(__name__) - -class Operations(pyfuse3.Operations): - - enable_writeback_cache = True - - def __init__(self, source): - super().__init__() - self._inode_path_map = { pyfuse3.ROOT_INODE: source } - self._lookup_cnt = defaultdict(lambda : 0) - self._fd_inode_map = dict() - self._inode_fd_map = dict() - self._fd_open_count = dict() - - def _inode_to_path(self, inode): - try: - val = self._inode_path_map[inode] - except KeyError: - raise FUSEError(errno.ENOENT) - - if isinstance(val, set): - # In case of hardlinks, pick any path - val = next(iter(val)) - return val - - def _add_path(self, inode, path): - log.debug('_add_path for %d, %s', inode, path) - self._lookup_cnt[inode] += 1 - - # With hardlinks, one inode may map to multiple paths. - if inode not in self._inode_path_map: - self._inode_path_map[inode] = path - return - - val = self._inode_path_map[inode] - if isinstance(val, set): - val.add(path) - elif val != path: - self._inode_path_map[inode] = { path, val } - - async def forget(self, inode_list): - for (inode, nlookup) in inode_list: - if self._lookup_cnt[inode] > nlookup: - self._lookup_cnt[inode] -= nlookup - continue - log.debug('forgetting about inode %d', inode) - assert inode not in self._inode_fd_map - del self._lookup_cnt[inode] - try: - del self._inode_path_map[inode] - except KeyError: # may have been deleted - pass - - async def lookup(self, inode_p, name, ctx=None): - name = fsdecode(name) - log.debug('lookup for %s in %d', name, inode_p) - path = os.path.join(self._inode_to_path(inode_p), name) - attr = self._getattr(path=path) - if name != '.' and name != '..': - self._add_path(attr.st_ino, path) - return attr - - async def getattr(self, inode, ctx=None): - if inode in self._inode_fd_map: - return self._getattr(fd=self._inode_fd_map[inode]) - else: - return self._getattr(path=self._inode_to_path(inode)) - - def _getattr(self, path=None, fd=None): - assert fd is None or path is None - assert not(fd is None and path is None) - try: - if fd is None: - stat = os.lstat(path) - else: - stat = os.fstat(fd) - except OSError as exc: - raise FUSEError(exc.errno) - - entry = pyfuse3.EntryAttributes() - for attr in ('st_ino', 'st_mode', 'st_nlink', 'st_uid', 'st_gid', - 'st_rdev', 'st_size', 'st_atime_ns', 'st_mtime_ns', - 'st_ctime_ns'): - setattr(entry, attr, getattr(stat, attr)) - entry.generation = 0 - entry.entry_timeout = 0 - entry.attr_timeout = 0 - entry.st_blksize = 512 - entry.st_blocks = ((entry.st_size+entry.st_blksize-1) // entry.st_blksize) - - return entry - - async def readlink(self, inode, ctx): - path = self._inode_to_path(inode) - try: - target = os.readlink(path) - except OSError as exc: - raise FUSEError(exc.errno) - return fsencode(target) - - async def opendir(self, inode, ctx): - return inode - - async def readdir(self, inode, off, token): - path = self._inode_to_path(inode) - log.debug('reading %s', path) - entries = [] - for name in os.listdir(path): - if name == '.' or name == '..': - continue - attr = self._getattr(path=os.path.join(path, name)) - entries.append((attr.st_ino, name, attr)) - - log.debug('read %d entries, starting at %d', len(entries), off) - - # This is not fully posix compatible. If there are hardlinks - # (two names with the same inode), we don't have a unique - # offset to start in between them. Note that we cannot simply - # count entries, because then we would skip over entries - # (or return them more than once) if the number of directory - # entries changes between two calls to readdir(). - for (ino, name, attr) in sorted(entries): - if ino <= off: - continue - if not pyfuse3.readdir_reply( - token, fsencode(name), attr, ino): - break - self._add_path(attr.st_ino, os.path.join(path, name)) - - async def unlink(self, inode_p, name, ctx): - name = fsdecode(name) - parent = self._inode_to_path(inode_p) - path = os.path.join(parent, name) - try: - inode = os.lstat(path).st_ino - os.unlink(path) - except OSError as exc: - raise FUSEError(exc.errno) - if inode in self._lookup_cnt: - self._forget_path(inode, path) - - async def rmdir(self, inode_p, name, ctx): - name = fsdecode(name) - parent = self._inode_to_path(inode_p) - path = os.path.join(parent, name) - try: - inode = os.lstat(path).st_ino - os.rmdir(path) - except OSError as exc: - raise FUSEError(exc.errno) - if inode in self._lookup_cnt: - self._forget_path(inode, path) - - def _forget_path(self, inode, path): - log.debug('forget %s for %d', path, inode) - val = self._inode_path_map[inode] - if isinstance(val, set): - val.remove(path) - if len(val) == 1: - self._inode_path_map[inode] = next(iter(val)) - else: - del self._inode_path_map[inode] - - async def symlink(self, inode_p, name, target, ctx): - name = fsdecode(name) - target = fsdecode(target) - parent = self._inode_to_path(inode_p) - path = os.path.join(parent, name) - try: - os.symlink(target, path) - os.chown(path, ctx.uid, ctx.gid, follow_symlinks=False) - except OSError as exc: - raise FUSEError(exc.errno) - stat = os.lstat(path) - self._add_path(stat.st_ino, path) - return await self.getattr(stat.st_ino) - - async def rename(self, inode_p_old, name_old, inode_p_new, name_new, - flags, ctx): - if flags != 0: - raise FUSEError(errno.EINVAL) - - name_old = fsdecode(name_old) - name_new = fsdecode(name_new) - parent_old = self._inode_to_path(inode_p_old) - parent_new = self._inode_to_path(inode_p_new) - path_old = os.path.join(parent_old, name_old) - path_new = os.path.join(parent_new, name_new) - try: - os.rename(path_old, path_new) - inode = os.lstat(path_new).st_ino - except OSError as exc: - raise FUSEError(exc.errno) - if inode not in self._lookup_cnt: - return - - val = self._inode_path_map[inode] - if isinstance(val, set): - assert len(val) > 1 - val.add(path_new) - val.remove(path_old) - else: - assert val == path_old - self._inode_path_map[inode] = path_new - - async def link(self, inode, new_inode_p, new_name, ctx): - new_name = fsdecode(new_name) - parent = self._inode_to_path(new_inode_p) - path = os.path.join(parent, new_name) - try: - os.link(self._inode_to_path(inode), path, follow_symlinks=False) - except OSError as exc: - raise FUSEError(exc.errno) - self._add_path(inode, path) - return await self.getattr(inode) - - async def setattr(self, inode, attr, fields, fh, ctx): - # We use the f* functions if possible so that we can handle - # a setattr() call for an inode without associated directory - # handle. - if fh is None: - path_or_fh = self._inode_to_path(inode) - truncate = os.truncate - chmod = os.chmod - chown = os.chown - stat = os.lstat - else: - path_or_fh = fh - truncate = os.ftruncate - chmod = os.fchmod - chown = os.fchown - stat = os.fstat - - try: - if fields.update_size: - truncate(path_or_fh, attr.st_size) - - if fields.update_mode: - # Under Linux, chmod always resolves symlinks so we should - # actually never get a setattr() request for a symbolic - # link. - assert not stat_m.S_ISLNK(attr.st_mode) - chmod(path_or_fh, stat_m.S_IMODE(attr.st_mode)) - - if fields.update_uid: - chown(path_or_fh, attr.st_uid, -1, follow_symlinks=False) - - if fields.update_gid: - chown(path_or_fh, -1, attr.st_gid, follow_symlinks=False) - - if fields.update_mode is False and \ - fields.update_uid is False and \ - fields.update_gid is False: - if chown == os.chown: - chown(path_or_fh, -1, -1, follow_symlinks=False) - if chown == os.fchown: - chown(path_or_fh, -1, -1) - - if fields.update_atime and fields.update_mtime: - if fh is None: - os.utime(path_or_fh, None, follow_symlinks=False, - ns=(attr.st_atime_ns, attr.st_mtime_ns)) - else: - os.utime(path_or_fh, None, - ns=(attr.st_atime_ns, attr.st_mtime_ns)) - elif fields.update_atime or fields.update_mtime: - # We can only set both values, so we first need to retrieve the - # one that we shouldn't be changing. - oldstat = stat(path_or_fh) - if not fields.update_atime: - attr.st_atime_ns = oldstat.st_atime_ns - else: - attr.st_mtime_ns = oldstat.st_mtime_ns - if fh is None: - os.utime(path_or_fh, None, follow_symlinks=False, - ns=(attr.st_atime_ns, attr.st_mtime_ns)) - else: - os.utime(path_or_fh, None, - ns=(attr.st_atime_ns, attr.st_mtime_ns)) - - except OSError as exc: - raise FUSEError(exc.errno) - - return await self.getattr(inode) - - async def mknod(self, inode_p, name, mode, rdev, ctx): - path = os.path.join(self._inode_to_path(inode_p), fsdecode(name)) - try: - os.mknod(path, mode=(mode & ~ctx.umask), device=rdev) - os.chown(path, ctx.uid, ctx.gid) - except OSError as exc: - raise FUSEError(exc.errno) - attr = self._getattr(path=path) - self._add_path(attr.st_ino, path) - return attr - - async def mkdir(self, inode_p, name, mode, ctx): - path = os.path.join(self._inode_to_path(inode_p), fsdecode(name)) - try: - os.mkdir(path, mode=(mode & ~ctx.umask)) - os.chown(path, ctx.uid, ctx.gid) - except OSError as exc: - raise FUSEError(exc.errno) - attr = self._getattr(path=path) - self._add_path(attr.st_ino, path) - return attr - - async def statfs(self, ctx): - root = self._inode_path_map[pyfuse3.ROOT_INODE] - stat_ = pyfuse3.StatvfsData() - try: - statfs = os.statvfs(root) - except OSError as exc: - raise FUSEError(exc.errno) - for attr in ('f_bsize', 'f_frsize', 'f_blocks', 'f_bfree', 'f_bavail', - 'f_files', 'f_ffree', 'f_favail'): - setattr(stat_, attr, getattr(statfs, attr)) - stat_.f_namemax = statfs.f_namemax - (len(root)+1) - return stat_ - - async def open(self, inode, flags, ctx): - if inode in self._inode_fd_map: - fd = self._inode_fd_map[inode] - self._fd_open_count[fd] += 1 - return pyfuse3.FileInfo(fh=fd) - assert flags & os.O_CREAT == 0 - try: - fd = os.open(self._inode_to_path(inode), flags) - except OSError as exc: - raise FUSEError(exc.errno) - self._inode_fd_map[inode] = fd - self._fd_inode_map[fd] = inode - self._fd_open_count[fd] = 1 - return pyfuse3.FileInfo(fh=fd) - - async def create(self, inode_p, name, mode, flags, ctx): - path = os.path.join(self._inode_to_path(inode_p), fsdecode(name)) - try: - fd = os.open(path, flags | os.O_CREAT | os.O_TRUNC) - os.fchmod(fd, mode) - os.fchown(fd, ctx.uid, ctx.gid) - except OSError as exc: - raise FUSEError(exc.errno) - attr = self._getattr(fd=fd) - self._add_path(attr.st_ino, path) - self._inode_fd_map[attr.st_ino] = fd - self._fd_inode_map[fd] = attr.st_ino - self._fd_open_count[fd] = 1 - return (pyfuse3.FileInfo(fh=fd), attr) - - async def read(self, fd, offset, length): - os.lseek(fd, offset, os.SEEK_SET) - return os.read(fd, length) - - async def write(self, fd, offset, buf): - os.lseek(fd, offset, os.SEEK_SET) - return os.write(fd, buf) - - async def release(self, fd): - if self._fd_open_count[fd] > 1: - self._fd_open_count[fd] -= 1 - return - - del self._fd_open_count[fd] - inode = self._fd_inode_map[fd] - del self._inode_fd_map[inode] - del self._fd_inode_map[fd] - try: - os.close(fd) - except OSError as exc: - raise FUSEError(exc.errno) - -def init_logging(debug=False): - formatter = logging.Formatter('%(asctime)s.%(msecs)03d %(threadName)s: ' - '[%(name)s] %(message)s', datefmt="%Y-%m-%d %H:%M:%S") - handler = logging.StreamHandler() - handler.setFormatter(formatter) - root_logger = logging.getLogger() - if debug: - handler.setLevel(logging.DEBUG) - root_logger.setLevel(logging.DEBUG) - else: - handler.setLevel(logging.INFO) - root_logger.setLevel(logging.INFO) - root_logger.addHandler(handler) - - -def parse_args(args): - '''Parse command line''' - - parser = ArgumentParser() - - parser.add_argument('source', type=str, - help='Directory tree to mirror') - parser.add_argument('mountpoint', type=str, - help='Where to mount the file system') - parser.add_argument('--debug', action='store_true', default=False, - help='Enable debugging output') - parser.add_argument('--debug-fuse', action='store_true', default=False, - help='Enable FUSE debugging output') - - return parser.parse_args(args) - -def main(): - options = parse_args(sys.argv[1:]) - init_logging(options.debug) - operations = Operations(options.source) - - log.debug('Mounting...') - fuse_options = set(pyfuse3.default_options) - fuse_options.add('fsname=passthroughfs') - fuse_options.add('allow_other') - fuse_options.add('suid') - fuse_options.add('dev') - if options.debug_fuse: - fuse_options.add('debug') - pyfuse3.init(operations, options.mountpoint, fuse_options) - - try: - log.debug('Entering main loop..') - trio.run(pyfuse3.main) - except: - pyfuse3.close(unmount=False) - raise - - log.debug('Unmounting..') - pyfuse3.close() - -if __name__ == '__main__': - main() - diff --git a/examples/passthroughfs.py b/examples/passthroughfs.py index ea904e4..cda3804 100755 --- a/examples/passthroughfs.py +++ b/examples/passthroughfs.py @@ -1,5 +1,6 @@ #!/usr/bin/env python3 ''' +ext filesystem compatibility checked by https://github.com/zfsonlinux/fstest.git passthroughfs.py - Example file system for pyfuse3 This file system mirrors the contents of a specified directory tree. @@ -312,6 +313,14 @@ async def setattr(self, inode, attr, fields, fh, ctx): if fields.update_gid: chown(path_or_fh, -1, attr.st_gid, follow_symlinks=False) + if fields.update_mode is False and \ + fields.update_uid is False and \ + fields.update_gid is False: + if chown == os.chown: + chown(path_or_fh, -1, -1, follow_symlinks=False) + if chown == os.fchown: + chown(path_or_fh, -1, -1) + if fields.update_atime and fields.update_mtime: if fh is None: os.utime(path_or_fh, None, follow_symlinks=False, @@ -393,6 +402,8 @@ async def create(self, inode_p, name, mode, flags, ctx): path = os.path.join(self._inode_to_path(inode_p), fsdecode(name)) try: fd = os.open(path, flags | os.O_CREAT | os.O_TRUNC) + os.fchmod(fd, mode) + os.fchown(fd, ctx.uid, ctx.gid) except OSError as exc: raise FUSEError(exc.errno) attr = self._getattr(fd=fd) @@ -463,6 +474,9 @@ def main(): log.debug('Mounting...') fuse_options = set(pyfuse3.default_options) fuse_options.add('fsname=passthroughfs') + fuse_options.add('allow_other') + fuse_options.add('suid') + fuse_options.add('dev') if options.debug_fuse: fuse_options.add('debug') pyfuse3.init(operations, options.mountpoint, fuse_options) @@ -479,3 +493,4 @@ def main(): if __name__ == '__main__': main() +