# 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), 1)
     95
     96    def open(self, inode, flags, ctx):
     97        if inode != self.hello_inode:
     98            raise llfuse.FUSEError(errno.ENOENT)
     99        if flags & os.O_RDWR or flags & os.O_WRONLY:
    100            raise llfuse.FUSEError(errno.EACCES)
    101        return inode
    102
    103    def read(self, fh, off, size):
    104        assert fh == self.hello_inode
    105        return self.hello_data[off:off+size]
    106
    107def init_logging(debug=False):
    108    formatter = logging.Formatter('%(asctime)s.%(msecs)03d %(threadName)s: '
    109                                  '[%(name)s] %(message)s', datefmt="%Y-%m-%d %H:%M:%S")
    110    handler = logging.StreamHandler()
    111    handler.setFormatter(formatter)
    112    root_logger = logging.getLogger()
    113    if debug:
    114        handler.setLevel(logging.DEBUG)
    115        root_logger.setLevel(logging.DEBUG)
    116    else:
    117        handler.setLevel(logging.INFO)
    118        root_logger.setLevel(logging.INFO)
    119    root_logger.addHandler(handler)
    120
    121def parse_args():
    122    '''Parse command line'''
    123
    124    parser = ArgumentParser()
    125
    126    parser.add_argument('mountpoint', type=str,
    127                        help='Where to mount the file system')
    128    parser.add_argument('--debug', action='store_true', default=False,
    129                        help='Enable debugging output')
    130    parser.add_argument('--debug-fuse', action='store_true', default=False,
    131                        help='Enable FUSE debugging output')
    132    return parser.parse_args()
    133
    134
    135def main():
    136    options = parse_args()
    137    init_logging(options.debug)
    138
    139    testfs = TestFs()
    140    fuse_options = set(llfuse.default_options)
    141    fuse_options.add('fsname=lltest')
    142    if options.debug_fuse:
    143        fuse_options.add('debug')
    144    llfuse.init(testfs, options.mountpoint, fuse_options)
    145    try:
    146        llfuse.main(workers=1)
    147    except:
    148        llfuse.close(unmount=False)
    149        raise
    150
    151    llfuse.close()
    152
    153
    154if __name__ == '__main__':
    155    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        row = self.get_row('SELECT * FROM inodes WHERE id=?', (inode,))
    150
    151        entry = llfuse.EntryAttributes()
    152        entry.st_ino = inode
    153        entry.generation = 0
    154        entry.entry_timeout = 300
    155        entry.attr_timeout = 300
    156        entry.st_mode = row['mode']
    157        entry.st_nlink = self.get_row("SELECT COUNT(inode) FROM contents WHERE inode=?",
    158                                     (inode,))[0]
    159        entry.st_uid = row['uid']
    160        entry.st_gid = row['gid']
    161        entry.st_rdev = row['rdev']
    162        entry.st_size = row['size']
    163
    164        entry.st_blksize = 512
    165        entry.st_blocks = 1
    166        entry.st_atime_ns = row['atime_ns']
    167        entry.st_mtime_ns = row['mtime_ns']
    168        entry.st_ctime_ns = row['ctime_ns']
    169
    170        return entry
    171
    172    def readlink(self, inode, ctx):
    173        return self.get_row('SELECT * FROM inodes WHERE id=?', (inode,))['target']
    174
    175    def opendir(self, inode, ctx):
    176        return inode
    177
    178    def readdir(self, inode, off):
    179        if off == 0:
    180            off = -1
    181
    182        cursor2 = self.db.cursor()
    183        cursor2.execute("SELECT * FROM contents WHERE parent_inode=? "
    184                        'AND rowid > ? ORDER BY rowid', (inode, off))
    185
    186        for row in cursor2:
    187            yield (row['name'], self.getattr(row['inode']), row['rowid'])
    188
    189    def unlink(self, inode_p, name,ctx):
    190        entry = self.lookup(inode_p, name)
    191
    192        if stat.S_ISDIR(entry.st_mode):
    193            raise llfuse.FUSEError(errno.EISDIR)
    194
    195        self._remove(inode_p, name, entry)
    196
    197    def rmdir(self, inode_p, name, ctx):
    198        entry = self.lookup(inode_p, name)
    199
    200        if not stat.S_ISDIR(entry.st_mode):
    201            raise llfuse.FUSEError(errno.ENOTDIR)
    202
    203        self._remove(inode_p, name, entry)
    204
    205    def _remove(self, inode_p, name, entry):
    206        if self.get_row("SELECT COUNT(inode) FROM contents WHERE parent_inode=?",
    207                        (entry.st_ino,))[0] > 0:
    208            raise llfuse.FUSEError(errno.ENOTEMPTY)
    209
    210        self.cursor.execute("DELETE FROM contents WHERE name=? AND parent_inode=?",
    211                        (name, inode_p))
    212
    213        if entry.st_nlink == 1 and entry.st_ino not in self.inode_open_count:
    214            self.cursor.execute("DELETE FROM inodes WHERE id=?", (entry.st_ino,))
    215
    216    def symlink(self, inode_p, name, target, ctx):
    217        mode = (stat.S_IFLNK | stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR |
    218                stat.S_IRGRP | stat.S_IWGRP | stat.S_IXGRP |
    219                stat.S_IROTH | stat.S_IWOTH | stat.S_IXOTH)
    220        return self._create(inode_p, name, mode, ctx, target=target)
    221
    222    def rename(self, inode_p_old, name_old, inode_p_new, name_new, ctx):
    223        entry_old = self.lookup(inode_p_old, name_old)
    224
    225        try:
    226            entry_new = self.lookup(inode_p_new, name_new)
    227        except llfuse.FUSEError as exc:
    228            if exc.errno != errno.ENOENT:
    229                raise
    230            target_exists = False
    231        else:
    232            target_exists = True
    233
    234        if target_exists:
    235            self._replace(inode_p_old, name_old, inode_p_new, name_new,
    236                          entry_old, entry_new)
    237        else:
    238            self.cursor.execute("UPDATE contents SET name=?, parent_inode=? WHERE name=? "
    239                                "AND parent_inode=?", (name_new, inode_p_new,
    240                                                       name_old, inode_p_old))
    241
    242    def _replace(self, inode_p_old, name_old, inode_p_new, name_new,
    243                 entry_old, entry_new):
    244
    245        if self.get_row("SELECT COUNT(inode) FROM contents WHERE parent_inode=?",
    246                        (entry_new.st_ino,))[0] > 0:
    247            raise llfuse.FUSEError(errno.ENOTEMPTY)
    248
    249        self.cursor.execute("UPDATE contents SET inode=? WHERE name=? AND parent_inode=?",
    250                            (entry_old.st_ino, name_new, inode_p_new))
    251        self.db.execute('DELETE FROM contents WHERE name=? AND parent_inode=?',
    252                        (name_old, inode_p_old))
    253
    254        if entry_new.st_nlink == 1 and entry_new.st_ino not in self.inode_open_count:
    255            self.cursor.execute("DELETE FROM inodes WHERE id=?", (entry_new.st_ino,))
    256
    257
    258    def link(self, inode, new_inode_p, new_name, ctx):
    259        entry_p = self.getattr(new_inode_p)
    260        if entry_p.st_nlink == 0:
    261            log.warn('Attempted to create entry %s with unlinked parent %d',
    262                     new_name, new_inode_p)
    263            raise FUSEError(errno.EINVAL)
    264
    265        self.cursor.execute("INSERT INTO contents (name, inode, parent_inode) VALUES(?,?,?)",
    266                            (new_name, inode, new_inode_p))
    267
    268        return self.getattr(inode)
    269
    270    def setattr(self, inode, attr, fields, fh, ctx):
    271
    272        if fields.update_size:
    273            data = self.get_row('SELECT data FROM inodes WHERE id=?', (inode,))[0]
    274            if data is None:
    275                data = b''
    276            if len(data) < attr.st_size:
    277                data = data + b'\0' * (attr.st_size - len(data))
    278            else:
    279                data = data[:attr.st_size]
    280            self.cursor.execute('UPDATE inodes SET data=?, size=? WHERE id=?',
    281                                (memoryview(data), attr.st_size, inode))
    282        if fields.update_mode:
    283            self.cursor.execute('UPDATE inodes SET mode=? WHERE id=?',
    284                                (attr.st_mode, inode))
    285
    286        if fields.update_uid:
    287            self.cursor.execute('UPDATE inodes SET uid=? WHERE id=?',
    288                                (attr.st_uid, inode))
    289
    290        if fields.update_gid:
    291            self.cursor.execute('UPDATE inodes SET gid=? WHERE id=?',
    292                                (attr.st_gid, inode))
    293
    294        if fields.update_atime:
    295            self.cursor.execute('UPDATE inodes SET atime_ns=? WHERE id=?',
    296                                (attr.st_atime_ns, inode))
    297
    298        if fields.update_mtime:
    299            self.cursor.execute('UPDATE inodes SET mtime_ns=? WHERE id=?',
    300                                (attr.st_mtime_ns, inode))
    301
    302        return self.getattr(inode)
    303
    304    def mknod(self, inode_p, name, mode, rdev, ctx):
    305        return self._create(inode_p, name, mode, ctx, rdev=rdev)
    306
    307    def mkdir(self, inode_p, name, mode, ctx):
    308        return self._create(inode_p, name, mode, ctx)
    309
    310    def statfs(self, ctx):
    311        stat_ = llfuse.StatvfsData()
    312
    313        stat_.f_bsize = 512
    314        stat_.f_frsize = 512
    315
    316        size = self.get_row('SELECT SUM(size) FROM inodes')[0]
    317        stat_.f_blocks = size // stat_.f_frsize
    318        stat_.f_bfree = max(size // stat_.f_frsize, 1024)
    319        stat_.f_bavail = stat_.f_bfree
    320
    321        inodes = self.get_row('SELECT COUNT(id) FROM inodes')[0]
    322        stat_.f_files = inodes
    323        stat_.f_ffree = max(inodes , 100)
    324        stat_.f_favail = stat_.f_ffree
    325
    326        return stat_
    327
    328    def open(self, inode, flags, ctx):
    329        # Yeah, unused arguments
    330        #pylint: disable=W0613
    331        self.inode_open_count[inode] += 1
    332
    333        # Use inodes as a file handles
    334        return inode
    335
    336    def access(self, inode, mode, ctx):
    337        # Yeah, could be a function and has unused arguments
    338        #pylint: disable=R0201,W0613
    339        return True
    340
    341    def create(self, inode_parent, name, mode, flags, ctx):
    342        #pylint: disable=W0612
    343        entry = self._create(inode_parent, name, mode, ctx)
    344        self.inode_open_count[entry.st_ino] += 1
    345        return (entry.st_ino, entry)
    346
    347    def _create(self, inode_p, name, mode, ctx, rdev=0, target=None):
    348        if self.getattr(inode_p).st_nlink == 0:
    349            log.warn('Attempted to create entry %s with unlinked parent %d',
    350                     name, inode_p)
    351            raise FUSEError(errno.EINVAL)
    352
    353        now_ns = int(time() * 1e9)
    354        self.cursor.execute('INSERT INTO inodes (uid, gid, mode, mtime_ns, atime_ns, '
    355                            'ctime_ns, target, rdev) VALUES(?, ?, ?, ?, ?, ?, ?, ?)',
    356                            (ctx.uid, ctx.gid, mode, now_ns, now_ns, now_ns, target, rdev))
    357
    358        inode = self.cursor.lastrowid
    359        self.db.execute("INSERT INTO contents(name, inode, parent_inode) VALUES(?,?,?)",
    360                        (name, inode, inode_p))
    361        return self.getattr(inode)
    362
    363    def read(self, fh, offset, length):
    364        data = self.get_row('SELECT data FROM inodes WHERE id=?', (fh,))[0]
    365        if data is None:
    366            data = b''
    367        return data[offset:offset+length]
    368
    369    def write(self, fh, offset, buf):
    370        data = self.get_row('SELECT data FROM inodes WHERE id=?', (fh,))[0]
    371        if data is None:
    372            data = b''
    373        data = data[:offset] + buf + data[offset+len(buf):]
    374
    375        self.cursor.execute('UPDATE inodes SET data=?, size=? WHERE id=?',
    376                            (memoryview(data), len(data), fh))
    377        return len(buf)
    378
    379    def release(self, fh):
    380        self.inode_open_count[fh] -= 1
    381
    382        if self.inode_open_count[fh] == 0:
    383            del self.inode_open_count[fh]
    384            if self.getattr(fh).st_nlink == 0:
    385                self.cursor.execute("DELETE FROM inodes WHERE id=?", (fh,))
    386
    387class NoUniqueValueError(Exception):
    388    def __str__(self):
    389        return 'Query generated more than 1 result row'
    390
    391
    392class NoSuchRowError(Exception):
    393    def __str__(self):
    394        return 'Query produced 0 result rows'
    395
    396def init_logging(debug=False):
    397    formatter = logging.Formatter('%(asctime)s.%(msecs)03d %(threadName)s: '
    398                                  '[%(name)s] %(message)s', datefmt="%Y-%m-%d %H:%M:%S")
    399    handler = logging.StreamHandler()
    400    handler.setFormatter(formatter)
    401    root_logger = logging.getLogger()
    402    if debug:
    403        handler.setLevel(logging.DEBUG)
    404        root_logger.setLevel(logging.DEBUG)
    405    else:
    406        handler.setLevel(logging.INFO)
    407        root_logger.setLevel(logging.INFO)
    408    root_logger.addHandler(handler)
    409
    410def parse_args():
    411    '''Parse command line'''
    412
    413    parser = ArgumentParser()
    414
    415    parser.add_argument('mountpoint', type=str,
    416                        help='Where to mount the file system')
    417    parser.add_argument('--debug', action='store_true', default=False,
    418                        help='Enable debugging output')
    419    parser.add_argument('--debug-fuse', action='store_true', default=False,
    420                        help='Enable FUSE debugging output')
    421
    422    return parser.parse_args()
    423
    424
    425if __name__ == '__main__':
    426
    427    options = parse_args()
    428    init_logging(options.debug)
    429    operations = Operations()
    430
    431    fuse_options = set(llfuse.default_options)
    432    fuse_options.add('fsname=tmpfs')
    433    fuse_options.discard('default_permissions')
    434    if options.debug_fuse:
    435        fuse_options.add('debug')
    436    llfuse.init(operations, options.mountpoint, fuse_options)
    437
    438    # sqlite3 does not support multithreading
    439    try:
    440        llfuse.main(workers=1)
    441    except:
    442        llfuse.close(unmount=False)
    443        raise
    444
    445    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()