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