Example File Systems¶
pyfuse3 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# -*- coding: utf-8 -*-
3'''
4hello.py - Example file system for pyfuse3.
5
6This program presents a static file system containing a single file.
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
25import os
26import sys
27
28# If we are running from the pyfuse3 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', 'pyfuse3', '__init__.pyx'))):
33 sys.path.insert(0, os.path.join(basedir, 'src'))
34
35from argparse import ArgumentParser
36import stat
37import logging
38import errno
39import pyfuse3
40import trio
41
42try:
43 import faulthandler
44except ImportError:
45 pass
46else:
47 faulthandler.enable()
48
49log = logging.getLogger(__name__)
50
51class TestFs(pyfuse3.Operations):
52 def __init__(self):
53 super(TestFs, self).__init__()
54 self.hello_name = b"message"
55 self.hello_inode = pyfuse3.ROOT_INODE+1
56 self.hello_data = b"hello world\n"
57
58 async def getattr(self, inode, ctx=None):
59 entry = pyfuse3.EntryAttributes()
60 if inode == pyfuse3.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 pyfuse3.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 async def lookup(self, parent_inode, name, ctx=None):
80 if parent_inode != pyfuse3.ROOT_INODE or name != self.hello_name:
81 raise pyfuse3.FUSEError(errno.ENOENT)
82 return await self.getattr(self.hello_inode)
83
84 async def opendir(self, inode, ctx):
85 if inode != pyfuse3.ROOT_INODE:
86 raise pyfuse3.FUSEError(errno.ENOENT)
87 return inode
88
89 async def readdir(self, fh, start_id, token):
90 assert fh == pyfuse3.ROOT_INODE
91
92 # only one entry
93 if start_id == 0:
94 pyfuse3.readdir_reply(
95 token, self.hello_name, await self.getattr(self.hello_inode), 1)
96 return
97
98 async def open(self, inode, flags, ctx):
99 if inode != self.hello_inode:
100 raise pyfuse3.FUSEError(errno.ENOENT)
101 if flags & os.O_RDWR or flags & os.O_WRONLY:
102 raise pyfuse3.FUSEError(errno.EACCES)
103 return pyfuse3.FileInfo(fh=inode)
104
105 async def read(self, fh, off, size):
106 assert fh == self.hello_inode
107 return self.hello_data[off:off+size]
108
109def init_logging(debug=False):
110 formatter = logging.Formatter('%(asctime)s.%(msecs)03d %(threadName)s: '
111 '[%(name)s] %(message)s', datefmt="%Y-%m-%d %H:%M:%S")
112 handler = logging.StreamHandler()
113 handler.setFormatter(formatter)
114 root_logger = logging.getLogger()
115 if debug:
116 handler.setLevel(logging.DEBUG)
117 root_logger.setLevel(logging.DEBUG)
118 else:
119 handler.setLevel(logging.INFO)
120 root_logger.setLevel(logging.INFO)
121 root_logger.addHandler(handler)
122
123def parse_args():
124 '''Parse command line'''
125
126 parser = ArgumentParser()
127
128 parser.add_argument('mountpoint', type=str,
129 help='Where to mount the file system')
130 parser.add_argument('--debug', action='store_true', default=False,
131 help='Enable debugging output')
132 parser.add_argument('--debug-fuse', action='store_true', default=False,
133 help='Enable FUSE debugging output')
134 return parser.parse_args()
135
136
137def main():
138 options = parse_args()
139 init_logging(options.debug)
140
141 testfs = TestFs()
142 fuse_options = set(pyfuse3.default_options)
143 fuse_options.add('fsname=hello')
144 if options.debug_fuse:
145 fuse_options.add('debug')
146 pyfuse3.init(testfs, options.mountpoint, fuse_options)
147 try:
148 trio.run(pyfuse3.main)
149 except:
150 pyfuse3.close(unmount=False)
151 raise
152
153 pyfuse3.close()
154
155
156if __name__ == '__main__':
157 main()
In-memory File System¶
(shipped as examples/tmpfs.py
)
1#!/usr/bin/env python3
2# -*- coding: utf-8 -*-
3'''
4tmpfs.py - Example file system for pyfuse3.
5
6This file system stores all data in memory.
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
24import os
25import sys
26
27# If we are running from the pyfuse3 source directory, try
28# to load the module from there first.
29basedir = os.path.abspath(os.path.join(os.path.dirname(sys.argv[0]), '..'))
30if (os.path.exists(os.path.join(basedir, 'setup.py')) and
31 os.path.exists(os.path.join(basedir, 'src', 'pyfuse3', '__init__.pyx'))):
32 sys.path.insert(0, os.path.join(basedir, 'src'))
33
34import pyfuse3
35import errno
36import stat
37from time import time
38import sqlite3
39import logging
40from collections import defaultdict
41from pyfuse3 import FUSEError
42from argparse import ArgumentParser
43import trio
44
45try:
46 import faulthandler
47except ImportError:
48 pass
49else:
50 faulthandler.enable()
51
52log = logging.getLogger()
53
54class Operations(pyfuse3.Operations):
55 '''An example filesystem that stores all data in memory
56
57 This is a very simple implementation with terrible performance.
58 Don't try to store significant amounts of data. Also, there are
59 some other flaws that have not been fixed to keep the code easier
60 to understand:
61
62 * atime, mtime and ctime are not updated
63 * generation numbers are not supported
64 * lookup counts are not maintained
65 '''
66
67 enable_writeback_cache = True
68
69 def __init__(self):
70 super(Operations, self).__init__()
71 self.db = sqlite3.connect(':memory:')
72 self.db.text_factory = str
73 self.db.row_factory = sqlite3.Row
74 self.cursor = self.db.cursor()
75 self.inode_open_count = defaultdict(int)
76 self.init_tables()
77
78 def init_tables(self):
79 '''Initialize file system tables'''
80
81 self.cursor.execute("""
82 CREATE TABLE inodes (
83 id INTEGER PRIMARY KEY,
84 uid INT NOT NULL,
85 gid INT NOT NULL,
86 mode INT NOT NULL,
87 mtime_ns INT NOT NULL,
88 atime_ns INT NOT NULL,
89 ctime_ns INT NOT NULL,
90 target BLOB(256) ,
91 size INT NOT NULL DEFAULT 0,
92 rdev INT NOT NULL DEFAULT 0,
93 data BLOB
94 )
95 """)
96
97 self.cursor.execute("""
98 CREATE TABLE contents (
99 rowid INTEGER PRIMARY KEY AUTOINCREMENT,
100 name BLOB(256) NOT NULL,
101 inode INT NOT NULL REFERENCES inodes(id),
102 parent_inode INT NOT NULL REFERENCES inodes(id),
103
104 UNIQUE (name, parent_inode)
105 )""")
106
107 # Insert root directory
108 now_ns = int(time() * 1e9)
109 self.cursor.execute("INSERT INTO inodes (id,mode,uid,gid,mtime_ns,atime_ns,ctime_ns) "
110 "VALUES (?,?,?,?,?,?,?)",
111 (pyfuse3.ROOT_INODE, stat.S_IFDIR | stat.S_IRUSR | stat.S_IWUSR
112 | stat.S_IXUSR | stat.S_IRGRP | stat.S_IXGRP | stat.S_IROTH
113 | stat.S_IXOTH, os.getuid(), os.getgid(), now_ns, now_ns, now_ns))
114 self.cursor.execute("INSERT INTO contents (name, parent_inode, inode) VALUES (?,?,?)",
115 (b'..', pyfuse3.ROOT_INODE, pyfuse3.ROOT_INODE))
116
117
118 def get_row(self, *a, **kw):
119 self.cursor.execute(*a, **kw)
120 try:
121 row = next(self.cursor)
122 except StopIteration:
123 raise NoSuchRowError()
124 try:
125 next(self.cursor)
126 except StopIteration:
127 pass
128 else:
129 raise NoUniqueValueError()
130
131 return row
132
133 async def lookup(self, inode_p, name, ctx=None):
134 if name == '.':
135 inode = inode_p
136 elif name == '..':
137 inode = self.get_row("SELECT * FROM contents WHERE inode=?",
138 (inode_p,))['parent_inode']
139 else:
140 try:
141 inode = self.get_row("SELECT * FROM contents WHERE name=? AND parent_inode=?",
142 (name, inode_p))['inode']
143 except NoSuchRowError:
144 raise(pyfuse3.FUSEError(errno.ENOENT))
145
146 return await self.getattr(inode, ctx)
147
148
149 async def getattr(self, inode, ctx=None):
150 try:
151 row = self.get_row("SELECT * FROM inodes WHERE id=?", (inode,))
152 except NoSuchRowError:
153 raise(pyfuse3.FUSEError(errno.ENOENT))
154
155 entry = pyfuse3.EntryAttributes()
156 entry.st_ino = inode
157 entry.generation = 0
158 entry.entry_timeout = 300
159 entry.attr_timeout = 300
160 entry.st_mode = row['mode']
161 entry.st_nlink = self.get_row("SELECT COUNT(inode) FROM contents WHERE inode=?",
162 (inode,))[0]
163 entry.st_uid = row['uid']
164 entry.st_gid = row['gid']
165 entry.st_rdev = row['rdev']
166 entry.st_size = row['size']
167
168 entry.st_blksize = 512
169 entry.st_blocks = 1
170 entry.st_atime_ns = row['atime_ns']
171 entry.st_mtime_ns = row['mtime_ns']
172 entry.st_ctime_ns = row['ctime_ns']
173
174 return entry
175
176 async def readlink(self, inode, ctx):
177 return self.get_row('SELECT * FROM inodes WHERE id=?', (inode,))['target']
178
179 async def opendir(self, inode, ctx):
180 return inode
181
182 async def readdir(self, inode, off, token):
183 if off == 0:
184 off = -1
185
186 cursor2 = self.db.cursor()
187 cursor2.execute("SELECT * FROM contents WHERE parent_inode=? "
188 'AND rowid > ? ORDER BY rowid', (inode, off))
189
190 for row in cursor2:
191 pyfuse3.readdir_reply(
192 token, row['name'], await self.getattr(row['inode']), row['rowid'])
193
194 async def unlink(self, inode_p, name,ctx):
195 entry = await self.lookup(inode_p, name)
196
197 if stat.S_ISDIR(entry.st_mode):
198 raise pyfuse3.FUSEError(errno.EISDIR)
199
200 self._remove(inode_p, name, entry)
201
202 async def rmdir(self, inode_p, name, ctx):
203 entry = await self.lookup(inode_p, name)
204
205 if not stat.S_ISDIR(entry.st_mode):
206 raise pyfuse3.FUSEError(errno.ENOTDIR)
207
208 self._remove(inode_p, name, entry)
209
210 def _remove(self, inode_p, name, entry):
211 if self.get_row("SELECT COUNT(inode) FROM contents WHERE parent_inode=?",
212 (entry.st_ino,))[0] > 0:
213 raise pyfuse3.FUSEError(errno.ENOTEMPTY)
214
215 self.cursor.execute("DELETE FROM contents WHERE name=? AND parent_inode=?",
216 (name, inode_p))
217
218 if entry.st_nlink == 1 and entry.st_ino not in self.inode_open_count:
219 self.cursor.execute("DELETE FROM inodes WHERE id=?", (entry.st_ino,))
220
221 async def symlink(self, inode_p, name, target, ctx):
222 mode = (stat.S_IFLNK | stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR |
223 stat.S_IRGRP | stat.S_IWGRP | stat.S_IXGRP |
224 stat.S_IROTH | stat.S_IWOTH | stat.S_IXOTH)
225 return await self._create(inode_p, name, mode, ctx, target=target)
226
227 async def rename(self, inode_p_old, name_old, inode_p_new, name_new,
228 flags, ctx):
229 if flags != 0:
230 raise FUSEError(errno.EINVAL)
231
232 entry_old = await self.lookup(inode_p_old, name_old)
233
234 try:
235 entry_new = await self.lookup(inode_p_new, name_new)
236 except pyfuse3.FUSEError as exc:
237 if exc.errno != errno.ENOENT:
238 raise
239 target_exists = False
240 else:
241 target_exists = True
242
243 if target_exists:
244 self._replace(inode_p_old, name_old, inode_p_new, name_new,
245 entry_old, entry_new)
246 else:
247 self.cursor.execute("UPDATE contents SET name=?, parent_inode=? WHERE name=? "
248 "AND parent_inode=?", (name_new, inode_p_new,
249 name_old, inode_p_old))
250
251 def _replace(self, inode_p_old, name_old, inode_p_new, name_new,
252 entry_old, entry_new):
253
254 if self.get_row("SELECT COUNT(inode) FROM contents WHERE parent_inode=?",
255 (entry_new.st_ino,))[0] > 0:
256 raise pyfuse3.FUSEError(errno.ENOTEMPTY)
257
258 self.cursor.execute("UPDATE contents SET inode=? WHERE name=? AND parent_inode=?",
259 (entry_old.st_ino, name_new, inode_p_new))
260 self.db.execute('DELETE FROM contents WHERE name=? AND parent_inode=?',
261 (name_old, inode_p_old))
262
263 if entry_new.st_nlink == 1 and entry_new.st_ino not in self.inode_open_count:
264 self.cursor.execute("DELETE FROM inodes WHERE id=?", (entry_new.st_ino,))
265
266
267 async def link(self, inode, new_inode_p, new_name, ctx):
268 entry_p = await self.getattr(new_inode_p)
269 if entry_p.st_nlink == 0:
270 log.warning('Attempted to create entry %s with unlinked parent %d',
271 new_name, new_inode_p)
272 raise FUSEError(errno.EINVAL)
273
274 self.cursor.execute("INSERT INTO contents (name, inode, parent_inode) VALUES(?,?,?)",
275 (new_name, inode, new_inode_p))
276
277 return await self.getattr(inode)
278
279 async def setattr(self, inode, attr, fields, fh, ctx):
280
281 if fields.update_size:
282 data = self.get_row('SELECT data FROM inodes WHERE id=?', (inode,))[0]
283 if data is None:
284 data = b''
285 if len(data) < attr.st_size:
286 data = data + b'\0' * (attr.st_size - len(data))
287 else:
288 data = data[:attr.st_size]
289 self.cursor.execute('UPDATE inodes SET data=?, size=? WHERE id=?',
290 (memoryview(data), attr.st_size, inode))
291 if fields.update_mode:
292 self.cursor.execute('UPDATE inodes SET mode=? WHERE id=?',
293 (attr.st_mode, inode))
294
295 if fields.update_uid:
296 self.cursor.execute('UPDATE inodes SET uid=? WHERE id=?',
297 (attr.st_uid, inode))
298
299 if fields.update_gid:
300 self.cursor.execute('UPDATE inodes SET gid=? WHERE id=?',
301 (attr.st_gid, inode))
302
303 if fields.update_atime:
304 self.cursor.execute('UPDATE inodes SET atime_ns=? WHERE id=?',
305 (attr.st_atime_ns, inode))
306
307 if fields.update_mtime:
308 self.cursor.execute('UPDATE inodes SET mtime_ns=? WHERE id=?',
309 (attr.st_mtime_ns, inode))
310
311 if fields.update_ctime:
312 self.cursor.execute('UPDATE inodes SET ctime_ns=? WHERE id=?',
313 (attr.st_ctime_ns, inode))
314 else:
315 self.cursor.execute('UPDATE inodes SET ctime_ns=? WHERE id=?',
316 (int(time()*1e9), inode))
317
318 return await self.getattr(inode)
319
320 async def mknod(self, inode_p, name, mode, rdev, ctx):
321 return await self._create(inode_p, name, mode, ctx, rdev=rdev)
322
323 async def mkdir(self, inode_p, name, mode, ctx):
324 return await self._create(inode_p, name, mode, ctx)
325
326 async def statfs(self, ctx):
327 stat_ = pyfuse3.StatvfsData()
328
329 stat_.f_bsize = 512
330 stat_.f_frsize = 512
331
332 size = self.get_row('SELECT SUM(size) FROM inodes')[0]
333 stat_.f_blocks = size // stat_.f_frsize
334 stat_.f_bfree = max(size // stat_.f_frsize, 1024)
335 stat_.f_bavail = stat_.f_bfree
336
337 inodes = self.get_row('SELECT COUNT(id) FROM inodes')[0]
338 stat_.f_files = inodes
339 stat_.f_ffree = max(inodes , 100)
340 stat_.f_favail = stat_.f_ffree
341
342 return stat_
343
344 async def open(self, inode, flags, ctx):
345 # Yeah, unused arguments
346 #pylint: disable=W0613
347 self.inode_open_count[inode] += 1
348
349 # Use inodes as a file handles
350 return pyfuse3.FileInfo(fh=inode)
351
352 async def access(self, inode, mode, ctx):
353 # Yeah, could be a function and has unused arguments
354 #pylint: disable=R0201,W0613
355 return True
356
357 async def create(self, inode_parent, name, mode, flags, ctx):
358 #pylint: disable=W0612
359 entry = await self._create(inode_parent, name, mode, ctx)
360 self.inode_open_count[entry.st_ino] += 1
361 return (pyfuse3.FileInfo(fh=entry.st_ino), entry)
362
363 async def _create(self, inode_p, name, mode, ctx, rdev=0, target=None):
364 if (await self.getattr(inode_p)).st_nlink == 0:
365 log.warning('Attempted to create entry %s with unlinked parent %d',
366 name, inode_p)
367 raise FUSEError(errno.EINVAL)
368
369 now_ns = int(time() * 1e9)
370 self.cursor.execute('INSERT INTO inodes (uid, gid, mode, mtime_ns, atime_ns, '
371 'ctime_ns, target, rdev) VALUES(?, ?, ?, ?, ?, ?, ?, ?)',
372 (ctx.uid, ctx.gid, mode, now_ns, now_ns, now_ns, target, rdev))
373
374 inode = self.cursor.lastrowid
375 self.db.execute("INSERT INTO contents(name, inode, parent_inode) VALUES(?,?,?)",
376 (name, inode, inode_p))
377 return await self.getattr(inode)
378
379 async def read(self, fh, offset, length):
380 data = self.get_row('SELECT data FROM inodes WHERE id=?', (fh,))[0]
381 if data is None:
382 data = b''
383 return data[offset:offset+length]
384
385 async def write(self, fh, offset, buf):
386 data = self.get_row('SELECT data FROM inodes WHERE id=?', (fh,))[0]
387 if data is None:
388 data = b''
389 data = data[:offset] + buf + data[offset+len(buf):]
390
391 self.cursor.execute('UPDATE inodes SET data=?, size=? WHERE id=?',
392 (memoryview(data), len(data), fh))
393 return len(buf)
394
395 async def release(self, fh):
396 self.inode_open_count[fh] -= 1
397
398 if self.inode_open_count[fh] == 0:
399 del self.inode_open_count[fh]
400 if (await self.getattr(fh)).st_nlink == 0:
401 self.cursor.execute("DELETE FROM inodes WHERE id=?", (fh,))
402
403class NoUniqueValueError(Exception):
404 def __str__(self):
405 return 'Query generated more than 1 result row'
406
407
408class NoSuchRowError(Exception):
409 def __str__(self):
410 return 'Query produced 0 result rows'
411
412def init_logging(debug=False):
413 formatter = logging.Formatter('%(asctime)s.%(msecs)03d %(threadName)s: '
414 '[%(name)s] %(message)s', datefmt="%Y-%m-%d %H:%M:%S")
415 handler = logging.StreamHandler()
416 handler.setFormatter(formatter)
417 root_logger = logging.getLogger()
418 if debug:
419 handler.setLevel(logging.DEBUG)
420 root_logger.setLevel(logging.DEBUG)
421 else:
422 handler.setLevel(logging.INFO)
423 root_logger.setLevel(logging.INFO)
424 root_logger.addHandler(handler)
425
426def parse_args():
427 '''Parse command line'''
428
429 parser = ArgumentParser()
430
431 parser.add_argument('mountpoint', type=str,
432 help='Where to mount the file system')
433 parser.add_argument('--debug', action='store_true', default=False,
434 help='Enable debugging output')
435 parser.add_argument('--debug-fuse', action='store_true', default=False,
436 help='Enable FUSE debugging output')
437
438 return parser.parse_args()
439
440if __name__ == '__main__':
441
442 options = parse_args()
443 init_logging(options.debug)
444 operations = Operations()
445
446 fuse_options = set(pyfuse3.default_options)
447 fuse_options.add('fsname=tmpfs')
448 fuse_options.discard('default_permissions')
449 if options.debug_fuse:
450 fuse_options.add('debug')
451 pyfuse3.init(operations, options.mountpoint, fuse_options)
452
453 try:
454 trio.run(pyfuse3.main)
455 except:
456 pyfuse3.close(unmount=False)
457 raise
458
459 pyfuse3.close()
Passthrough / Overlay File System¶
(shipped as examples/passthroughfs.py
)
1#!/usr/bin/env python3
2'''
3passthroughfs.py - Example file system for pyfuse3
4
5This file system mirrors the contents of a specified directory tree.
6
7Caveats:
8
9 * Inode generation numbers are not passed through but set to zero.
10
11 * Block size (st_blksize) and number of allocated blocks (st_blocks) are not
12 passed through.
13
14 * Performance for large directories is not good, because the directory
15 is always read completely.
16
17 * There may be a way to break-out of the directory tree.
18
19 * The readdir implementation is not fully POSIX compliant. If a directory
20 contains hardlinks and is modified during a readdir call, readdir()
21 may return some of the hardlinked files twice or omit them completely.
22
23 * If you delete or rename files in the underlying file system, the
24 passthrough file system will get confused.
25
26Copyright © Nikolaus Rath <Nikolaus.org>
27
28Permission is hereby granted, free of charge, to any person obtaining a copy of
29this software and associated documentation files (the "Software"), to deal in
30the Software without restriction, including without limitation the rights to
31use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
32the Software, and to permit persons to whom the Software is furnished to do so.
33
34THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
35IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
36FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
37COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
38IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
39CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
40'''
41
42import os
43import sys
44
45# If we are running from the pyfuse3 source directory, try
46# to load the module from there first.
47basedir = os.path.abspath(os.path.join(os.path.dirname(sys.argv[0]), '..'))
48if (os.path.exists(os.path.join(basedir, 'setup.py')) and
49 os.path.exists(os.path.join(basedir, 'src', 'pyfuse3', '__init__.pyx'))):
50 sys.path.insert(0, os.path.join(basedir, 'src'))
51
52import pyfuse3
53from argparse import ArgumentParser
54import errno
55import logging
56import stat as stat_m
57from pyfuse3 import FUSEError
58from os import fsencode, fsdecode
59from collections import defaultdict
60import trio
61
62import faulthandler
63faulthandler.enable()
64
65log = logging.getLogger(__name__)
66
67class Operations(pyfuse3.Operations):
68
69 enable_writeback_cache = True
70
71 def __init__(self, source):
72 super().__init__()
73 self._inode_path_map = { pyfuse3.ROOT_INODE: source }
74 self._lookup_cnt = defaultdict(lambda : 0)
75 self._fd_inode_map = dict()
76 self._inode_fd_map = dict()
77 self._fd_open_count = dict()
78
79 def _inode_to_path(self, inode):
80 try:
81 val = self._inode_path_map[inode]
82 except KeyError:
83 raise FUSEError(errno.ENOENT)
84
85 if isinstance(val, set):
86 # In case of hardlinks, pick any path
87 val = next(iter(val))
88 return val
89
90 def _add_path(self, inode, path):
91 log.debug('_add_path for %d, %s', inode, path)
92 self._lookup_cnt[inode] += 1
93
94 # With hardlinks, one inode may map to multiple paths.
95 if inode not in self._inode_path_map:
96 self._inode_path_map[inode] = path
97 return
98
99 val = self._inode_path_map[inode]
100 if isinstance(val, set):
101 val.add(path)
102 elif val != path:
103 self._inode_path_map[inode] = { path, val }
104
105 async def forget(self, inode_list):
106 for (inode, nlookup) in inode_list:
107 if self._lookup_cnt[inode] > nlookup:
108 self._lookup_cnt[inode] -= nlookup
109 continue
110 log.debug('forgetting about inode %d', inode)
111 assert inode not in self._inode_fd_map
112 del self._lookup_cnt[inode]
113 try:
114 del self._inode_path_map[inode]
115 except KeyError: # may have been deleted
116 pass
117
118 async def lookup(self, inode_p, name, ctx=None):
119 name = fsdecode(name)
120 log.debug('lookup for %s in %d', name, inode_p)
121 path = os.path.join(self._inode_to_path(inode_p), name)
122 attr = self._getattr(path=path)
123 if name != '.' and name != '..':
124 self._add_path(attr.st_ino, path)
125 return attr
126
127 async def getattr(self, inode, ctx=None):
128 if inode in self._inode_fd_map:
129 return self._getattr(fd=self._inode_fd_map[inode])
130 else:
131 return self._getattr(path=self._inode_to_path(inode))
132
133 def _getattr(self, path=None, fd=None):
134 assert fd is None or path is None
135 assert not(fd is None and path is None)
136 try:
137 if fd is None:
138 stat = os.lstat(path)
139 else:
140 stat = os.fstat(fd)
141 except OSError as exc:
142 raise FUSEError(exc.errno)
143
144 entry = pyfuse3.EntryAttributes()
145 for attr in ('st_ino', 'st_mode', 'st_nlink', 'st_uid', 'st_gid',
146 'st_rdev', 'st_size', 'st_atime_ns', 'st_mtime_ns',
147 'st_ctime_ns'):
148 setattr(entry, attr, getattr(stat, attr))
149 entry.generation = 0
150 entry.entry_timeout = 0
151 entry.attr_timeout = 0
152 entry.st_blksize = 512
153 entry.st_blocks = ((entry.st_size+entry.st_blksize-1) // entry.st_blksize)
154
155 return entry
156
157 async def readlink(self, inode, ctx):
158 path = self._inode_to_path(inode)
159 try:
160 target = os.readlink(path)
161 except OSError as exc:
162 raise FUSEError(exc.errno)
163 return fsencode(target)
164
165 async def opendir(self, inode, ctx):
166 return inode
167
168 async def readdir(self, inode, off, token):
169 path = self._inode_to_path(inode)
170 log.debug('reading %s', path)
171 entries = []
172 for name in os.listdir(path):
173 if name == '.' or name == '..':
174 continue
175 attr = self._getattr(path=os.path.join(path, name))
176 entries.append((attr.st_ino, name, attr))
177
178 log.debug('read %d entries, starting at %d', len(entries), off)
179
180 # This is not fully posix compatible. If there are hardlinks
181 # (two names with the same inode), we don't have a unique
182 # offset to start in between them. Note that we cannot simply
183 # count entries, because then we would skip over entries
184 # (or return them more than once) if the number of directory
185 # entries changes between two calls to readdir().
186 for (ino, name, attr) in sorted(entries):
187 if ino <= off:
188 continue
189 if not pyfuse3.readdir_reply(
190 token, fsencode(name), attr, ino):
191 break
192 self._add_path(attr.st_ino, os.path.join(path, name))
193
194 async def unlink(self, inode_p, name, ctx):
195 name = fsdecode(name)
196 parent = self._inode_to_path(inode_p)
197 path = os.path.join(parent, name)
198 try:
199 inode = os.lstat(path).st_ino
200 os.unlink(path)
201 except OSError as exc:
202 raise FUSEError(exc.errno)
203 if inode in self._lookup_cnt:
204 self._forget_path(inode, path)
205
206 async def rmdir(self, inode_p, name, ctx):
207 name = fsdecode(name)
208 parent = self._inode_to_path(inode_p)
209 path = os.path.join(parent, name)
210 try:
211 inode = os.lstat(path).st_ino
212 os.rmdir(path)
213 except OSError as exc:
214 raise FUSEError(exc.errno)
215 if inode in self._lookup_cnt:
216 self._forget_path(inode, path)
217
218 def _forget_path(self, inode, path):
219 log.debug('forget %s for %d', path, inode)
220 val = self._inode_path_map[inode]
221 if isinstance(val, set):
222 val.remove(path)
223 if len(val) == 1:
224 self._inode_path_map[inode] = next(iter(val))
225 else:
226 del self._inode_path_map[inode]
227
228 async def symlink(self, inode_p, name, target, ctx):
229 name = fsdecode(name)
230 target = fsdecode(target)
231 parent = self._inode_to_path(inode_p)
232 path = os.path.join(parent, name)
233 try:
234 os.symlink(target, path)
235 os.chown(path, ctx.uid, ctx.gid, follow_symlinks=False)
236 except OSError as exc:
237 raise FUSEError(exc.errno)
238 stat = os.lstat(path)
239 self._add_path(stat.st_ino, path)
240 return await self.getattr(stat.st_ino)
241
242 async def rename(self, inode_p_old, name_old, inode_p_new, name_new,
243 flags, ctx):
244 if flags != 0:
245 raise FUSEError(errno.EINVAL)
246
247 name_old = fsdecode(name_old)
248 name_new = fsdecode(name_new)
249 parent_old = self._inode_to_path(inode_p_old)
250 parent_new = self._inode_to_path(inode_p_new)
251 path_old = os.path.join(parent_old, name_old)
252 path_new = os.path.join(parent_new, name_new)
253 try:
254 os.rename(path_old, path_new)
255 inode = os.lstat(path_new).st_ino
256 except OSError as exc:
257 raise FUSEError(exc.errno)
258 if inode not in self._lookup_cnt:
259 return
260
261 val = self._inode_path_map[inode]
262 if isinstance(val, set):
263 assert len(val) > 1
264 val.add(path_new)
265 val.remove(path_old)
266 else:
267 assert val == path_old
268 self._inode_path_map[inode] = path_new
269
270 async def link(self, inode, new_inode_p, new_name, ctx):
271 new_name = fsdecode(new_name)
272 parent = self._inode_to_path(new_inode_p)
273 path = os.path.join(parent, new_name)
274 try:
275 os.link(self._inode_to_path(inode), path, follow_symlinks=False)
276 except OSError as exc:
277 raise FUSEError(exc.errno)
278 self._add_path(inode, path)
279 return await self.getattr(inode)
280
281 async def setattr(self, inode, attr, fields, fh, ctx):
282 # We use the f* functions if possible so that we can handle
283 # a setattr() call for an inode without associated directory
284 # handle.
285 if fh is None:
286 path_or_fh = self._inode_to_path(inode)
287 truncate = os.truncate
288 chmod = os.chmod
289 chown = os.chown
290 stat = os.lstat
291 else:
292 path_or_fh = fh
293 truncate = os.ftruncate
294 chmod = os.fchmod
295 chown = os.fchown
296 stat = os.fstat
297
298 try:
299 if fields.update_size:
300 truncate(path_or_fh, attr.st_size)
301
302 if fields.update_mode:
303 # Under Linux, chmod always resolves symlinks so we should
304 # actually never get a setattr() request for a symbolic
305 # link.
306 assert not stat_m.S_ISLNK(attr.st_mode)
307 chmod(path_or_fh, stat_m.S_IMODE(attr.st_mode))
308
309 if fields.update_uid:
310 chown(path_or_fh, attr.st_uid, -1, follow_symlinks=False)
311
312 if fields.update_gid:
313 chown(path_or_fh, -1, attr.st_gid, follow_symlinks=False)
314
315 if fields.update_atime and fields.update_mtime:
316 if fh is None:
317 os.utime(path_or_fh, None, follow_symlinks=False,
318 ns=(attr.st_atime_ns, attr.st_mtime_ns))
319 else:
320 os.utime(path_or_fh, None,
321 ns=(attr.st_atime_ns, attr.st_mtime_ns))
322 elif fields.update_atime or fields.update_mtime:
323 # We can only set both values, so we first need to retrieve the
324 # one that we shouldn't be changing.
325 oldstat = stat(path_or_fh)
326 if not fields.update_atime:
327 attr.st_atime_ns = oldstat.st_atime_ns
328 else:
329 attr.st_mtime_ns = oldstat.st_mtime_ns
330 if fh is None:
331 os.utime(path_or_fh, None, follow_symlinks=False,
332 ns=(attr.st_atime_ns, attr.st_mtime_ns))
333 else:
334 os.utime(path_or_fh, None,
335 ns=(attr.st_atime_ns, attr.st_mtime_ns))
336
337 except OSError as exc:
338 raise FUSEError(exc.errno)
339
340 return await self.getattr(inode)
341
342 async def mknod(self, inode_p, name, mode, rdev, ctx):
343 path = os.path.join(self._inode_to_path(inode_p), fsdecode(name))
344 try:
345 os.mknod(path, mode=(mode & ~ctx.umask), device=rdev)
346 os.chown(path, ctx.uid, ctx.gid)
347 except OSError as exc:
348 raise FUSEError(exc.errno)
349 attr = self._getattr(path=path)
350 self._add_path(attr.st_ino, path)
351 return attr
352
353 async def mkdir(self, inode_p, name, mode, ctx):
354 path = os.path.join(self._inode_to_path(inode_p), fsdecode(name))
355 try:
356 os.mkdir(path, mode=(mode & ~ctx.umask))
357 os.chown(path, ctx.uid, ctx.gid)
358 except OSError as exc:
359 raise FUSEError(exc.errno)
360 attr = self._getattr(path=path)
361 self._add_path(attr.st_ino, path)
362 return attr
363
364 async def statfs(self, ctx):
365 root = self._inode_path_map[pyfuse3.ROOT_INODE]
366 stat_ = pyfuse3.StatvfsData()
367 try:
368 statfs = os.statvfs(root)
369 except OSError as exc:
370 raise FUSEError(exc.errno)
371 for attr in ('f_bsize', 'f_frsize', 'f_blocks', 'f_bfree', 'f_bavail',
372 'f_files', 'f_ffree', 'f_favail'):
373 setattr(stat_, attr, getattr(statfs, attr))
374 stat_.f_namemax = statfs.f_namemax - (len(root)+1)
375 return stat_
376
377 async def open(self, inode, flags, ctx):
378 if inode in self._inode_fd_map:
379 fd = self._inode_fd_map[inode]
380 self._fd_open_count[fd] += 1
381 return pyfuse3.FileInfo(fh=fd)
382 assert flags & os.O_CREAT == 0
383 try:
384 fd = os.open(self._inode_to_path(inode), flags)
385 except OSError as exc:
386 raise FUSEError(exc.errno)
387 self._inode_fd_map[inode] = fd
388 self._fd_inode_map[fd] = inode
389 self._fd_open_count[fd] = 1
390 return pyfuse3.FileInfo(fh=fd)
391
392 async def create(self, inode_p, name, mode, flags, ctx):
393 path = os.path.join(self._inode_to_path(inode_p), fsdecode(name))
394 try:
395 fd = os.open(path, flags | os.O_CREAT | os.O_TRUNC)
396 except OSError as exc:
397 raise FUSEError(exc.errno)
398 attr = self._getattr(fd=fd)
399 self._add_path(attr.st_ino, path)
400 self._inode_fd_map[attr.st_ino] = fd
401 self._fd_inode_map[fd] = attr.st_ino
402 self._fd_open_count[fd] = 1
403 return (pyfuse3.FileInfo(fh=fd), attr)
404
405 async def read(self, fd, offset, length):
406 os.lseek(fd, offset, os.SEEK_SET)
407 return os.read(fd, length)
408
409 async def write(self, fd, offset, buf):
410 os.lseek(fd, offset, os.SEEK_SET)
411 return os.write(fd, buf)
412
413 async def release(self, fd):
414 if self._fd_open_count[fd] > 1:
415 self._fd_open_count[fd] -= 1
416 return
417
418 del self._fd_open_count[fd]
419 inode = self._fd_inode_map[fd]
420 del self._inode_fd_map[inode]
421 del self._fd_inode_map[fd]
422 try:
423 os.close(fd)
424 except OSError as exc:
425 raise FUSEError(exc.errno)
426
427def init_logging(debug=False):
428 formatter = logging.Formatter('%(asctime)s.%(msecs)03d %(threadName)s: '
429 '[%(name)s] %(message)s', datefmt="%Y-%m-%d %H:%M:%S")
430 handler = logging.StreamHandler()
431 handler.setFormatter(formatter)
432 root_logger = logging.getLogger()
433 if debug:
434 handler.setLevel(logging.DEBUG)
435 root_logger.setLevel(logging.DEBUG)
436 else:
437 handler.setLevel(logging.INFO)
438 root_logger.setLevel(logging.INFO)
439 root_logger.addHandler(handler)
440
441
442def parse_args(args):
443 '''Parse command line'''
444
445 parser = ArgumentParser()
446
447 parser.add_argument('source', type=str,
448 help='Directory tree to mirror')
449 parser.add_argument('mountpoint', type=str,
450 help='Where to mount the file system')
451 parser.add_argument('--debug', action='store_true', default=False,
452 help='Enable debugging output')
453 parser.add_argument('--debug-fuse', action='store_true', default=False,
454 help='Enable FUSE debugging output')
455
456 return parser.parse_args(args)
457
458def main():
459 options = parse_args(sys.argv[1:])
460 init_logging(options.debug)
461 operations = Operations(options.source)
462
463 log.debug('Mounting...')
464 fuse_options = set(pyfuse3.default_options)
465 fuse_options.add('fsname=passthroughfs')
466 if options.debug_fuse:
467 fuse_options.add('debug')
468 pyfuse3.init(operations, options.mountpoint, fuse_options)
469
470 try:
471 log.debug('Entering main loop..')
472 trio.run(pyfuse3.main)
473 except:
474 pyfuse3.close(unmount=False)
475 raise
476
477 log.debug('Unmounting..')
478 pyfuse3.close()
479
480if __name__ == '__main__':
481 main()