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