Example File Systems — pyfuse3 3.4.3.dev19+g57a14ee07.d20260509 documentation

# Example File Systems

pyfuse3 comes with several example file systems in the `examples` directory of the release tarball. For completeness, these examples are also included here.

## Single-file, Read-only File System

(shipped as `examples/lltest.py`)

      1#!/usr/bin/env python3
      2# -*- coding: utf-8 -*-
      3'''
      4hello.py - Example file system for pyfuse3.
      5
      6This program presents a static file system containing a single file.
      7
      8Copyright © 2015 Nikolaus Rath <Nikolaus.org>
      9Copyright © 2015 Gerion Entrup.
     10
     11Permission is hereby granted, free of charge, to any person obtaining a copy of
     12this software and associated documentation files (the "Software"), to deal in
     13the Software without restriction, including without limitation the rights to
     14use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
     15the Software, and to permit persons to whom the Software is furnished to do so.
     16
     17THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
     18IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
     19FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
     20COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
     21IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
     22CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
     23'''
     24
     25import errno
     26import logging
     27import os
     28import stat
     29from argparse import ArgumentParser, Namespace
     30from typing import cast
     31
     32import trio
     33
     34import pyfuse3
     35from pyfuse3 import EntryAttributes, FileHandleT, FileInfo, InodeT, ReaddirToken, RequestContext
     36
     37try:
     38    import faulthandler
     39except ImportError:
     40    pass
     41else:
     42    faulthandler.enable()
     43
     44log = logging.getLogger(__name__)
     45
     46
     47class TestFs(pyfuse3.Operations):
     48    def __init__(self) -> None:
     49        super(TestFs, self).__init__()
     50        self.hello_name = b"message"
     51        self.hello_inode = pyfuse3.ROOT_INODE + 1
     52        self.hello_data = b"hello world\n"
     53
     54    async def getattr(self, inode: InodeT, ctx: RequestContext | None = None) -> EntryAttributes:
     55        entry = EntryAttributes()
     56        if inode == pyfuse3.ROOT_INODE:
     57            entry.st_mode = stat.S_IFDIR | 0o755
     58            entry.st_size = 0
     59        elif inode == self.hello_inode:
     60            entry.st_mode = stat.S_IFREG | 0o644
     61            entry.st_size = len(self.hello_data)
     62        else:
     63            raise pyfuse3.FUSEError(errno.ENOENT)
     64
     65        stamp = int(1438467123.985654 * 1e9)
     66        entry.st_atime_ns = stamp
     67        entry.st_ctime_ns = stamp
     68        entry.st_mtime_ns = stamp
     69        entry.st_gid = os.getgid()
     70        entry.st_uid = os.getuid()
     71        entry.st_ino = inode
     72
     73        return entry
     74
     75    async def lookup(
     76        self, parent_inode: InodeT, name: bytes, ctx: RequestContext
     77    ) -> EntryAttributes:
     78        if parent_inode != pyfuse3.ROOT_INODE or name != self.hello_name:
     79            raise pyfuse3.FUSEError(errno.ENOENT)
     80        return await self.getattr(self.hello_inode, ctx)
     81
     82    async def opendir(self, inode: InodeT, ctx: RequestContext) -> FileHandleT:
     83        if inode != pyfuse3.ROOT_INODE:
     84            raise pyfuse3.FUSEError(errno.ENOENT)
     85        # For simplicity, we use the inode as file handle
     86        return FileHandleT(inode)
     87
     88    async def readdir(self, fh: FileHandleT, start_id: int, token: ReaddirToken) -> None:
     89        assert fh == pyfuse3.ROOT_INODE
     90
     91        # only one entry
     92        if start_id == 0:
     93            pyfuse3.readdir_reply(token, self.hello_name, await self.getattr(self.hello_inode), 1)
     94        return
     95
     96    async def open(self, inode: InodeT, flags: int, ctx: RequestContext) -> FileInfo:
     97        if inode != self.hello_inode:
     98            raise pyfuse3.FUSEError(errno.ENOENT)
     99        if flags & os.O_RDWR or flags & os.O_WRONLY:
    100            raise pyfuse3.FUSEError(errno.EACCES)
    101        # For simplicity, we use the inode as file handle
    102        return FileInfo(fh=FileHandleT(inode))
    103
    104    async def read(self, fh: FileHandleT, off: int, size: int) -> bytes:
    105        assert fh == self.hello_inode
    106        return self.hello_data[off : off + size]
    107
    108
    109def init_logging(debug: bool = False) -> None:
    110    formatter = logging.Formatter(
    111        '%(asctime)s.%(msecs)03d %(threadName)s: [%(name)s] %(message)s',
    112        datefmt="%Y-%m-%d %H:%M:%S",
    113    )
    114    handler = logging.StreamHandler()
    115    handler.setFormatter(formatter)
    116    root_logger = logging.getLogger()
    117    if debug:
    118        handler.setLevel(logging.DEBUG)
    119        root_logger.setLevel(logging.DEBUG)
    120    else:
    121        handler.setLevel(logging.INFO)
    122        root_logger.setLevel(logging.INFO)
    123    root_logger.addHandler(handler)
    124
    125
    126def parse_args() -> Namespace:
    127    '''Parse command line'''
    128
    129    parser = ArgumentParser()
    130
    131    parser.add_argument('mountpoint', type=str, help='Where to mount the file system')
    132    parser.add_argument(
    133        '--debug', action='store_true', default=False, help='Enable debugging output'
    134    )
    135    parser.add_argument(
    136        '--debug-fuse', action='store_true', default=False, help='Enable FUSE debugging output'
    137    )
    138    return parser.parse_args()
    139
    140
    141def main() -> None:
    142    options = parse_args()
    143    init_logging(options.debug)
    144
    145    testfs = TestFs()
    146    fuse_options = set(pyfuse3.default_options)
    147    fuse_options.add('fsname=hello')
    148    if options.debug_fuse:
    149        fuse_options.add('debug')
    150    pyfuse3.init(testfs, options.mountpoint, fuse_options)
    151    try:
    152        trio.run(pyfuse3.main)
    153    except:
    154        pyfuse3.close(unmount=False)
    155        raise
    156
    157    pyfuse3.close()
    158
    159
    160if __name__ == '__main__':
    161    main()

## In-memory File System

(shipped as `examples/tmpfs.py`)

      1#!/usr/bin/env python3
      2# -*- coding: utf-8 -*-
      3'''
      4tmpfs.py - Example file system for pyfuse3.
      5
      6This file system stores all data in memory.
      7
      8Copyright © 2013 Nikolaus Rath <Nikolaus.org>
      9
     10Permission is hereby granted, free of charge, to any person obtaining a copy of
     11this software and associated documentation files (the "Software"), to deal in
     12the Software without restriction, including without limitation the rights to
     13use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
     14the Software, and to permit persons to whom the Software is furnished to do so.
     15
     16THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
     17IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
     18FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
     19COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
     20IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
     21CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
     22'''
     23
     24import errno
     25import logging
     26import os
     27import sqlite3
     28import stat
     29from argparse import ArgumentParser, Namespace
     30from collections import defaultdict
     31from time import time
     32from typing import Any, cast
     33
     34import trio
     35
     36import pyfuse3
     37from pyfuse3 import (
     38    EntryAttributes,
     39    FileHandleT,
     40    FileInfo,
     41    FUSEError,
     42    InodeT,
     43    ReaddirToken,
     44    RequestContext,
     45    SetattrFields,
     46    StatvfsData,
     47)
     48
     49try:
     50    import faulthandler
     51except ImportError:
     52    pass
     53else:
     54    faulthandler.enable()
     55
     56log = logging.getLogger()
     57
     58
     59class Operations(pyfuse3.Operations):
     60    '''An example filesystem that stores all data in memory
     61
     62    This is a very simple implementation with terrible performance.
     63    Don't try to store significant amounts of data. Also, there are
     64    some other flaws that have not been fixed to keep the code easier
     65    to understand:
     66
     67    * atime, mtime and ctime are not updated
     68    * generation numbers are not supported
     69    * lookup counts are not maintained
     70    '''
     71
     72    enable_writeback_cache = True
     73
     74    def __init__(self) -> None:
     75        super(Operations, self).__init__()
     76        self.db: sqlite3.Connection = sqlite3.connect(':memory:')
     77        self.db.text_factory = str
     78        self.db.row_factory = sqlite3.Row
     79        self.cursor: sqlite3.Cursor = self.db.cursor()
     80        self.inode_open_count: defaultdict[InodeT, int] = defaultdict(int)
     81        self.init_tables()
     82
     83    def init_tables(self) -> None:
     84        '''Initialize file system tables'''
     85
     86        self.cursor.execute("""
     87        CREATE TABLE inodes (
     88            id        INTEGER PRIMARY KEY,
     89            uid       INT NOT NULL,
     90            gid       INT NOT NULL,
     91            mode      INT NOT NULL,
     92            mtime_ns  INT NOT NULL,
     93            atime_ns  INT NOT NULL,
     94            ctime_ns  INT NOT NULL,
     95            target    BLOB(256) ,
     96            size      INT NOT NULL DEFAULT 0,
     97            rdev      INT NOT NULL DEFAULT 0,
     98            data      BLOB
     99        )
    100        """)
    101
    102        self.cursor.execute("""
    103        CREATE TABLE contents (
    104            rowid     INTEGER PRIMARY KEY AUTOINCREMENT,
    105            name      BLOB(256) NOT NULL,
    106            inode     INT NOT NULL REFERENCES inodes(id),
    107            parent_inode INT NOT NULL REFERENCES inodes(id),
    108
    109            UNIQUE (name, parent_inode)
    110        )""")
    111
    112        # Insert root directory
    113        now_ns = int(time() * 1e9)
    114        self.cursor.execute(
    115            "INSERT INTO inodes (id,mode,uid,gid,mtime_ns,atime_ns,ctime_ns) "
    116            "VALUES (?,?,?,?,?,?,?)",
    117            (
    118                pyfuse3.ROOT_INODE,
    119                stat.S_IFDIR
    120                | stat.S_IRUSR
    121                | stat.S_IWUSR
    122                | stat.S_IXUSR
    123                | stat.S_IRGRP
    124                | stat.S_IXGRP
    125                | stat.S_IROTH
    126                | stat.S_IXOTH,
    127                os.getuid(),
    128                os.getgid(),
    129                now_ns,
    130                now_ns,
    131                now_ns,
    132            ),
    133        )
    134        self.cursor.execute(
    135            "INSERT INTO contents (name, parent_inode, inode) VALUES (?,?,?)",
    136            (b'..', pyfuse3.ROOT_INODE, pyfuse3.ROOT_INODE),
    137        )
    138
    139    def get_row(self, *a: Any, **kw: Any) -> sqlite3.Row:
    140        self.cursor.execute(*a, **kw)
    141        try:
    142            row = next(self.cursor)
    143        except StopIteration:
    144            raise NoSuchRowError()
    145        try:
    146            next(self.cursor)
    147        except StopIteration:
    148            pass
    149        else:
    150            raise NoUniqueValueError()
    151
    152        return row
    153
    154    async def lookup(
    155        self, parent_inode: InodeT, name: bytes, ctx: RequestContext
    156    ) -> EntryAttributes:
    157        if name == b'.':
    158            inode = parent_inode
    159        elif name == b'..':
    160            inode = self.get_row("SELECT * FROM contents WHERE inode=?", (parent_inode,))[
    161                'parent_inode'
    162            ]
    163        else:
    164            try:
    165                inode = self.get_row(
    166                    "SELECT * FROM contents WHERE name=? AND parent_inode=?", (name, parent_inode)
    167                )['inode']
    168            except NoSuchRowError:
    169                raise (pyfuse3.FUSEError(errno.ENOENT))
    170
    171        return await self.getattr(InodeT(inode), ctx)
    172
    173    async def getattr(self, inode: InodeT, ctx: RequestContext | None = None) -> EntryAttributes:
    174        try:
    175            row = self.get_row("SELECT * FROM inodes WHERE id=?", (inode,))
    176        except NoSuchRowError:
    177            raise (pyfuse3.FUSEError(errno.ENOENT))
    178
    179        entry = EntryAttributes()
    180        entry.st_ino = inode
    181        entry.generation = 0
    182        entry.entry_timeout = 300
    183        entry.attr_timeout = 300
    184        entry.st_mode = row['mode']
    185        entry.st_nlink = self.get_row("SELECT COUNT(inode) FROM contents WHERE inode=?", (inode,))[
    186            0
    187        ]
    188        entry.st_uid = row['uid']
    189        entry.st_gid = row['gid']
    190        entry.st_rdev = row['rdev']
    191        entry.st_size = row['size']
    192
    193        entry.st_blksize = 512
    194        entry.st_blocks = 1
    195        entry.st_atime_ns = row['atime_ns']
    196        entry.st_mtime_ns = row['mtime_ns']
    197        entry.st_ctime_ns = row['ctime_ns']
    198
    199        return entry
    200
    201    async def readlink(self, inode: InodeT, ctx: RequestContext) -> bytes:
    202        return self.get_row('SELECT * FROM inodes WHERE id=?', (inode,))['target']
    203
    204    async def opendir(self, inode: InodeT, ctx: RequestContext) -> FileHandleT:
    205        # For simplicity, we use the inode as file handle
    206        return FileHandleT(inode)
    207
    208    async def readdir(self, fh: FileHandleT, start_id: int, token: ReaddirToken) -> None:
    209        if start_id == 0:
    210            off = -1
    211        else:
    212            off = start_id
    213
    214        cursor2 = self.db.cursor()
    215        cursor2.execute(
    216            "SELECT * FROM contents WHERE parent_inode=? AND rowid > ? ORDER BY rowid", (fh, off)
    217        )
    218
    219        for row in cursor2:
    220            pyfuse3.readdir_reply(
    221                token, row['name'], await self.getattr(InodeT(row['inode'])), row['rowid']
    222            )
    223
    224    async def unlink(self, parent_inode: InodeT, name: bytes, ctx: RequestContext) -> None:
    225        entry = await self.lookup(parent_inode, name, ctx)
    226
    227        if stat.S_ISDIR(entry.st_mode):
    228            raise pyfuse3.FUSEError(errno.EISDIR)
    229
    230        self._remove(parent_inode, name, entry)
    231
    232    async def rmdir(self, parent_inode: InodeT, name: bytes, ctx: RequestContext) -> None:
    233        entry = await self.lookup(parent_inode, name, ctx)
    234
    235        if not stat.S_ISDIR(entry.st_mode):
    236            raise pyfuse3.FUSEError(errno.ENOTDIR)
    237
    238        self._remove(parent_inode, name, entry)
    239
    240    def _remove(self, parent_inode: InodeT, name: bytes, entry: EntryAttributes) -> None:
    241        if (
    242            self.get_row("SELECT COUNT(inode) FROM contents WHERE parent_inode=?", (entry.st_ino,))[
    243                0
    244            ]
    245            > 0
    246        ):
    247            raise pyfuse3.FUSEError(errno.ENOTEMPTY)
    248
    249        self.cursor.execute(
    250            "DELETE FROM contents WHERE name=? AND parent_inode=?", (name, parent_inode)
    251        )
    252
    253        if entry.st_nlink == 1 and entry.st_ino not in self.inode_open_count:
    254            self.cursor.execute("DELETE FROM inodes WHERE id=?", (entry.st_ino,))
    255
    256    async def symlink(
    257        self, parent_inode: InodeT, name: bytes, target: bytes, ctx: RequestContext
    258    ) -> EntryAttributes:
    259        mode = (
    260            stat.S_IFLNK
    261            | stat.S_IRUSR
    262            | stat.S_IWUSR
    263            | stat.S_IXUSR
    264            | stat.S_IRGRP
    265            | stat.S_IWGRP
    266            | stat.S_IXGRP
    267            | stat.S_IROTH
    268            | stat.S_IWOTH
    269            | stat.S_IXOTH
    270        )
    271        return await self._create(parent_inode, name, mode, ctx, target=target)
    272
    273    async def rename(
    274        self,
    275        parent_inode_old: InodeT,
    276        name_old: bytes,
    277        parent_inode_new: InodeT,
    278        name_new: bytes,
    279        flags: int,
    280        ctx: RequestContext,
    281    ) -> None:
    282        if flags != 0:
    283            raise FUSEError(errno.EINVAL)
    284
    285        entry_old = await self.lookup(parent_inode_old, name_old, ctx)
    286
    287        entry_new = None
    288        try:
    289            entry_new = await self.lookup(
    290                parent_inode_new,
    291                name_new if isinstance(name_new, bytes) else name_new.encode(),
    292                ctx,
    293            )
    294        except pyfuse3.FUSEError as exc:
    295            if exc.errno != errno.ENOENT:
    296                raise
    297
    298        if entry_new is not None:
    299            self._replace(
    300                parent_inode_old, name_old, parent_inode_new, name_new, entry_old, entry_new
    301            )
    302        else:
    303            self.cursor.execute(
    304                "UPDATE contents SET name=?, parent_inode=? WHERE name=? AND parent_inode=?",
    305                (name_new, parent_inode_new, name_old, parent_inode_old),
    306            )
    307
    308    def _replace(
    309        self,
    310        parent_inode_old: InodeT,
    311        name_old: bytes,
    312        parent_inode_new: InodeT,
    313        name_new: bytes,
    314        entry_old: EntryAttributes,
    315        entry_new: EntryAttributes,
    316    ) -> None:
    317        if (
    318            self.get_row(
    319                "SELECT COUNT(inode) FROM contents WHERE parent_inode=?", (entry_new.st_ino,)
    320            )[0]
    321            > 0
    322        ):
    323            raise pyfuse3.FUSEError(errno.ENOTEMPTY)
    324
    325        self.cursor.execute(
    326            "UPDATE contents SET inode=? WHERE name=? AND parent_inode=?",
    327            (entry_old.st_ino, name_new, parent_inode_new),
    328        )
    329        self.db.execute(
    330            'DELETE FROM contents WHERE name=? AND parent_inode=?', (name_old, parent_inode_old)
    331        )
    332
    333        if entry_new.st_nlink == 1 and entry_new.st_ino not in self.inode_open_count:
    334            self.cursor.execute("DELETE FROM inodes WHERE id=?", (entry_new.st_ino,))
    335
    336    async def link(
    337        self, inode: InodeT, new_parent_inode: InodeT, new_name: bytes, ctx: RequestContext
    338    ) -> EntryAttributes:
    339        entry_p = await self.getattr(new_parent_inode, ctx)
    340        if entry_p.st_nlink == 0:
    341            log.warning(
    342                'Attempted to create entry %s with unlinked parent %d', new_name, new_parent_inode
    343            )
    344            raise FUSEError(errno.EINVAL)
    345
    346        self.cursor.execute(
    347            "INSERT INTO contents (name, inode, parent_inode) VALUES(?,?,?)",
    348            (new_name, inode, new_parent_inode),
    349        )
    350
    351        return await self.getattr(inode, ctx)
    352
    353    async def setattr(
    354        self,
    355        inode: InodeT,
    356        attr: EntryAttributes,
    357        fields: SetattrFields,
    358        fh: FileHandleT | None,
    359        ctx: RequestContext,
    360    ) -> EntryAttributes:
    361        if fields.update_size:
    362            data = self.get_row('SELECT data FROM inodes WHERE id=?', (inode,))[0]
    363            if data is None:
    364                data = b''
    365            if len(data) < attr.st_size:
    366                data = data + b'\0' * (attr.st_size - len(data))
    367            else:
    368                data = data[: attr.st_size]
    369            self.cursor.execute(
    370                'UPDATE inodes SET data=?, size=? WHERE id=?',
    371                (memoryview(data), attr.st_size, inode),
    372            )
    373        if fields.update_mode:
    374            self.cursor.execute('UPDATE inodes SET mode=? WHERE id=?', (attr.st_mode, inode))
    375
    376        if fields.update_uid:
    377            self.cursor.execute('UPDATE inodes SET uid=? WHERE id=?', (attr.st_uid, inode))
    378
    379        if fields.update_gid:
    380            self.cursor.execute('UPDATE inodes SET gid=? WHERE id=?', (attr.st_gid, inode))
    381
    382        if fields.update_atime:
    383            self.cursor.execute(
    384                'UPDATE inodes SET atime_ns=? WHERE id=?', (attr.st_atime_ns, inode)
    385            )
    386
    387        if fields.update_mtime:
    388            self.cursor.execute(
    389                'UPDATE inodes SET mtime_ns=? WHERE id=?', (attr.st_mtime_ns, inode)
    390            )
    391
    392        if fields.update_ctime:
    393            self.cursor.execute(
    394                'UPDATE inodes SET ctime_ns=? WHERE id=?', (attr.st_ctime_ns, inode)
    395            )
    396        else:
    397            self.cursor.execute(
    398                'UPDATE inodes SET ctime_ns=? WHERE id=?', (int(time() * 1e9), inode)
    399            )
    400
    401        return await self.getattr(inode, ctx)
    402
    403    async def mknod(
    404        self, parent_inode: InodeT, name: bytes, mode: int, rdev: int, ctx: RequestContext
    405    ) -> EntryAttributes:
    406        return await self._create(parent_inode, name, mode, ctx, rdev=rdev)
    407
    408    async def mkdir(
    409        self, parent_inode: InodeT, name: bytes, mode: int, ctx: RequestContext
    410    ) -> EntryAttributes:
    411        return await self._create(parent_inode, name, mode, ctx)
    412
    413    async def statfs(self, ctx: RequestContext) -> StatvfsData:
    414        stat_ = StatvfsData()
    415
    416        stat_.f_bsize = 512
    417        stat_.f_frsize = 512
    418
    419        size = self.get_row('SELECT SUM(size) FROM inodes')[0]
    420        stat_.f_blocks = size // stat_.f_frsize
    421        stat_.f_bfree = max(size // stat_.f_frsize, 1024)
    422        stat_.f_bavail = stat_.f_bfree
    423
    424        inodes = self.get_row('SELECT COUNT(id) FROM inodes')[0]
    425        stat_.f_files = inodes
    426        stat_.f_ffree = max(inodes, 100)
    427        stat_.f_favail = stat_.f_ffree
    428
    429        return stat_
    430
    431    async def open(self, inode: InodeT, flags: int, ctx: RequestContext) -> FileInfo:
    432        self.inode_open_count[inode] += 1
    433
    434        # For simplicity, we use the inode as file handle
    435        return FileInfo(fh=FileHandleT(inode))
    436
    437    async def access(self, inode: InodeT, mode: int, ctx: RequestContext) -> bool:
    438        # Yeah, could be a function and has unused arguments
    439        # pylint: disable=R0201,W0613
    440        return True
    441
    442    async def create(
    443        self, parent_inode: InodeT, name: bytes, mode: int, flags: int, ctx: RequestContext
    444    ) -> tuple[FileInfo, EntryAttributes]:
    445        # pylint: disable=W0612
    446        entry = await self._create(parent_inode, name, mode, ctx)
    447        self.inode_open_count[entry.st_ino] += 1
    448        # For simplicity, we use the inode as file handle
    449        return (FileInfo(fh=FileHandleT(entry.st_ino)), entry)
    450
    451    async def _create(
    452        self,
    453        parent_inode: InodeT,
    454        name: bytes,
    455        mode: int,
    456        ctx: RequestContext,
    457        rdev: int = 0,
    458        target: bytes | None = None,
    459    ) -> EntryAttributes:
    460        if (await self.getattr(parent_inode, ctx)).st_nlink == 0:
    461            log.warning('Attempted to create entry %s with unlinked parent %d', name, parent_inode)
    462            raise FUSEError(errno.EINVAL)
    463
    464        now_ns = int(time() * 1e9)
    465        self.cursor.execute(
    466            'INSERT INTO inodes (uid, gid, mode, mtime_ns, atime_ns, '
    467            'ctime_ns, target, rdev) VALUES(?, ?, ?, ?, ?, ?, ?, ?)',
    468            (ctx.uid, ctx.gid, mode, now_ns, now_ns, now_ns, target, rdev),
    469        )
    470
    471        inode = cast(InodeT, self.cursor.lastrowid)
    472        self.db.execute(
    473            "INSERT INTO contents(name, inode, parent_inode) VALUES(?,?,?)",
    474            (name, inode, parent_inode),
    475        )
    476        return await self.getattr(inode, ctx)
    477
    478    async def read(self, fh: FileHandleT, off: int, size: int) -> bytes:
    479        data = self.get_row('SELECT data FROM inodes WHERE id=?', (fh,))[0]
    480        if data is None:
    481            data = b''
    482        return data[off : off + size]
    483
    484    async def write(self, fh: FileHandleT, off: int, buf: bytes) -> int:
    485        data = self.get_row('SELECT data FROM inodes WHERE id=?', (fh,))[0]
    486        if data is None:
    487            data = b''
    488        data = data[:off] + buf + data[off + len(buf) :]
    489
    490        self.cursor.execute(
    491            'UPDATE inodes SET data=?, size=? WHERE id=?', (memoryview(data), len(data), fh)
    492        )
    493        return len(buf)
    494
    495    async def release(self, fh: FileHandleT) -> None:
    496        inode = fh
    497        self.inode_open_count[inode] -= 1
    498
    499        if self.inode_open_count[inode] == 0:
    500            del self.inode_open_count[inode]
    501            if (await self.getattr(inode)).st_nlink == 0:
    502                self.cursor.execute("DELETE FROM inodes WHERE id=?", (inode,))
    503
    504
    505class NoUniqueValueError(Exception):
    506    def __str__(self) -> str:
    507        return 'Query generated more than 1 result row'
    508
    509
    510class NoSuchRowError(Exception):
    511    def __str__(self) -> str:
    512        return 'Query produced 0 result rows'
    513
    514
    515def init_logging(debug: bool = False) -> None:
    516    formatter = logging.Formatter(
    517        '%(asctime)s.%(msecs)03d %(threadName)s: [%(name)s] %(message)s',
    518        datefmt="%Y-%m-%d %H:%M:%S",
    519    )
    520    handler = logging.StreamHandler()
    521    handler.setFormatter(formatter)
    522    root_logger = logging.getLogger()
    523    if debug:
    524        handler.setLevel(logging.DEBUG)
    525        root_logger.setLevel(logging.DEBUG)
    526    else:
    527        handler.setLevel(logging.INFO)
    528        root_logger.setLevel(logging.INFO)
    529    root_logger.addHandler(handler)
    530
    531
    532def parse_args() -> Namespace:
    533    '''Parse command line'''
    534
    535    parser = ArgumentParser()
    536
    537    parser.add_argument('mountpoint', type=str, help='Where to mount the file system')
    538    parser.add_argument(
    539        '--debug', action='store_true', default=False, help='Enable debugging output'
    540    )
    541    parser.add_argument(
    542        '--debug-fuse', action='store_true', default=False, help='Enable FUSE debugging output'
    543    )
    544
    545    return parser.parse_args()
    546
    547
    548if __name__ == '__main__':
    549    options = parse_args()
    550    init_logging(options.debug)
    551    operations = Operations()
    552
    553    fuse_options = set(pyfuse3.default_options)
    554    fuse_options.add('fsname=tmpfs')
    555    fuse_options.discard('default_permissions')
    556    if options.debug_fuse:
    557        fuse_options.add('debug')
    558    pyfuse3.init(operations, options.mountpoint, fuse_options)
    559
    560    try:
    561        trio.run(pyfuse3.main)
    562    except:
    563        pyfuse3.close(unmount=False)
    564        raise
    565
    566    pyfuse3.close()

## Passthrough / Overlay File System

(shipped as `examples/passthroughfs.py`)

      1#!/usr/bin/env python3
      2'''
      3passthroughfs.py - Example file system for pyfuse3
      4
      5This file system mirrors the contents of a specified directory tree.
      6
      7Caveats:
      8
      9 * Inode generation numbers are not passed through but set to zero.
     10
     11 * Block size (st_blksize) and number of allocated blocks (st_blocks) are not
     12   passed through.
     13
     14 * Performance for large directories is not good, because the directory
     15   is always read completely.
     16
     17 * There may be a way to break-out of the directory tree.
     18
     19 * The readdir implementation is not fully POSIX compliant. If a directory
     20   contains hardlinks and is modified during a readdir call, readdir()
     21   may return some of the hardlinked files twice or omit them completely.
     22
     23 * If you delete or rename files in the underlying file system, the
     24   passthrough file system will get confused.
     25
     26Copyright ©  Nikolaus Rath <Nikolaus.org>
     27
     28Permission is hereby granted, free of charge, to any person obtaining a copy of
     29this software and associated documentation files (the "Software"), to deal in
     30the Software without restriction, including without limitation the rights to
     31use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
     32the Software, and to permit persons to whom the Software is furnished to do so.
     33
     34THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
     35IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
     36FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
     37COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
     38IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
     39CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
     40'''
     41
     42import errno
     43import faulthandler
     44import logging
     45import os
     46import stat as stat_m
     47import sys
     48from argparse import ArgumentParser, Namespace
     49from collections import defaultdict
     50from collections.abc import Sequence
     51from os import fsdecode, fsencode
     52
     53import trio
     54
     55import pyfuse3
     56from pyfuse3 import (
     57    EntryAttributes,
     58    FileHandleT,
     59    FileInfo,
     60    FUSEError,
     61    InodeT,
     62    ReaddirToken,
     63    RequestContext,
     64    SetattrFields,
     65    StatvfsData,
     66)
     67
     68faulthandler.enable()
     69
     70log = logging.getLogger(__name__)
     71
     72
     73class Operations(pyfuse3.Operations):
     74    def __init__(self, source: str, enable_writeback_cache: bool = False) -> None:
     75        super().__init__()
     76        self.enable_writeback_cache = enable_writeback_cache
     77        self._inode_path_map: dict[InodeT, str | set[str]] = {pyfuse3.ROOT_INODE: source}
     78        self._lookup_cnt: defaultdict[InodeT, int] = defaultdict(lambda: 0)
     79        self._fd_inode_map: dict[int, InodeT] = dict()
     80        self._inode_fd_map: dict[InodeT, int] = dict()
     81        self._fd_open_count: dict[int, int] = dict()
     82
     83    def _inode_to_path(self, inode: InodeT) -> str:
     84        try:
     85            val = self._inode_path_map[inode]
     86        except KeyError:
     87            raise FUSEError(errno.ENOENT)
     88
     89        if isinstance(val, set):
     90            # In case of hardlinks, pick any path
     91            val = next(iter(val))
     92        return val
     93
     94    def _add_path(self, inode: InodeT, path: str) -> None:
     95        log.debug('_add_path for %d, %s', inode, path)
     96        self._lookup_cnt[inode] += 1
     97
     98        # With hardlinks, one inode may map to multiple paths.
     99        if inode not in self._inode_path_map:
    100            self._inode_path_map[inode] = path
    101            return
    102
    103        val = self._inode_path_map[inode]
    104        if isinstance(val, set):
    105            val.add(path)
    106        elif val != path:
    107            self._inode_path_map[inode] = {path, val}
    108
    109    async def forget(self, inode_list: Sequence[tuple[InodeT, int]]) -> None:
    110        for inode, nlookup in inode_list:
    111            if self._lookup_cnt[inode] > nlookup:
    112                self._lookup_cnt[inode] -= nlookup
    113                continue
    114            log.debug('forgetting about inode %d', inode)
    115            assert inode not in self._inode_fd_map
    116            del self._lookup_cnt[inode]
    117            try:
    118                del self._inode_path_map[inode]
    119            except KeyError:  # may have been deleted
    120                pass
    121
    122    async def lookup(
    123        self, parent_inode: InodeT, name: bytes, ctx: RequestContext
    124    ) -> EntryAttributes:
    125        name_str = fsdecode(name)
    126        log.debug('lookup for %s in %d', name_str, parent_inode)
    127        path = os.path.join(self._inode_to_path(parent_inode), name_str)
    128        attr = self._getattr(path=path)
    129        if name_str != '.' and name_str != '..':
    130            self._add_path(InodeT(attr.st_ino), path)
    131        return attr
    132
    133    async def getattr(self, inode: InodeT, ctx: RequestContext | None = None) -> EntryAttributes:
    134        if inode in self._inode_fd_map:
    135            return self._getattr(fd=self._inode_fd_map[inode])
    136        else:
    137            return self._getattr(path=self._inode_to_path(inode))
    138
    139    def _getattr(self, path: str | None = None, fd: int | None = None) -> EntryAttributes:
    140        assert fd is None or path is None
    141        assert not (fd is None and path is None)
    142        try:
    143            if fd is None:
    144                assert path is not None
    145                stat = os.lstat(path)
    146            else:
    147                stat = os.fstat(fd)
    148        except OSError as exc:
    149            assert exc.errno is not None
    150            raise FUSEError(exc.errno)
    151
    152        entry = EntryAttributes()
    153        for attr in (
    154            'st_ino',
    155            'st_mode',
    156            'st_nlink',
    157            'st_uid',
    158            'st_gid',
    159            'st_rdev',
    160            'st_size',
    161            'st_atime_ns',
    162            'st_mtime_ns',
    163            'st_ctime_ns',
    164        ):
    165            setattr(entry, attr, getattr(stat, attr))
    166        entry.generation = 0
    167        entry.entry_timeout = 0
    168        entry.attr_timeout = 0
    169        entry.st_blksize = 512
    170        entry.st_blocks = (entry.st_size + entry.st_blksize - 1) // entry.st_blksize
    171
    172        return entry
    173
    174    async def readlink(self, inode: InodeT, ctx: RequestContext) -> bytes:
    175        path = self._inode_to_path(inode)
    176        try:
    177            target = os.readlink(path)
    178        except OSError as exc:
    179            assert exc.errno is not None
    180            raise FUSEError(exc.errno)
    181        return fsencode(target)
    182
    183    async def opendir(self, inode: InodeT, ctx: RequestContext) -> FileHandleT:
    184        # For simplicity, we use the inode as file handle
    185        return FileHandleT(inode)
    186
    187    async def readdir(self, fh: FileHandleT, start_id: int, token: ReaddirToken) -> None:
    188        path = self._inode_to_path(InodeT(fh))
    189        log.debug('reading %s', path)
    190        entries: list[tuple[InodeT, str, EntryAttributes]] = []
    191        for name in os.listdir(path):
    192            if name == '.' or name == '..':
    193                continue
    194            attr = self._getattr(path=os.path.join(path, name))
    195            entries.append((InodeT(attr.st_ino), name, attr))
    196
    197        log.debug('read %d entries, starting at %d', len(entries), start_id)
    198
    199        # This is not fully posix compatible. If there are hardlinks
    200        # (two names with the same inode), we don't have a unique
    201        # offset to start in between them. Note that we cannot simply
    202        # count entries, because then we would skip over entries
    203        # (or return them more than once) if the number of directory
    204        # entries changes between two calls to readdir().
    205        for ino, name, attr in sorted(entries):
    206            if ino <= start_id:
    207                continue
    208            if not pyfuse3.readdir_reply(token, fsencode(name), attr, ino):
    209                break
    210            self._add_path(attr.st_ino, os.path.join(path, name))
    211
    212    async def unlink(self, parent_inode: InodeT, name: bytes, ctx: RequestContext) -> None:
    213        name_str = fsdecode(name)
    214        parent = self._inode_to_path(parent_inode)
    215        path = os.path.join(parent, name_str)
    216        try:
    217            inode = os.lstat(path).st_ino
    218            os.unlink(path)
    219        except OSError as exc:
    220            assert exc.errno is not None
    221            raise FUSEError(exc.errno)
    222        if inode in self._lookup_cnt:
    223            self._forget_path(InodeT(inode), path)
    224
    225    async def rmdir(self, parent_inode: InodeT, name: bytes, ctx: RequestContext) -> None:
    226        name_str = fsdecode(name)
    227        parent = self._inode_to_path(parent_inode)
    228        path = os.path.join(parent, name_str)
    229        try:
    230            inode = os.lstat(path).st_ino
    231            os.rmdir(path)
    232        except OSError as exc:
    233            assert exc.errno is not None
    234            raise FUSEError(exc.errno)
    235        if inode in self._lookup_cnt:
    236            self._forget_path(InodeT(inode), path)
    237
    238    def _forget_path(self, inode: InodeT, path: str) -> None:
    239        log.debug('forget %s for %d', path, inode)
    240        val = self._inode_path_map[inode]
    241        if isinstance(val, set):
    242            val.remove(path)
    243            if len(val) == 1:
    244                self._inode_path_map[inode] = next(iter(val))
    245        else:
    246            del self._inode_path_map[inode]
    247
    248    async def symlink(
    249        self, parent_inode: InodeT, name: bytes, target: bytes, ctx: RequestContext
    250    ) -> EntryAttributes:
    251        name_str = fsdecode(name)
    252        target_str = fsdecode(target)
    253        parent = self._inode_to_path(parent_inode)
    254        path = os.path.join(parent, name_str)
    255        try:
    256            os.symlink(target_str, path)
    257            os.lchown(path, ctx.uid, ctx.gid)
    258        except OSError as exc:
    259            assert exc.errno is not None
    260            raise FUSEError(exc.errno)
    261        inode = InodeT(os.lstat(path).st_ino)
    262        self._add_path(inode, path)
    263        return await self.getattr(inode, ctx)
    264
    265    async def rename(
    266        self,
    267        parent_inode_old: InodeT,
    268        name_old: bytes,
    269        parent_inode_new: InodeT,
    270        name_new: bytes,
    271        flags: int,
    272        ctx: RequestContext,
    273    ) -> None:
    274        if flags != 0:
    275            raise FUSEError(errno.EINVAL)
    276
    277        name_old_str = fsdecode(name_old)
    278        name_new_str = fsdecode(name_new)
    279        parent_old = self._inode_to_path(parent_inode_old)
    280        parent_new = self._inode_to_path(parent_inode_new)
    281        path_old = os.path.join(parent_old, name_old_str)
    282        path_new = os.path.join(parent_new, name_new_str)
    283        try:
    284            os.rename(path_old, path_new)
    285            inode = os.lstat(path_new).st_ino
    286        except OSError as exc:
    287            assert exc.errno is not None
    288            raise FUSEError(exc.errno)
    289        if inode not in self._lookup_cnt:
    290            return
    291
    292        val = self._inode_path_map[inode]
    293        if isinstance(val, set):
    294            assert len(val) > 1
    295            val.add(path_new)
    296            val.remove(path_old)
    297        else:
    298            assert val == path_old
    299            self._inode_path_map[inode] = path_new
    300
    301    async def link(
    302        self, inode: InodeT, new_parent_inode: InodeT, new_name: bytes, ctx: RequestContext
    303    ) -> EntryAttributes:
    304        new_name_str = fsdecode(new_name)
    305        parent = self._inode_to_path(new_parent_inode)
    306        path = os.path.join(parent, new_name_str)
    307        try:
    308            os.link(self._inode_to_path(inode), path, follow_symlinks=False)
    309        except OSError as exc:
    310            assert exc.errno is not None
    311            raise FUSEError(exc.errno)
    312        self._add_path(inode, path)
    313        return await self.getattr(inode, ctx)
    314
    315    async def setattr(
    316        self,
    317        inode: InodeT,
    318        attr: EntryAttributes,
    319        fields: SetattrFields,
    320        fh: FileHandleT | None,
    321        ctx: RequestContext,
    322    ) -> EntryAttributes:
    323        try:
    324            if fields.update_size:
    325                if fh is None:
    326                    os.truncate(self._inode_to_path(inode), attr.st_size)
    327                else:
    328                    os.ftruncate(fh, attr.st_size)
    329
    330            if fields.update_mode:
    331                # Under Linux, chmod always resolves symlinks so we should
    332                # actually never get a setattr() request for a symbolic
    333                # link.
    334                assert not stat_m.S_ISLNK(attr.st_mode)
    335                if fh is None:
    336                    os.chmod(self._inode_to_path(inode), stat_m.S_IMODE(attr.st_mode))
    337                else:
    338                    os.fchmod(fh, stat_m.S_IMODE(attr.st_mode))
    339
    340            if fields.update_uid and fields.update_gid:
    341                if fh is None:
    342                    os.chown(
    343                        self._inode_to_path(inode), attr.st_uid, attr.st_gid, follow_symlinks=False
    344                    )
    345                else:
    346                    os.fchown(fh, attr.st_uid, attr.st_gid)
    347
    348            elif fields.update_uid:
    349                if fh is None:
    350                    os.chown(self._inode_to_path(inode), attr.st_uid, -1, follow_symlinks=False)
    351                else:
    352                    os.fchown(fh, attr.st_uid, -1)
    353
    354            elif fields.update_gid:
    355                if fh is None:
    356                    os.chown(self._inode_to_path(inode), -1, attr.st_gid, follow_symlinks=False)
    357                else:
    358                    os.fchown(fh, -1, attr.st_gid)
    359
    360            if fields.update_atime and fields.update_mtime:
    361                if fh is None:
    362                    os.utime(
    363                        self._inode_to_path(inode),
    364                        None,
    365                        follow_symlinks=False,
    366                        ns=(attr.st_atime_ns, attr.st_mtime_ns),
    367                    )
    368                else:
    369                    os.utime(fh, None, ns=(attr.st_atime_ns, attr.st_mtime_ns))
    370            elif fields.update_atime or fields.update_mtime:
    371                # We can only set both values, so we first need to retrieve the
    372                # one that we shouldn't be changing.
    373                if fh is None:
    374                    path = self._inode_to_path(inode)
    375                    oldstat = os.stat(path, follow_symlinks=False)
    376                else:
    377                    oldstat = os.fstat(fh)
    378                if not fields.update_atime:
    379                    attr.st_atime_ns = oldstat.st_atime_ns
    380                else:
    381                    attr.st_mtime_ns = oldstat.st_mtime_ns
    382                if fh is None:
    383                    os.utime(
    384                        path,  # pyright: ignore[reportPossiblyUnboundVariable]
    385                        None,
    386                        follow_symlinks=False,
    387                        ns=(attr.st_atime_ns, attr.st_mtime_ns),
    388                    )
    389                else:
    390                    os.utime(fh, None, ns=(attr.st_atime_ns, attr.st_mtime_ns))
    391
    392        except OSError as exc:
    393            assert exc.errno is not None
    394            raise FUSEError(exc.errno)
    395
    396        return await self.getattr(inode, ctx)
    397
    398    async def mknod(
    399        self, parent_inode: InodeT, name: bytes, mode: int, rdev: int, ctx: RequestContext
    400    ) -> EntryAttributes:
    401        path = os.path.join(self._inode_to_path(parent_inode), fsdecode(name))
    402        try:
    403            os.mknod(path, mode=(mode & ~ctx.umask), device=rdev)
    404            os.chown(path, ctx.uid, ctx.gid)
    405        except OSError as exc:
    406            assert exc.errno is not None
    407            raise FUSEError(exc.errno)
    408        attr = self._getattr(path=path)
    409        self._add_path(attr.st_ino, path)
    410        return attr
    411
    412    async def mkdir(
    413        self, parent_inode: InodeT, name: bytes, mode: int, ctx: RequestContext
    414    ) -> EntryAttributes:
    415        path = os.path.join(self._inode_to_path(parent_inode), fsdecode(name))
    416        try:
    417            os.mkdir(path, mode=(mode & ~ctx.umask))
    418            os.chown(path, ctx.uid, ctx.gid)
    419        except OSError as exc:
    420            assert exc.errno is not None
    421            raise FUSEError(exc.errno)
    422        attr = self._getattr(path=path)
    423        self._add_path(attr.st_ino, path)
    424        return attr
    425
    426    async def statfs(self, ctx: RequestContext) -> StatvfsData:
    427        root = self._inode_path_map[pyfuse3.ROOT_INODE]
    428        assert isinstance(root, str)
    429        stat_ = StatvfsData()
    430        try:
    431            statfs = os.statvfs(root)
    432        except OSError as exc:
    433            assert exc.errno is not None
    434            raise FUSEError(exc.errno)
    435        for attr in (
    436            'f_bsize',
    437            'f_frsize',
    438            'f_blocks',
    439            'f_bfree',
    440            'f_bavail',
    441            'f_files',
    442            'f_ffree',
    443            'f_favail',
    444        ):
    445            setattr(stat_, attr, getattr(statfs, attr))
    446        stat_.f_namemax = statfs.f_namemax - (len(root) + 1)
    447        return stat_
    448
    449    async def open(self, inode: InodeT, flags: int, ctx: RequestContext) -> FileInfo:
    450        if inode in self._inode_fd_map:
    451            fd = self._inode_fd_map[inode]
    452            self._fd_open_count[fd] += 1
    453            return FileInfo(fh=FileHandleT(fd))
    454        assert flags & os.O_CREAT == 0
    455        try:
    456            fd = os.open(self._inode_to_path(inode), flags)
    457        except OSError as exc:
    458            assert exc.errno is not None
    459            raise FUSEError(exc.errno)
    460        self._inode_fd_map[inode] = fd
    461        self._fd_inode_map[fd] = inode
    462        self._fd_open_count[fd] = 1
    463        return FileInfo(fh=fd)
    464
    465    async def create(
    466        self, parent_inode: InodeT, name: bytes, mode: int, flags: int, ctx: RequestContext
    467    ) -> tuple[FileInfo, EntryAttributes]:
    468        path = os.path.join(self._inode_to_path(parent_inode), fsdecode(name))
    469        try:
    470            fd = os.open(path, flags | os.O_CREAT | os.O_TRUNC)
    471        except OSError as exc:
    472            assert exc.errno is not None
    473            raise FUSEError(exc.errno)
    474        attr = self._getattr(fd=fd)
    475        self._add_path(attr.st_ino, path)
    476        self._inode_fd_map[attr.st_ino] = fd
    477        self._fd_inode_map[fd] = attr.st_ino
    478        self._fd_open_count[fd] = 1
    479        return (FileInfo(fh=fd), attr)
    480
    481    async def read(self, fh: FileHandleT, off: int, size: int) -> bytes:
    482        os.lseek(fh, off, os.SEEK_SET)
    483        return os.read(fh, size)
    484
    485    async def write(self, fh: FileHandleT, off: int, buf: bytes) -> int:
    486        os.lseek(fh, off, os.SEEK_SET)
    487        return os.write(fh, buf)
    488
    489    async def release(self, fh: FileHandleT) -> None:
    490        if self._fd_open_count[fh] > 1:
    491            self._fd_open_count[fh] -= 1
    492            return
    493
    494        del self._fd_open_count[fh]
    495        inode = self._fd_inode_map[fh]
    496        del self._inode_fd_map[inode]
    497        del self._fd_inode_map[fh]
    498        try:
    499            os.close(fh)
    500        except OSError as exc:
    501            assert exc.errno is not None
    502            raise FUSEError(exc.errno)
    503
    504
    505def init_logging(debug: bool = False) -> None:
    506    formatter = logging.Formatter(
    507        '%(asctime)s.%(msecs)03d %(threadName)s: [%(name)s] %(message)s',
    508        datefmt="%Y-%m-%d %H:%M:%S",
    509    )
    510    handler = logging.StreamHandler()
    511    handler.setFormatter(formatter)
    512    root_logger = logging.getLogger()
    513    if debug:
    514        handler.setLevel(logging.DEBUG)
    515        root_logger.setLevel(logging.DEBUG)
    516    else:
    517        handler.setLevel(logging.INFO)
    518        root_logger.setLevel(logging.INFO)
    519    root_logger.addHandler(handler)
    520
    521
    522def parse_args(args: list[str]) -> Namespace:
    523    '''Parse command line'''
    524
    525    parser = ArgumentParser()
    526
    527    parser.add_argument('source', type=str, help='Directory tree to mirror')
    528    parser.add_argument('mountpoint', type=str, help='Where to mount the file system')
    529    parser.add_argument(
    530        '--debug', action='store_true', default=False, help='Enable debugging output'
    531    )
    532    parser.add_argument(
    533        '--debug-fuse', action='store_true', default=False, help='Enable FUSE debugging output'
    534    )
    535    parser.add_argument(
    536        '--enable-writeback-cache',
    537        action='store_true',
    538        default=False,
    539        help='Enable writeback cache (default: disabled)',
    540    )
    541
    542    return parser.parse_args(args)
    543
    544
    545def main() -> None:
    546    options = parse_args(sys.argv[1:])
    547    init_logging(options.debug)
    548    operations = Operations(options.source, enable_writeback_cache=options.enable_writeback_cache)
    549
    550    log.debug('Mounting...')
    551    fuse_options = set(pyfuse3.default_options)
    552    fuse_options.add('fsname=passthroughfs')
    553    if options.debug_fuse:
    554        fuse_options.add('debug')
    555    pyfuse3.init(operations, options.mountpoint, fuse_options)
    556
    557    try:
    558        log.debug('Entering main loop..')
    559        trio.run(pyfuse3.main)
    560    except:
    561        pyfuse3.close(unmount=False)
    562        raise
    563
    564    log.debug('Unmounting..')
    565    pyfuse3.close()
    566
    567
    568if __name__ == '__main__':
    569    main()