1 # Copyright (C) The Arvados Authors. All rights reserved.
3 # SPDX-License-Identifier: AGPL-3.0
5 """FUSE driver for Arvados Keep
9 There is one `Operations` object per mount point. It is the entry point for all
10 read and write requests from the llfuse module.
12 The operations object owns an `Inodes` object. The inodes object stores the
13 mapping from numeric inode (used throughout the file system API to uniquely
14 identify files) to the Python objects that implement files and directories.
16 The `Inodes` object owns an `InodeCache` object. The inode cache records the
17 memory footprint of file system objects and when they are last used. When the
18 cache limit is exceeded, the least recently used objects are cleared.
20 File system objects inherit from `fresh.FreshBase` which manages the object lifecycle.
22 File objects inherit from `fusefile.File`. Key methods are `readfrom` and `writeto`
23 which implement actual reads and writes.
25 Directory objects inherit from `fusedir.Directory`. The directory object wraps
26 a Python dict which stores the mapping from filenames to directory entries.
27 Directory contents can be accessed through the Python operators such as `[]`
28 and `in`. These methods automatically check if the directory is fresh (up to
29 date) or stale (needs update) and will call `update` if necessary before
32 The general FUSE operation flow is as follows:
34 - The request handler is called with either an inode or file handle that is the
35 subject of the operation.
37 - Look up the inode using the Inodes table or the file handle in the
38 filehandles table to get the file system object.
40 - For methods that alter files or directories, check that the operation is
41 valid and permitted using _check_writable().
43 - Call the relevant method on the file system object.
47 The FUSE driver supports the Arvados event bus. When an event is received for
48 an object that is live in the inode cache, that object is immediately updated.
50 Implementation note: in the code, the terms 'object', 'entry' and
51 'inode' are used somewhat interchangeably, but generally mean an
52 arvados_fuse.File or arvados_fuse.Directory object which has numeric
53 inode assigned to it and appears in the Inodes._entries dictionary.
71 from prometheus_client import Summary
73 from dataclasses import dataclass
76 from .fusedir import Directory, CollectionDirectory, TmpCollectionDirectory, MagicDirectory, TagsDirectory, ProjectDirectory, SharedDirectory, CollectionDirectoryBase
77 from .fusefile import File, StringFile, FuseArvadosFile
79 _logger = logging.getLogger('arvados.arvados_fuse')
81 # Uncomment this to enable llfuse debug logging.
82 # log_handler = logging.StreamHandler()
83 # llogger = logging.getLogger('llfuse')
84 # llogger.addHandler(log_handler)
85 # llogger.setLevel(logging.DEBUG)
88 """Connects a numeric file handle to a File or Directory object that has
89 been opened by the client."""
91 def __init__(self, fh, obj):
103 class FileHandle(Handle):
104 """Connects a numeric file handle to a File object that has
105 been opened by the client."""
108 if self.obj.writable():
109 return self.obj.flush()
112 class DirectoryHandle(Handle):
113 """Connects a numeric file handle to a Directory object that has
114 been opened by the client.
116 DirectoryHandle is used by opendir() and readdir() to get
117 directory listings. Entries returned by readdir() don't increment
118 the lookup count (kernel references), so increment our internal
119 "use count" to avoid having an item being removed mid-read.
123 def __init__(self, fh, dirobj, entries):
124 super(DirectoryHandle, self).__init__(fh, dirobj)
125 self.entries = entries
127 for ent in self.entries:
131 for ent in self.entries:
133 super(DirectoryHandle, self).release()
136 class InodeCache(object):
137 """Records the memory footprint of objects and when they are last used.
139 When the cache limit is exceeded, the least recently used objects
140 are cleared. Clearing the object means discarding its contents to
141 release memory. The next time the object is accessed, it must be
142 re-fetched from the server. Note that the inode cache limit is a
143 soft limit; the cache limit may be exceeded if necessary to load
144 very large projects or collections, it may also be exceeded if an
145 inode can't be safely discarded based on kernel lookups
146 (has_ref()) or internal use count (in_use()).
150 def __init__(self, cap, min_entries=4):
151 # Standard dictionaries are ordered, but OrderedDict is still better here, see
152 # https://docs.python.org/3.11/library/collections.html#ordereddict-objects
153 # specifically we use move_to_end() which standard dicts don't have.
154 self._cache_entries = collections.OrderedDict()
157 self.min_entries = min_entries
162 def evict_candidates(self):
163 """Yield entries that are candidates to be evicted
164 and stop when the cache total has shrunk sufficiently.
166 Implements a LRU cache, when an item is added or touch()ed it
167 goes to the back of the OrderedDict, so items in the front are
168 oldest. The Inodes._remove() function determines if the entry
169 can actually be removed safely.
173 if self._total <= self.cap:
176 _logger.debug("InodeCache evict_candidates total %i cap %i entries %i", self._total, self.cap, len(self._cache_entries))
178 # Copy this into a deque for two reasons:
180 # 1. _cache_entries is modified by unmanage() which is called
183 # 2. popping off the front means the reference goes away
184 # immediately intead of sticking around for the lifetime of
186 values = collections.deque(self._cache_entries.values())
189 if self._total < self.cap or len(self._cache_entries) < self.min_entries:
191 yield values.popleft()
193 def unmanage(self, entry):
194 """Stop managing an object in the cache.
196 This happens when an object is being removed from the inode
201 if entry.inode not in self._cache_entries:
204 # manage cache size running sum
205 self._total -= entry.cache_size
208 # Now forget about it
209 del self._cache_entries[entry.inode]
211 def update_cache_size(self, obj):
212 """Update the cache total in response to the footprint of an
213 object changing (usually because it has been loaded or
216 Adds or removes entries to the cache list based on the object
221 if not obj.persisted():
224 if obj.inode in self._cache_entries:
225 self._total -= obj.cache_size
227 obj.cache_size = obj.objsize()
229 if obj.cache_size > 0 or obj.parent_inode is None:
230 self._total += obj.cache_size
231 self._cache_entries[obj.inode] = obj
232 elif obj.cache_size == 0 and obj.inode in self._cache_entries:
233 del self._cache_entries[obj.inode]
235 def touch(self, obj):
236 """Indicate an object was used recently, making it low
237 priority to be removed from the cache.
240 if obj.inode in self._cache_entries:
241 self._cache_entries.move_to_end(obj.inode)
246 self._cache_entries.clear()
251 entry: typing.Union[Directory, File]
252 def inode_op(self, inodes, locked_ops):
253 if locked_ops is None:
254 inodes._remove(self.entry)
257 locked_ops.append(self)
261 class InvalidateInode:
263 def inode_op(self, inodes, locked_ops):
264 llfuse.invalidate_inode(self.inode)
268 class InvalidateEntry:
271 def inode_op(self, inodes, locked_ops):
272 llfuse.invalidate_entry(self.inode, self.name)
276 class EvictCandidates:
277 def inode_op(self, inodes, locked_ops):
281 class Inodes(object):
282 """Manage the set of inodes.
284 This is the mapping from a numeric id to a concrete File or
289 def __init__(self, inode_cache, encoding="utf-8", fsns=None, shutdown_started=None):
291 self._counter = itertools.count(llfuse.ROOT_INODE)
292 self.inode_cache = inode_cache
293 self.encoding = encoding
295 self._shutdown_started = shutdown_started or threading.Event()
297 self._inode_remove_queue = queue.Queue()
298 self._inode_remove_thread = threading.Thread(None, self._inode_remove)
299 self._inode_remove_thread.daemon = True
300 self._inode_remove_thread.start()
302 self._by_uuid = collections.defaultdict(list)
304 def __getitem__(self, item):
305 return self._entries[item]
307 def __setitem__(self, key, item):
308 self._entries[key] = item
311 return iter(self._entries.keys())
314 return self._entries.items()
316 def __contains__(self, k):
317 return k in self._entries
319 def touch(self, entry):
320 """Update the access time, adjust the cache position, and
321 notify the _inode_remove thread to recheck the cache.
325 entry._atime = time.time()
326 if self.inode_cache.touch(entry):
330 """Notify the _inode_remove thread to recheck the cache."""
331 if self._inode_remove_queue.empty():
332 self._inode_remove_queue.put(EvictCandidates())
334 def update_uuid(self, entry):
335 """Update the Arvados uuid associated with an inode entry.
337 This is used to look up inodes that need to be invalidated
338 when a websocket event indicates the object has changed on the
342 if entry.cache_uuid and entry in self._by_uuid[entry.cache_uuid]:
343 self._by_uuid[entry.cache_uuid].remove(entry)
345 entry.cache_uuid = entry.uuid()
346 if entry.cache_uuid and entry not in self._by_uuid[entry.cache_uuid]:
347 self._by_uuid[entry.cache_uuid].append(entry)
349 if not self._by_uuid[entry.cache_uuid]:
350 del self._by_uuid[entry.cache_uuid]
352 def add_entry(self, entry):
353 """Assign a numeric inode to a new entry."""
355 entry.inode = next(self._counter)
356 if entry.inode == llfuse.ROOT_INODE:
358 self._entries[entry.inode] = entry
360 self.update_uuid(entry)
361 self.inode_cache.update_cache_size(entry)
365 def del_entry(self, entry):
366 """Remove entry from the inode table.
368 Indicate this inode entry is pending deletion by setting
369 parent_inode to None. Notify the _inode_remove thread to try
374 entry.parent_inode = None
375 self._inode_remove_queue.put(RemoveInode(entry))
376 _logger.debug("del_entry on inode %i with refcount %i", entry.inode, entry.ref_count)
378 def _inode_remove(self):
379 """Background thread to handle tasks related to invalidating
380 inodes in the kernel, and removing objects from the inodes
385 locked_ops = collections.deque()
386 shutting_down = False
387 while not shutting_down:
392 qentry = self._inode_remove_queue.get(blocking_get)
401 # Process (or defer) this entry
402 qentry.inode_op(self, locked_ops)
405 # Give up the reference
410 locked_ops.popleft().inode_op(self, None)
411 for entry in self.inode_cache.evict_candidates():
414 # Unblock _inode_remove_queue.join() only when all of the
415 # deferred work is done, i.e., after calling inode_op()
416 # and then evict_candidates().
417 for _ in range(tasks_done):
418 self._inode_remove_queue.task_done()
420 def wait_remove_queue_empty(self):
422 self._inode_remove_queue.join()
424 def _remove(self, entry):
425 """Remove an inode entry if possible.
427 If the entry is still referenced or in use, don't do anything.
428 If this is not referenced but the parent is still referenced,
429 clear any data held by the object (which may include directory
430 entries under the object) but don't remove it from the inode
435 if entry.inode is None:
439 if entry.inode == llfuse.ROOT_INODE:
443 # referenced internally, stay pinned
444 #_logger.debug("InodeCache cannot clear inode %i, in use", entry.inode)
447 # Tell the kernel it should forget about it
448 entry.kernel_invalidate()
451 # has kernel reference, could still be accessed.
452 # when the kernel forgets about it, we can delete it.
453 #_logger.debug("InodeCache cannot clear inode %i, is referenced", entry.inode)
456 # commit any pending changes
457 with llfuse.lock_released:
463 if entry.parent_inode is None:
464 _logger.debug("InodeCache forgetting inode %i, object cache_size %i, cache total %i, forget_inode True, inode entries %i, type %s",
465 entry.inode, entry.cache_size, self.inode_cache.total(),
466 len(self._entries), type(entry))
469 self._by_uuid[entry.cache_uuid].remove(entry)
470 if not self._by_uuid[entry.cache_uuid]:
471 del self._by_uuid[entry.cache_uuid]
472 entry.cache_uuid = None
474 self.inode_cache.unmanage(entry)
476 del self._entries[entry.inode]
479 except Exception as e:
480 _logger.exception("failed remove")
482 def invalidate_inode(self, entry):
484 # Only necessary if the kernel has previously done a lookup on this
485 # inode and hasn't yet forgotten about it.
486 self._inode_remove_queue.put(InvalidateInode(entry.inode))
488 def invalidate_entry(self, entry, name):
490 # Only necessary if the kernel has previously done a lookup on this
491 # inode and hasn't yet forgotten about it.
492 self._inode_remove_queue.put(InvalidateEntry(entry.inode, name.encode(self.encoding)))
494 def begin_shutdown(self):
495 self._inode_remove_queue.put(None)
496 if self._inode_remove_thread is not None:
497 self._inode_remove_thread.join()
498 self._inode_remove_thread = None
501 with llfuse.lock_released:
502 self.begin_shutdown()
504 self.inode_cache.clear()
505 self._by_uuid.clear()
507 for k,v in self._entries.items():
510 except Exception as e:
511 _logger.exception("Error during finalize of inode %i", k)
513 self._entries.clear()
515 def forward_slash_subst(self):
518 def find_by_uuid(self, uuid):
519 """Return a list of zero or more inode entries corresponding
520 to this Arvados UUID."""
521 return self._by_uuid.get(uuid, [])
524 def catch_exceptions(orig_func):
525 """Catch uncaught exceptions and log them consistently."""
527 @functools.wraps(orig_func)
528 def catch_exceptions_wrapper(self, *args, **kwargs):
530 return orig_func(self, *args, **kwargs)
531 except llfuse.FUSEError:
533 except EnvironmentError as e:
534 raise llfuse.FUSEError(e.errno)
535 except NotImplementedError:
536 raise llfuse.FUSEError(errno.ENOTSUP)
537 except arvados.errors.KeepWriteError as e:
538 _logger.error("Keep write error: " + str(e))
539 raise llfuse.FUSEError(errno.EIO)
540 except arvados.errors.NotFoundError as e:
541 _logger.error("Block not found error: " + str(e))
542 raise llfuse.FUSEError(errno.EIO)
544 _logger.exception("Unhandled exception during FUSE operation")
545 raise llfuse.FUSEError(errno.EIO)
547 return catch_exceptions_wrapper
550 class Operations(llfuse.Operations):
551 """This is the main interface with llfuse.
553 The methods on this object are called by llfuse threads to service FUSE
554 events to query and read from the file system.
556 llfuse has its own global lock which is acquired before calling a request handler,
557 so request handlers do not run concurrently unless the lock is explicitly released
558 using 'with llfuse.lock_released:'
562 fuse_time = Summary('arvmount_fuse_operations_seconds', 'Time spent during FUSE operations', labelnames=['op'])
563 read_time = fuse_time.labels(op='read')
564 write_time = fuse_time.labels(op='write')
565 destroy_time = fuse_time.labels(op='destroy')
566 on_event_time = fuse_time.labels(op='on_event')
567 getattr_time = fuse_time.labels(op='getattr')
568 setattr_time = fuse_time.labels(op='setattr')
569 lookup_time = fuse_time.labels(op='lookup')
570 forget_time = fuse_time.labels(op='forget')
571 open_time = fuse_time.labels(op='open')
572 release_time = fuse_time.labels(op='release')
573 opendir_time = fuse_time.labels(op='opendir')
574 readdir_time = fuse_time.labels(op='readdir')
575 statfs_time = fuse_time.labels(op='statfs')
576 create_time = fuse_time.labels(op='create')
577 mkdir_time = fuse_time.labels(op='mkdir')
578 unlink_time = fuse_time.labels(op='unlink')
579 rmdir_time = fuse_time.labels(op='rmdir')
580 rename_time = fuse_time.labels(op='rename')
581 flush_time = fuse_time.labels(op='flush')
583 def __init__(self, uid, gid, api_client, encoding="utf-8", inode_cache=None, num_retries=4, enable_write=False, fsns=None):
584 super(Operations, self).__init__()
586 self._api_client = api_client
589 inode_cache = InodeCache(cap=256*1024*1024)
593 fsns = self._api_client.config()["Collections"]["ForwardSlashNameSubstitution"]
595 # old API server with no FSNS config
598 if fsns == '' or fsns == '/':
601 # If we get overlapping shutdown events (e.g., fusermount -u
602 # -z and operations.destroy()) llfuse calls forget() on inodes
603 # that have already been deleted. To avoid this, we make
604 # forget() a no-op if called after destroy().
605 self._shutdown_started = threading.Event()
607 self.inodes = Inodes(inode_cache, encoding=encoding, fsns=fsns,
608 shutdown_started=self._shutdown_started)
611 self.enable_write = enable_write
613 # dict of inode to filehandle
614 self._filehandles = {}
615 self._filehandles_counter = itertools.count(0)
617 # Other threads that need to wait until the fuse driver
618 # is fully initialized should wait() on this event object.
619 self.initlock = threading.Event()
621 self.num_retries = num_retries
623 self.read_counter = arvados.keep.Counter()
624 self.write_counter = arvados.keep.Counter()
625 self.read_ops_counter = arvados.keep.Counter()
626 self.write_ops_counter = arvados.keep.Counter()
631 # Allow threads that are waiting for the driver to be finished
632 # initializing to continue
635 def metric_samples(self):
636 return self.fuse_time.collect()[0].samples
638 def metric_op_names(self):
640 for cur_op in [sample.labels['op'] for sample in self.metric_samples()]:
641 if cur_op not in ops:
645 def metric_value(self, opname, metric):
646 op_value = [sample.value for sample in self.metric_samples()
647 if sample.name == metric and sample.labels['op'] == opname]
648 return op_value[0] if len(op_value) == 1 else None
650 def metric_sum_func(self, opname):
651 return lambda: self.metric_value(opname, "arvmount_fuse_operations_seconds_sum")
653 def metric_count_func(self, opname):
654 return lambda: int(self.metric_value(opname, "arvmount_fuse_operations_seconds_count"))
656 def begin_shutdown(self):
657 self._shutdown_started.set()
658 self.inodes.begin_shutdown()
663 _logger.debug("arv-mount destroy: start")
665 with llfuse.lock_released:
666 self.begin_shutdown()
674 _logger.debug("arv-mount destroy: complete")
677 def access(self, inode, mode, ctx):
680 def listen_for_events(self):
681 self.events = arvados.events.subscribe(
683 [["event_type", "in", ["create", "update", "delete"]]],
686 @on_event_time.time()
688 def on_event(self, ev):
689 if 'event_type' not in ev or ev["event_type"] not in ("create", "update", "delete"):
692 properties = ev.get("properties") or {}
693 old_attrs = properties.get("old_attributes") or {}
694 new_attrs = properties.get("new_attributes") or {}
696 for item in self.inodes.find_by_uuid(ev["object_uuid"]):
699 oldowner = old_attrs.get("owner_uuid")
700 newowner = ev.get("object_owner_uuid")
702 self.inodes.find_by_uuid(oldowner) +
703 self.inodes.find_by_uuid(newowner)):
708 def getattr(self, inode, ctx=None):
709 if inode not in self.inodes:
710 _logger.debug("arv-mount getattr: inode %i missing", inode)
711 raise llfuse.FUSEError(errno.ENOENT)
713 e = self.inodes[inode]
717 parent = self.inodes[e.parent_inode]
718 self.inodes.touch(parent)
720 entry = llfuse.EntryAttributes()
723 entry.entry_timeout = parent.time_to_next_poll() if parent is not None else 0
724 entry.attr_timeout = e.time_to_next_poll() if e.allow_attr_cache else 0
726 entry.st_mode = stat.S_IRUSR | stat.S_IRGRP | stat.S_IROTH
727 if isinstance(e, Directory):
728 entry.st_mode |= stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH | stat.S_IFDIR
730 entry.st_mode |= stat.S_IFREG
731 if isinstance(e, FuseArvadosFile):
732 entry.st_mode |= stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH
734 if self.enable_write and e.writable():
735 entry.st_mode |= stat.S_IWUSR | stat.S_IWGRP | stat.S_IWOTH
738 entry.st_uid = self.uid
739 entry.st_gid = self.gid
742 entry.st_size = e.size()
744 entry.st_blksize = 512
745 entry.st_blocks = (entry.st_size // 512) + 1
746 if hasattr(entry, 'st_atime_ns'):
748 entry.st_atime_ns = int(e.atime() * 1000000000)
749 entry.st_mtime_ns = int(e.mtime() * 1000000000)
750 entry.st_ctime_ns = int(e.mtime() * 1000000000)
753 entry.st_atime = int(e.atime)
754 entry.st_mtime = int(e.mtime)
755 entry.st_ctime = int(e.mtime)
761 def setattr(self, inode, attr, fields=None, fh=None, ctx=None):
762 entry = self.getattr(inode)
764 if fh is not None and fh in self._filehandles:
765 handle = self._filehandles[fh]
768 e = self.inodes[inode]
772 update_size = attr.st_size is not None
775 update_size = fields.update_size
776 if update_size and isinstance(e, FuseArvadosFile):
777 with llfuse.lock_released:
778 e.arvfile.truncate(attr.st_size)
779 entry.st_size = e.arvfile.size()
785 def lookup(self, parent_inode, name, ctx=None):
786 name = str(name, self.inodes.encoding)
791 elif parent_inode in self.inodes:
792 p = self.inodes[parent_inode]
795 inode = p.parent_inode
796 elif isinstance(p, Directory) and name in p:
797 if p[name].inode is None:
798 _logger.debug("arv-mount lookup: parent_inode %i name '%s' found but inode was None",
800 raise llfuse.FUSEError(errno.ENOENT)
802 inode = p[name].inode
805 _logger.debug("arv-mount lookup: parent_inode %i name '%s' inode %i",
806 parent_inode, name, inode)
807 self.inodes.touch(self.inodes[inode])
808 self.inodes[inode].inc_ref()
809 return self.getattr(inode)
811 _logger.debug("arv-mount lookup: parent_inode %i name '%s' not found",
813 raise llfuse.FUSEError(errno.ENOENT)
817 def forget(self, inodes):
818 if self._shutdown_started.is_set():
820 for inode, nlookup in inodes:
821 ent = self.inodes[inode]
822 _logger.debug("arv-mount forget: inode %i nlookup %i ref_count %i", inode, nlookup, ent.ref_count)
823 if ent.dec_ref(nlookup) == 0 and ent.parent_inode is None:
824 self.inodes.del_entry(ent)
828 def open(self, inode, flags, ctx=None):
829 if inode in self.inodes:
830 p = self.inodes[inode]
832 _logger.debug("arv-mount open: inode %i missing", inode)
833 raise llfuse.FUSEError(errno.ENOENT)
835 if isinstance(p, Directory):
836 raise llfuse.FUSEError(errno.EISDIR)
838 if ((flags & os.O_WRONLY) or (flags & os.O_RDWR)) and not p.writable():
839 raise llfuse.FUSEError(errno.EPERM)
841 fh = next(self._filehandles_counter)
842 self._filehandles[fh] = FileHandle(fh, p)
845 # Normally, we will have received an "update" event if the
846 # parent collection is stale here. However, even if the parent
847 # collection hasn't changed, the manifest might have been
848 # fetched so long ago that the signatures on the data block
849 # locators have expired. Calling checkupdate() on all
850 # ancestors ensures the signatures will be refreshed if
852 while p.parent_inode in self.inodes:
853 if p == self.inodes[p.parent_inode]:
855 p = self.inodes[p.parent_inode]
859 _logger.debug("arv-mount open inode %i flags %x fh %i", inode, flags, fh)
865 def read(self, fh, off, size):
866 _logger.debug("arv-mount read fh %i off %i size %i", fh, off, size)
867 self.read_ops_counter.add(1)
869 if fh in self._filehandles:
870 handle = self._filehandles[fh]
872 raise llfuse.FUSEError(errno.EBADF)
874 self.inodes.touch(handle.obj)
876 r = handle.obj.readfrom(off, size, self.num_retries)
878 self.read_counter.add(len(r))
883 def write(self, fh, off, buf):
884 _logger.debug("arv-mount write %i %i %i", fh, off, len(buf))
885 self.write_ops_counter.add(1)
887 if fh in self._filehandles:
888 handle = self._filehandles[fh]
890 raise llfuse.FUSEError(errno.EBADF)
892 if not handle.obj.writable():
893 raise llfuse.FUSEError(errno.EPERM)
895 self.inodes.touch(handle.obj)
897 w = handle.obj.writeto(off, buf, self.num_retries)
899 self.write_counter.add(w)
904 def release(self, fh):
905 if fh in self._filehandles:
906 _logger.debug("arv-mount release fh %i", fh)
908 self._filehandles[fh].flush()
912 self._filehandles[fh].release()
913 del self._filehandles[fh]
914 self.inodes.cap_cache()
916 def releasedir(self, fh):
921 def opendir(self, inode, ctx=None):
922 _logger.debug("arv-mount opendir: inode %i", inode)
924 if inode in self.inodes:
925 p = self.inodes[inode]
927 _logger.debug("arv-mount opendir: called with unknown or removed inode %i", inode)
928 raise llfuse.FUSEError(errno.ENOENT)
930 if not isinstance(p, Directory):
931 raise llfuse.FUSEError(errno.ENOTDIR)
933 fh = next(self._filehandles_counter)
934 if p.parent_inode in self.inodes:
935 parent = self.inodes[p.parent_inode]
937 _logger.warning("arv-mount opendir: parent inode %i of %i is missing", p.parent_inode, inode)
938 raise llfuse.FUSEError(errno.EIO)
940 _logger.debug("arv-mount opendir: inode %i fh %i ", inode, fh)
944 self._filehandles[fh] = DirectoryHandle(fh, p, [('.', p), ('..', parent)] + p.items())
951 def readdir(self, fh, off):
952 _logger.debug("arv-mount readdir: fh %i off %i", fh, off)
954 if fh in self._filehandles:
955 handle = self._filehandles[fh]
957 raise llfuse.FUSEError(errno.EBADF)
960 while e < len(handle.entries):
961 ent = handle.entries[e]
962 if ent[1].inode in self.inodes:
963 yield (ent[0].encode(self.inodes.encoding), self.getattr(ent[1].inode), e+1)
968 def statfs(self, ctx=None):
969 st = llfuse.StatvfsData()
970 st.f_bsize = 128 * 1024
983 def _check_writable(self, inode_parent):
984 if not self.enable_write:
985 raise llfuse.FUSEError(errno.EROFS)
987 if inode_parent in self.inodes:
988 p = self.inodes[inode_parent]
990 raise llfuse.FUSEError(errno.ENOENT)
992 if not isinstance(p, Directory):
993 raise llfuse.FUSEError(errno.ENOTDIR)
996 raise llfuse.FUSEError(errno.EPERM)
1002 def create(self, inode_parent, name, mode, flags, ctx=None):
1003 name = name.decode(encoding=self.inodes.encoding)
1004 _logger.debug("arv-mount create: parent_inode %i '%s' %o", inode_parent, name, mode)
1006 p = self._check_writable(inode_parent)
1009 # The file entry should have been implicitly created by callback.
1011 fh = next(self._filehandles_counter)
1012 self._filehandles[fh] = FileHandle(fh, f)
1013 self.inodes.touch(p)
1016 return (fh, self.getattr(f.inode))
1020 def mkdir(self, inode_parent, name, mode, ctx=None):
1021 name = name.decode(encoding=self.inodes.encoding)
1022 _logger.debug("arv-mount mkdir: parent_inode %i '%s' %o", inode_parent, name, mode)
1024 p = self._check_writable(inode_parent)
1027 # The dir entry should have been implicitly created by callback.
1031 return self.getattr(d.inode)
1035 def unlink(self, inode_parent, name, ctx=None):
1036 name = name.decode(encoding=self.inodes.encoding)
1037 _logger.debug("arv-mount unlink: parent_inode %i '%s'", inode_parent, name)
1038 p = self._check_writable(inode_parent)
1043 def rmdir(self, inode_parent, name, ctx=None):
1044 name = name.decode(encoding=self.inodes.encoding)
1045 _logger.debug("arv-mount rmdir: parent_inode %i '%s'", inode_parent, name)
1046 p = self._check_writable(inode_parent)
1051 def rename(self, inode_parent_old, name_old, inode_parent_new, name_new, ctx=None):
1052 name_old = name_old.decode(encoding=self.inodes.encoding)
1053 name_new = name_new.decode(encoding=self.inodes.encoding)
1054 _logger.debug("arv-mount rename: old_parent_inode %i '%s' new_parent_inode %i '%s'", inode_parent_old, name_old, inode_parent_new, name_new)
1055 src = self._check_writable(inode_parent_old)
1056 dest = self._check_writable(inode_parent_new)
1057 dest.rename(name_old, name_new, src)
1061 def flush(self, fh):
1062 if fh in self._filehandles:
1063 self._filehandles[fh].flush()
1065 def fsync(self, fh, datasync):
1068 def fsyncdir(self, fh, datasync):