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()