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