# Example File Systems

Python-LLFUSE 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'''
      3lltest.py - Example file system for Python-LLFUSE.
      4
      5This program presents a static file system containing a single file. It is
      6compatible with both Python 2.x and 3.x. Based on an example from Gerion Entrup.
      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
     25
     26import os
     27import sys
     28
     29# If we are running from the Python-LLFUSE source directory, try
     30# to load the module from there first.
     31basedir = os.path.abspath(os.path.join(os.path.dirname(sys.argv[0]), '..'))
     32if (os.path.exists(os.path.join(basedir, 'setup.py')) and
     33    os.path.exists(os.path.join(basedir, 'src', 'llfuse.pyx'))):
     34    sys.path.insert(0, os.path.join(basedir, 'src'))
     35
     36from argparse import ArgumentParser
     37import stat
     38import logging
     39import errno
     40import llfuse
     41
     42try:
     43    import faulthandler
     44except ImportError:
     45    pass
     46else:
     47    faulthandler.enable()
     48
     49log = logging.getLogger(__name__)
     50
     51class TestFs(llfuse.Operations):
     52    def __init__(self):
     53        super().__init__()
     54        self.hello_name = b"message"
     55        self.hello_inode = llfuse.ROOT_INODE+1
     56        self.hello_data = b"hello world\n"
     57
     58    def getattr(self, inode, ctx=None):
     59        entry = llfuse.EntryAttributes()
     60        if inode == llfuse.ROOT_INODE:
     61            entry.st_mode = (stat.S_IFDIR | 0o755)
     62            entry.st_size = 0
     63        elif inode == self.hello_inode:
     64            entry.st_mode = (stat.S_IFREG | 0o644)
     65            entry.st_size = len(self.hello_data)
     66        else:
     67            raise llfuse.FUSEError(errno.ENOENT)
     68
     69        stamp = int(1438467123.985654 * 1e9)
     70        entry.st_atime_ns = stamp
     71        entry.st_ctime_ns = stamp
     72        entry.st_mtime_ns = stamp
     73        entry.st_gid = os.getgid()
     74        entry.st_uid = os.getuid()
     75        entry.st_ino = inode
     76
     77        return entry
     78
     79    def lookup(self, parent_inode, name, ctx=None):
     80        if parent_inode != llfuse.ROOT_INODE or name != self.hello_name:
     81            raise llfuse.FUSEError(errno.ENOENT)
     82        return self.getattr(self.hello_inode)
     83
     84    def opendir(self, inode, ctx):
     85        if inode != llfuse.ROOT_INODE:
     86            raise llfuse.FUSEError(errno.ENOENT)
     87        return inode
     88
     89    def readdir(self, fh, off):
     90        assert fh == llfuse.ROOT_INODE
     91
     92        # only one entry
     93        if off == 0:
     94            yield (self.hello_name, self.getattr(self.hello_inode), self.hello_inode)
     95
     96    def statfs(self, ctx):
     97        stat_ = llfuse.StatvfsData()
     98
     99        stat_.f_bsize = 512
    100        stat_.f_frsize = 512
    101
    102        size = 1024 * stat_.f_frsize
    103        stat_.f_blocks = size // stat_.f_frsize
    104        stat_.f_bfree = max(size // stat_.f_frsize, 1024)
    105        stat_.f_bavail = stat_.f_bfree
    106
    107        inodes = 100
    108        stat_.f_files = inodes
    109        stat_.f_ffree = max(inodes, 100)
    110        stat_.f_favail = stat_.f_ffree
    111
    112        return stat_
    113
    114    def open(self, inode, flags, ctx):
    115        if inode != self.hello_inode:
    116            raise llfuse.FUSEError(errno.ENOENT)
    117        if flags & os.O_RDWR or flags & os.O_WRONLY:
    118            raise llfuse.FUSEError(errno.EACCES)
    119        return inode
    120
    121    def read(self, fh, off, size):
    122        assert fh == self.hello_inode
    123        return self.hello_data[off:off+size]
    124
    125def init_logging(debug=False):
    126    formatter = logging.Formatter('%(asctime)s.%(msecs)03d %(threadName)s: '
    127                                  '[%(name)s] %(message)s', datefmt="%Y-%m-%d %H:%M:%S")
    128    handler = logging.StreamHandler()
    129    handler.setFormatter(formatter)
    130    root_logger = logging.getLogger()
    131    if debug:
    132        handler.setLevel(logging.DEBUG)
    133        root_logger.setLevel(logging.DEBUG)
    134    else:
    135        handler.setLevel(logging.INFO)
    136        root_logger.setLevel(logging.INFO)
    137    root_logger.addHandler(handler)
    138
    139def parse_args():
    140    '''Parse command line'''
    141
    142    parser = ArgumentParser()
    143
    144    parser.add_argument('mountpoint', type=str,
    145                        help='Where to mount the file system')
    146    parser.add_argument('--debug', action='store_true', default=False,
    147                        help='Enable debugging output')
    148    parser.add_argument('--debug-fuse', action='store_true', default=False,
    149                        help='Enable FUSE debugging output')
    150    return parser.parse_args()
    151
    152
    153def main():
    154    options = parse_args()
    155    init_logging(options.debug)
    156
    157    testfs = TestFs()
    158    fuse_options = set(llfuse.default_options)
    159    fuse_options.add('fsname=lltest')
    160    if options.debug_fuse:
    161        fuse_options.add('debug')
    162    llfuse.init(testfs, options.mountpoint, fuse_options)
    163    try:
    164        llfuse.main(workers=1)
    165    except:
    166        llfuse.close(unmount=False)
    167        raise
    168
    169    llfuse.close()
    170
    171
    172if __name__ == '__main__':
    173    main()

## In-memory File System

(shipped as `examples/tmpfs.py`)

      1#!/usr/bin/env python3
      2'''
      3tmpfs.py - Example file system for Python-LLFUSE.
      4
      5This file system stores all data in memory. It is compatible with both Python
      62.x and 3.x.
      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
     24
     25import os
     26import sys
     27
     28# If we are running from the Python-LLFUSE source directory, try
     29# to load the module from there first.
     30basedir = os.path.abspath(os.path.join(os.path.dirname(sys.argv[0]), '..'))
     31if (os.path.exists(os.path.join(basedir, 'setup.py')) and
     32    os.path.exists(os.path.join(basedir, 'src', 'llfuse.pyx'))):
     33    sys.path.insert(0, os.path.join(basedir, 'src'))
     34
     35import llfuse
     36import errno
     37import stat
     38from time import time
     39import sqlite3
     40import logging
     41from collections import defaultdict
     42from llfuse import FUSEError
     43from argparse import ArgumentParser
     44
     45try:
     46    import faulthandler
     47except ImportError:
     48    pass
     49else:
     50    faulthandler.enable()
     51
     52log = logging.getLogger()
     53
     54
     55class Operations(llfuse.Operations):
     56    '''An example filesystem that stores all data in memory
     57
     58    This is a very simple implementation with terrible performance.
     59    Don't try to store significant amounts of data. Also, there are
     60    some other flaws that have not been fixed to keep the code easier
     61    to understand:
     62
     63    * atime, mtime and ctime are not updated
     64    * generation numbers are not supported
     65    '''
     66
     67
     68    def __init__(self):
     69        super().__init__()
     70        self.db = sqlite3.connect(':memory:')
     71        self.db.text_factory = str
     72        self.db.row_factory = sqlite3.Row
     73        self.cursor = self.db.cursor()
     74        self.inode_open_count = defaultdict(int)
     75        self.init_tables()
     76
     77    def init_tables(self):
     78        '''Initialize file system tables'''
     79
     80        self.cursor.execute("""
     81        CREATE TABLE inodes (
     82            id        INTEGER PRIMARY KEY,
     83            uid       INT NOT NULL,
     84            gid       INT NOT NULL,
     85            mode      INT NOT NULL,
     86            mtime_ns  INT NOT NULL,
     87            atime_ns  INT NOT NULL,
     88            ctime_ns  INT NOT NULL,
     89            target    BLOB(256) ,
     90            size      INT NOT NULL DEFAULT 0,
     91            rdev      INT NOT NULL DEFAULT 0,
     92            data      BLOB
     93        )
     94        """)
     95
     96        self.cursor.execute("""
     97        CREATE TABLE contents (
     98            rowid     INTEGER PRIMARY KEY AUTOINCREMENT,
     99            name      BLOB(256) NOT NULL,
    100            inode     INT NOT NULL REFERENCES inodes(id),
    101            parent_inode INT NOT NULL REFERENCES inodes(id),
    102
    103            UNIQUE (name, parent_inode)
    104        )""")
    105
    106        # Insert root directory
    107        now_ns = int(time() * 1e9)
    108        self.cursor.execute("INSERT INTO inodes (id,mode,uid,gid,mtime_ns,atime_ns,ctime_ns) "
    109                            "VALUES (?,?,?,?,?,?,?)",
    110                            (llfuse.ROOT_INODE, stat.S_IFDIR | stat.S_IRUSR | stat.S_IWUSR
    111                              | stat.S_IXUSR | stat.S_IRGRP | stat.S_IXGRP | stat.S_IROTH
    112                              | stat.S_IXOTH, os.getuid(), os.getgid(), now_ns, now_ns, now_ns))
    113        self.cursor.execute("INSERT INTO contents (name, parent_inode, inode) VALUES (?,?,?)",
    114                            (b'..', llfuse.ROOT_INODE, llfuse.ROOT_INODE))
    115
    116
    117    def get_row(self, *a, **kw):
    118        self.cursor.execute(*a, **kw)
    119        try:
    120            row = next(self.cursor)
    121        except StopIteration:
    122            raise NoSuchRowError()
    123        try:
    124            next(self.cursor)
    125        except StopIteration:
    126            pass
    127        else:
    128            raise NoUniqueValueError()
    129
    130        return row
    131
    132    def lookup(self, inode_p, name, ctx=None):
    133        if name == '.':
    134            inode = inode_p
    135        elif name == '..':
    136            inode = self.get_row("SELECT * FROM contents WHERE inode=?",
    137                                 (inode_p,))['parent_inode']
    138        else:
    139            try:
    140                inode = self.get_row("SELECT * FROM contents WHERE name=? AND parent_inode=?",
    141                                     (name, inode_p))['inode']
    142            except NoSuchRowError:
    143                raise(llfuse.FUSEError(errno.ENOENT))
    144
    145        return self.getattr(inode, ctx)
    146
    147
    148    def getattr(self, inode, ctx=None):
    149        try:
    150            row = self.get_row('SELECT * FROM inodes WHERE id=?', (inode,))
    151        except NoSuchRowError:
    152            raise(llfuse.FUSEError(errno.ENOENT))
    153
    154        entry = llfuse.EntryAttributes()
    155        entry.st_ino = inode
    156        entry.generation = 0
    157        entry.entry_timeout = 300
    158        entry.attr_timeout = 300
    159        entry.st_mode = row['mode']
    160        entry.st_nlink = self.get_row("SELECT COUNT(inode) FROM contents WHERE inode=?",
    161                                     (inode,))[0]
    162        entry.st_uid = row['uid']
    163        entry.st_gid = row['gid']
    164        entry.st_rdev = row['rdev']
    165        entry.st_size = row['size']
    166
    167        entry.st_blksize = 512
    168        entry.st_blocks = 1
    169        entry.st_atime_ns = row['atime_ns']
    170        entry.st_mtime_ns = row['mtime_ns']
    171        entry.st_ctime_ns = row['ctime_ns']
    172
    173        return entry
    174
    175    def readlink(self, inode, ctx):
    176        return self.get_row('SELECT * FROM inodes WHERE id=?', (inode,))['target']
    177
    178    def opendir(self, inode, ctx):
    179        return inode
    180
    181    def readdir(self, inode, off):
    182        if off == 0:
    183            off = -1
    184
    185        cursor2 = self.db.cursor()
    186        cursor2.execute("SELECT * FROM contents WHERE parent_inode=? "
    187                        'AND rowid > ? ORDER BY rowid', (inode, off))
    188
    189        for row in cursor2:
    190            yield (row['name'], self.getattr(row['inode']), row['rowid'])
    191
    192    def unlink(self, inode_p, name,ctx):
    193        entry = self.lookup(inode_p, name)
    194
    195        if stat.S_ISDIR(entry.st_mode):
    196            raise llfuse.FUSEError(errno.EISDIR)
    197
    198        self._remove(inode_p, name, entry)
    199
    200    def rmdir(self, inode_p, name, ctx):
    201        entry = self.lookup(inode_p, name)
    202
    203        if not stat.S_ISDIR(entry.st_mode):
    204            raise llfuse.FUSEError(errno.ENOTDIR)
    205
    206        self._remove(inode_p, name, entry)
    207
    208    def _remove(self, inode_p, name, entry):
    209        if self.get_row("SELECT COUNT(inode) FROM contents WHERE parent_inode=?",
    210                        (entry.st_ino,))[0] > 0:
    211            raise llfuse.FUSEError(errno.ENOTEMPTY)
    212
    213        self.cursor.execute("DELETE FROM contents WHERE name=? AND parent_inode=?",
    214                        (name, inode_p))
    215
    216        if entry.st_nlink == 1 and entry.st_ino not in self.inode_open_count:
    217            self.cursor.execute("DELETE FROM inodes WHERE id=?", (entry.st_ino,))
    218
    219    def symlink(self, inode_p, name, target, ctx):
    220        mode = (stat.S_IFLNK | stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR |
    221                stat.S_IRGRP | stat.S_IWGRP | stat.S_IXGRP |
    222                stat.S_IROTH | stat.S_IWOTH | stat.S_IXOTH)
    223        return self._create(inode_p, name, mode, ctx, target=target)
    224
    225    def rename(self, inode_p_old, name_old, inode_p_new, name_new, ctx):
    226        entry_old = self.lookup(inode_p_old, name_old)
    227
    228        try:
    229            entry_new = self.lookup(inode_p_new, name_new)
    230        except llfuse.FUSEError as exc:
    231            if exc.errno != errno.ENOENT:
    232                raise
    233            target_exists = False
    234        else:
    235            target_exists = True
    236
    237        if target_exists:
    238            self._replace(inode_p_old, name_old, inode_p_new, name_new,
    239                          entry_old, entry_new)
    240        else:
    241            self.cursor.execute("UPDATE contents SET name=?, parent_inode=? WHERE name=? "
    242                                "AND parent_inode=?", (name_new, inode_p_new,
    243                                                       name_old, inode_p_old))
    244
    245    def _replace(self, inode_p_old, name_old, inode_p_new, name_new,
    246                 entry_old, entry_new):
    247
    248        if self.get_row("SELECT COUNT(inode) FROM contents WHERE parent_inode=?",
    249                        (entry_new.st_ino,))[0] > 0:
    250            raise llfuse.FUSEError(errno.ENOTEMPTY)
    251
    252        self.cursor.execute("UPDATE contents SET inode=? WHERE name=? AND parent_inode=?",
    253                            (entry_old.st_ino, name_new, inode_p_new))
    254        self.db.execute('DELETE FROM contents WHERE name=? AND parent_inode=?',
    255                        (name_old, inode_p_old))
    256
    257        if entry_new.st_nlink == 1 and entry_new.st_ino not in self.inode_open_count:
    258            self.cursor.execute("DELETE FROM inodes WHERE id=?", (entry_new.st_ino,))
    259
    260
    261    def link(self, inode, new_inode_p, new_name, ctx):
    262        entry_p = self.getattr(new_inode_p)
    263        if entry_p.st_nlink == 0:
    264            log.warning('Attempted to create entry %s with unlinked parent %d',
    265                        new_name, new_inode_p)
    266            raise FUSEError(errno.EINVAL)
    267
    268        self.cursor.execute("INSERT INTO contents (name, inode, parent_inode) VALUES(?,?,?)",
    269                            (new_name, inode, new_inode_p))
    270
    271        return self.getattr(inode)
    272
    273    def setattr(self, inode, attr, fields, fh, ctx):
    274
    275        if fields.update_size:
    276            data = self.get_row('SELECT data FROM inodes WHERE id=?', (inode,))[0]
    277            if data is None:
    278                data = b''
    279            if len(data) < attr.st_size:
    280                data = data + b'\0' * (attr.st_size - len(data))
    281            else:
    282                data = data[:attr.st_size]
    283            self.cursor.execute('UPDATE inodes SET data=?, size=? WHERE id=?',
    284                                (memoryview(data), attr.st_size, inode))
    285        if fields.update_mode:
    286            self.cursor.execute('UPDATE inodes SET mode=? WHERE id=?',
    287                                (attr.st_mode, inode))
    288
    289        if fields.update_uid:
    290            self.cursor.execute('UPDATE inodes SET uid=? WHERE id=?',
    291                                (attr.st_uid, inode))
    292
    293        if fields.update_gid:
    294            self.cursor.execute('UPDATE inodes SET gid=? WHERE id=?',
    295                                (attr.st_gid, inode))
    296
    297        if fields.update_atime:
    298            self.cursor.execute('UPDATE inodes SET atime_ns=? WHERE id=?',
    299                                (attr.st_atime_ns, inode))
    300
    301        if fields.update_mtime:
    302            self.cursor.execute('UPDATE inodes SET mtime_ns=? WHERE id=?',
    303                                (attr.st_mtime_ns, inode))
    304
    305        return self.getattr(inode)
    306
    307    def mknod(self, inode_p, name, mode, rdev, ctx):
    308        return self._create(inode_p, name, mode, ctx, rdev=rdev)
    309
    310    def mkdir(self, inode_p, name, mode, ctx):
    311        return self._create(inode_p, name, mode, ctx)
    312
    313    def statfs(self, ctx):
    314        stat_ = llfuse.StatvfsData()
    315
    316        stat_.f_bsize = 512
    317        stat_.f_frsize = 512
    318
    319        size = self.get_row('SELECT SUM(size) FROM inodes')[0]
    320        stat_.f_blocks = size // stat_.f_frsize
    321        stat_.f_bfree = max(size // stat_.f_frsize, 1024)
    322        stat_.f_bavail = stat_.f_bfree
    323
    324        inodes = self.get_row('SELECT COUNT(id) FROM inodes')[0]
    325        stat_.f_files = inodes
    326        stat_.f_ffree = max(inodes , 100)
    327        stat_.f_favail = stat_.f_ffree
    328
    329        return stat_
    330
    331    def open(self, inode, flags, ctx):
    332        # Yeah, unused arguments
    333        #pylint: disable=W0613
    334        self.inode_open_count[inode] += 1
    335
    336        # Use inodes as a file handles
    337        return inode
    338
    339    def access(self, inode, mode, ctx):
    340        # Yeah, could be a function and has unused arguments
    341        #pylint: disable=R0201,W0613
    342        return True
    343
    344    def create(self, inode_parent, name, mode, flags, ctx):
    345        #pylint: disable=W0612
    346        entry = self._create(inode_parent, name, mode, ctx)
    347        self.inode_open_count[entry.st_ino] += 1
    348        return (entry.st_ino, entry)
    349
    350    def _create(self, inode_p, name, mode, ctx, rdev=0, target=None):
    351        if self.getattr(inode_p).st_nlink == 0:
    352            log.warning('Attempted to create entry %s with unlinked parent %d',
    353                        name, inode_p)
    354            raise FUSEError(errno.EINVAL)
    355
    356        now_ns = int(time() * 1e9)
    357        self.cursor.execute('INSERT INTO inodes (uid, gid, mode, mtime_ns, atime_ns, '
    358                            'ctime_ns, target, rdev) VALUES(?, ?, ?, ?, ?, ?, ?, ?)',
    359                            (ctx.uid, ctx.gid, mode, now_ns, now_ns, now_ns, target, rdev))
    360
    361        inode = self.cursor.lastrowid
    362        self.db.execute("INSERT INTO contents(name, inode, parent_inode) VALUES(?,?,?)",
    363                        (name, inode, inode_p))
    364        return self.getattr(inode)
    365
    366    def read(self, fh, offset, length):
    367        data = self.get_row('SELECT data FROM inodes WHERE id=?', (fh,))[0]
    368        if data is None:
    369            data = b''
    370        return data[offset:offset+length]
    371
    372    def write(self, fh, offset, buf):
    373        data = self.get_row('SELECT data FROM inodes WHERE id=?', (fh,))[0]
    374        if data is None:
    375            data = b''
    376        data = data[:offset] + buf + data[offset+len(buf):]
    377
    378        self.cursor.execute('UPDATE inodes SET data=?, size=? WHERE id=?',
    379                            (memoryview(data), len(data), fh))
    380        return len(buf)
    381
    382    def release(self, fh):
    383        self.inode_open_count[fh] -= 1
    384
    385        if self.inode_open_count[fh] == 0:
    386            del self.inode_open_count[fh]
    387            if self.getattr(fh).st_nlink == 0:
    388                self.cursor.execute("DELETE FROM inodes WHERE id=?", (fh,))
    389
    390class NoUniqueValueError(Exception):
    391    def __str__(self):
    392        return 'Query generated more than 1 result row'
    393
    394
    395class NoSuchRowError(Exception):
    396    def __str__(self):
    397        return 'Query produced 0 result rows'
    398
    399def init_logging(debug=False):
    400    formatter = logging.Formatter('%(asctime)s.%(msecs)03d %(threadName)s: '
    401                                  '[%(name)s] %(message)s', datefmt="%Y-%m-%d %H:%M:%S")
    402    handler = logging.StreamHandler()
    403    handler.setFormatter(formatter)
    404    root_logger = logging.getLogger()
    405    if debug:
    406        handler.setLevel(logging.DEBUG)
    407        root_logger.setLevel(logging.DEBUG)
    408    else:
    409        handler.setLevel(logging.INFO)
    410        root_logger.setLevel(logging.INFO)
    411    root_logger.addHandler(handler)
    412
    413def parse_args():
    414    '''Parse command line'''
    415
    416    parser = ArgumentParser()
    417
    418    parser.add_argument('mountpoint', type=str,
    419                        help='Where to mount the file system')
    420    parser.add_argument('--debug', action='store_true', default=False,
    421                        help='Enable debugging output')
    422    parser.add_argument('--debug-fuse', action='store_true', default=False,
    423                        help='Enable FUSE debugging output')
    424
    425    return parser.parse_args()
    426
    427
    428if __name__ == '__main__':
    429
    430    options = parse_args()
    431    init_logging(options.debug)
    432    operations = Operations()
    433
    434    fuse_options = set(llfuse.default_options)
    435    fuse_options.add('fsname=tmpfs')
    436    fuse_options.discard('default_permissions')
    437    if options.debug_fuse:
    438        fuse_options.add('debug')
    439    llfuse.init(operations, options.mountpoint, fuse_options)
    440
    441    # sqlite3 does not support multithreading
    442    try:
    443        llfuse.main(workers=1)
    444    except:
    445        llfuse.close(unmount=False)
    446        raise
    447
    448    llfuse.close()

## Passthrough / Overlay File System

(shipped as `examples/passthroughfs.py`)

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