tessl install tessl/pypi-pyfuse3@3.4.0Python 3 bindings for libfuse 3 with async I/O support
The Operations class defines all filesystem operation handlers that a pyfuse3 filesystem can implement. Subclass this class and override the methods you need to implement your filesystem.
class Operations:
"""
Base class for FUSE filesystem implementations.
All operation handlers are async methods except init() and stacktrace().
Unimplemented handlers must raise FUSEError(errno.ENOSYS).
Only FUSEError exceptions should be raised from handlers.
Other exceptions may crash the filesystem or cause undefined behavior.
"""
supports_dot_lookup: bool = True
enable_writeback_cache: bool = False
enable_acl: bool = Falsesupports_dot_lookup: Whether filesystem supports . and .. lookup (default: True)
. and ..enable_writeback_cache: Enable kernel writeback caching (default: False)
enable_acl: Enable ACL support (default: False)
Called when filesystem starts, before handling requests. Must not raise exceptions.
def init(self) -> None:
"""
Initialize operations.
Called just before the filesystem starts handling requests.
Must not raise any exceptions (not even FUSEError).
Use this to set up any internal state.
This is a synchronous method (not async).
Notes:
- Called from main thread before worker tasks start
- Any setup that might fail should be done before pyfuse3.init()
- Use for initializing caches, connections, etc.
- Can't communicate with kernel (no FUSE operations available)
"""Look up a directory entry by name and get its attributes.
async def lookup(self, parent_inode: InodeT, name: FileNameT, ctx: RequestContext) -> EntryAttributes:
"""
Look up a directory entry by name.
Args:
parent_inode: Inode of parent directory
name: Name of entry to look up (bytes, no path separators)
ctx: Request context with caller information
Returns:
EntryAttributes for the entry
Notes:
- Return EntryAttributes with st_ino=0 for negative lookup (cached)
- Or raise FUSEError(errno.ENOENT) for negative lookup (not cached)
- Must handle . and .. lookups (unless supports_dot_lookup=False)
- Successful execution increases lookup count by one
- name is bytes, not str; may not be valid UTF-8
- name never contains b'/' or b'\0'
- name is never b'.' or b'..' if supports_dot_lookup=False
- entry_timeout controls how long entry is cached
- attr_timeout controls how long attributes are cached
Raises:
FUSEError(errno.ENOENT): Entry not found
FUSEError(errno.ENOTDIR): parent_inode is not a directory
FUSEError(errno.EACCES): Permission denied
Lookup Count:
Increases by 1 on successful return (not for negative lookups)
"""Get attributes for an inode.
async def getattr(self, inode: InodeT, ctx: RequestContext) -> EntryAttributes:
"""
Get attributes for inode.
Args:
inode: Inode number
ctx: Request context with caller information (may be None)
Returns:
EntryAttributes with current attributes
Notes:
- entry_timeout field is ignored in this context
- attr_timeout is still respected
- ctx may be None for kernel-initiated getattr calls
- Called frequently; should be fast
- Should return current attributes, not cached
Raises:
FUSEError(errno.ENOENT): Inode not found
FUSEError(errno.ENOSYS): Operation not implemented
Lookup Count:
Does not affect lookup count
"""Change attributes of an inode.
async def setattr(self, inode: InodeT, attr: EntryAttributes, fields: SetattrFields, fh: Optional[FileHandleT], ctx: RequestContext) -> EntryAttributes:
"""
Change attributes of inode.
Args:
inode: Inode number
attr: EntryAttributes with new values
fields: SetattrFields indicating which attributes to update
fh: File handle if called via fd (ftruncate, fchmod), None for path-based
ctx: Request context with caller information
Returns:
EntryAttributes with updated attributes
Notes:
- Only update attributes where fields indicates True
- Typically also update st_ctime_ns to current time
- attr contains new values only for attributes being updated
- fh is provided for operations on open files (ftruncate, fchmod, etc.)
- If fh is None, operation is on path (truncate, chmod, etc.)
- Must handle size changes (truncate/extend file)
- Must validate permission for requested changes
- entry_timeout field is ignored in this context
Raises:
FUSEError(errno.ENOENT): Inode not found
FUSEError(errno.EACCES): Permission denied
FUSEError(errno.EINVAL): Invalid attribute value
FUSEError(errno.ENOSPC): No space to extend file
FUSEError(errno.ENOSYS): Operation not implemented
Lookup Count:
Does not affect lookup count
"""Decrease lookup counts for inodes.
async def forget(self, inode_list: Sequence[Tuple[InodeT, int]]) -> None:
"""
Decrease lookup counts for inodes.
Args:
inode_list: List of (inode, nlookup) tuples
Notes:
- Reduce lookup count for each inode by nlookup
- When lookup count reaches zero, inode is not known to kernel
- Remove inode if no directory entries refer to it
- Must not raise exceptions (not even FUSEError)
- At unmount, may have non-zero lookup counts remaining
- May be called with same inode multiple times in list
- nlookup is always >= 1
- Can safely delete inode data when:
1. Lookup count is zero (after this call)
2. No directory entries reference it (nlink == 0)
Important:
This method MUST NOT raise any exceptions.
Errors should be logged but not raised.
Raising exceptions from forget() may crash the filesystem.
Lookup Count:
Decreases by nlookup for each (inode, nlookup) pair
"""Return target of a symbolic link.
async def readlink(self, inode: InodeT, ctx: RequestContext) -> FileNameT:
"""
Return target of symbolic link.
Args:
inode: Inode of symbolic link
ctx: Request context with caller information
Returns:
Target path as bytes
Notes:
- Return value may be absolute or relative path
- Return value is bytes, not str
- Target path is not validated or resolved
- Should return exact target as stored (no normalization)
- Kernel handles path resolution
Raises:
FUSEError(errno.ENOENT): Inode not found
FUSEError(errno.EINVAL): Not a symbolic link
FUSEError(errno.ENOSYS): Operation not implemented
Lookup Count:
Does not affect lookup count
"""Create a file (special or regular) in a directory.
async def mknod(self, parent_inode: InodeT, name: FileNameT, mode: ModeT, rdev: int, ctx: RequestContext) -> EntryAttributes:
"""
Create (possibly special) file.
Args:
parent_inode: Parent directory inode
name: Name for new file (bytes)
mode: File mode (type and permissions)
rdev: Device ID for block/char devices (otherwise ignored)
ctx: Request context with caller information
Returns:
EntryAttributes for newly created entry
Notes:
- Successful execution increases lookup count by one
- mode includes file type (S_IFREG, S_IFCHR, S_IFBLK, S_IFIFO, S_IFSOCK)
- Apply ctx.umask to mode: final_mode = mode & ~ctx.umask
- rdev only used for S_IFCHR and S_IFBLK
- Set owner to ctx.uid and ctx.gid
- Update parent directory mtime and ctime
- Kernel uses create() for regular files opened with O_CREAT
Raises:
FUSEError(errno.ENOENT): Parent directory not found
FUSEError(errno.ENOTDIR): parent_inode not a directory
FUSEError(errno.EEXIST): Entry already exists
FUSEError(errno.EACCES): Permission denied
FUSEError(errno.ENOSPC): No space available
FUSEError(errno.ENOSYS): Operation not implemented
Lookup Count:
Increases by 1 on success
"""Create a new directory.
async def mkdir(self, parent_inode: InodeT, name: FileNameT, mode: ModeT, ctx: RequestContext) -> EntryAttributes:
"""
Create a directory.
Args:
parent_inode: Parent directory inode
name: Name for new directory (bytes)
mode: Directory mode (permissions)
ctx: Request context with caller information
Returns:
EntryAttributes for newly created directory
Notes:
- Successful execution increases lookup count by one
- Apply ctx.umask to mode: final_mode = mode & ~ctx.umask
- Set owner to ctx.uid and ctx.gid
- Initialize st_nlink to 2 (. and ..)
- Update parent directory mtime, ctime, and nlink
- Parent nlink increases by 1 (new .. entry)
Raises:
FUSEError(errno.ENOENT): Parent directory not found
FUSEError(errno.ENOTDIR): parent_inode not a directory
FUSEError(errno.EEXIST): Entry already exists
FUSEError(errno.EACCES): Permission denied
FUSEError(errno.ENOSPC): No space available
FUSEError(errno.ENOSYS): Operation not implemented
Lookup Count:
Increases by 1 on success
"""Create a symbolic link.
async def symlink(self, parent_inode: InodeT, name: FileNameT, target: FileNameT, ctx: RequestContext) -> EntryAttributes:
"""
Create a symbolic link.
Args:
parent_inode: Parent directory inode
name: Name for new symlink (bytes)
target: Target path (bytes)
ctx: Request context with caller information
Returns:
EntryAttributes for newly created symlink
Notes:
- Successful execution increases lookup count by one
- target is stored as-is, not validated or resolved
- target may be absolute or relative path
- Set st_mode to S_IFLNK | 0o777 (symlinks have no permissions)
- Set st_size to len(target)
- Set owner to ctx.uid and ctx.gid
- Update parent directory mtime and ctime
Raises:
FUSEError(errno.ENOENT): Parent directory not found
FUSEError(errno.ENOTDIR): parent_inode not a directory
FUSEError(errno.EEXIST): Entry already exists
FUSEError(errno.EACCES): Permission denied
FUSEError(errno.ENOSPC): No space available
FUSEError(errno.ENAMETOOLONG): target too long
FUSEError(errno.ENOSYS): Operation not implemented
Lookup Count:
Increases by 1 on success
"""Create a hard link to an existing inode.
async def link(self, inode: InodeT, new_parent_inode: InodeT, new_name: FileNameT, ctx: RequestContext) -> EntryAttributes:
"""
Create directory entry referring to existing inode.
Args:
inode: Existing inode to link to
new_parent_inode: Parent directory for new entry
new_name: Name for new entry (bytes)
ctx: Request context with caller information
Returns:
EntryAttributes for newly created entry (same inode)
Notes:
- Successful execution increases lookup count by one
- Increment st_nlink for the inode
- Update inode's ctime
- Update parent directory mtime and ctime
- Cannot hardlink directories (except in rare cases)
- Should verify caller has permission for parent directory
Raises:
FUSEError(errno.ENOENT): inode or parent not found
FUSEError(errno.ENOTDIR): new_parent_inode not a directory
FUSEError(errno.EEXIST): Entry already exists
FUSEError(errno.EACCES): Permission denied
FUSEError(errno.EPERM): Cannot hardlink directories
FUSEError(errno.EMLINK): Too many links
FUSEError(errno.ENOSPC): No space available
FUSEError(errno.ENOSYS): Operation not implemented
Lookup Count:
Increases by 1 on success
"""Remove a file from a directory.
async def unlink(self, parent_inode: InodeT, name: FileNameT, ctx: RequestContext) -> None:
"""
Remove a (possibly special) file.
Args:
parent_inode: Parent directory inode
name: Name of file to remove (bytes)
ctx: Request context with caller information
Notes:
- Only remove directory entry; don't delete inode data yet
- Decrement st_nlink for the inode
- Update inode ctime and parent directory mtime/ctime
- If inode has non-zero lookup count, defer deletion to forget()
- If nlink reaches 0 and lookup count is 0, can delete inode data
- Must handle open files: deletion deferred until release()
- Kernel ensures entry exists and is not a directory
Raises:
FUSEError(errno.ENOENT): Parent or entry not found
FUSEError(errno.ENOTDIR): parent_inode not a directory
FUSEError(errno.EISDIR): Entry is a directory (use rmdir)
FUSEError(errno.EACCES): Permission denied
FUSEError(errno.ENOSYS): Operation not implemented
Lookup Count:
Does not affect lookup count (kernel handles separately)
"""Remove a directory from its parent.
async def rmdir(self, parent_inode: InodeT, name: FileNameT, ctx: RequestContext) -> None:
"""
Remove directory.
Args:
parent_inode: Parent directory inode
name: Name of directory to remove (bytes)
ctx: Request context with caller information
Notes:
- Raise FUSEError(errno.ENOTEMPTY) if directory not empty
- Only remove directory entry; don't delete inode data yet
- Decrement parent's st_nlink (removed .. entry)
- Decrement directory's st_nlink to 0
- Update directory ctime and parent directory mtime/ctime
- If directory has non-zero lookup count, defer deletion to forget()
- Kernel ensures entry exists and is a directory
- Must check directory is empty (no entries except . and ..)
Raises:
FUSEError(errno.ENOENT): Parent or entry not found
FUSEError(errno.ENOTDIR): parent_inode not a directory
FUSEError(errno.ENOTEMPTY): Directory not empty
FUSEError(errno.ENOTDIR): Entry is not a directory
FUSEError(errno.EACCES): Permission denied
FUSEError(errno.EBUSY): Directory in use
FUSEError(errno.ENOSYS): Operation not implemented
Lookup Count:
Does not affect lookup count (kernel handles separately)
"""Rename or move a directory entry.
async def rename(self, parent_inode_old: InodeT, name_old: FileNameT, parent_inode_new: InodeT, name_new: FileNameT, flags: FlagT, ctx: RequestContext) -> None:
"""
Rename a directory entry.
Args:
parent_inode_old: Old parent directory inode
name_old: Old name (bytes)
parent_inode_new: New parent directory inode
name_new: New name (bytes)
flags: RENAME_EXCHANGE, RENAME_NOREPLACE, or 0
ctx: Request context with caller information
Notes:
- If name_new exists, overwrite it (unless RENAME_NOREPLACE)
- RENAME_NOREPLACE (0x1): return EEXIST if name_new exists
- RENAME_EXCHANGE (0x2): atomically exchange the two files (both must exist)
- flags=0: standard rename (overwrite name_new if exists)
- Update ctime for renamed inode
- Update mtime/ctime for both parent directories
- Handle lookup counts correctly for replaced entries
- If moving directory, update .. entry and nlink counts
- If replacing entry, decrement its nlink and handle deletion
Raises:
FUSEError(errno.ENOENT): Old entry not found, or new entry not found (RENAME_EXCHANGE)
FUSEError(errno.ENOTDIR): Parent not a directory, or old is directory but new isn't
FUSEError(errno.EISDIR): old is not directory but new is
FUSEError(errno.EEXIST): name_new exists (RENAME_NOREPLACE)
FUSEError(errno.ENOTEMPTY): Replacing non-empty directory
FUSEError(errno.EINVAL): Invalid flags or renaming directory to subdirectory
FUSEError(errno.EACCES): Permission denied
FUSEError(errno.ENOSPC): No space available
FUSEError(errno.ENOSYS): Operation not implemented
Lookup Count:
Does not change lookup counts (kernel handles separately)
"""Open a file and return a file handle.
async def open(self, inode: InodeT, flags: FlagT, ctx: RequestContext) -> FileInfo:
"""
Open a file.
Args:
inode: Inode to open
flags: Open flags (O_RDONLY, O_WRONLY, O_RDWR, O_APPEND, O_ASYNC, O_SYNC, etc.)
ctx: Request context with caller information
Returns:
FileInfo with file handle and options
Notes:
- flags excludes O_CREAT, O_EXCL, O_NOCTTY, O_TRUNC (handled by kernel/create)
- FileInfo.fh must contain unique integer file handle
- File handle passed to read, write, flush, fsync, release
- Can set direct_io, keep_cache, nonseekable in FileInfo
- direct_io=True: bypass page cache (disable caching)
- keep_cache=True: keep page cache on open (don't invalidate)
- keep_cache=False: invalidate page cache on open
- nonseekable=True: file does not support seeking (pipes, sockets)
- Check permissions based on flags (read, write, or both)
- O_TRUNC is handled by kernel before calling open (via setattr)
Raises:
FUSEError(errno.ENOENT): Inode not found
FUSEError(errno.EISDIR): Inode is a directory
FUSEError(errno.EACCES): Permission denied
FUSEError(errno.ENFILE): Too many open files in system
FUSEError(errno.EMFILE): Too many open files for this filesystem
FUSEError(errno.ENOSYS): Operation not implemented
Lookup Count:
Does not affect lookup count
"""Read data from an open file.
async def read(self, fh: FileHandleT, off: int, size: int) -> bytes:
"""
Read data from file.
Args:
fh: File handle from open/create
off: Offset to read from (0-based byte offset)
size: Number of bytes to read
Returns:
Bytes read (exactly size bytes except on EOF or error)
Notes:
- Should return exactly size bytes except at EOF or error
- If fewer bytes returned, kernel assumes EOF
- If return empty bytes (b''), indicates EOF at offset off
- Otherwise data substituted with zeroes
- off may be beyond current file size (sparse files)
- Not required to check permissions (done at open time)
- May be called concurrently for same fh
Raises:
FUSEError(errno.EBADF): Invalid file handle
FUSEError(errno.EIO): I/O error
FUSEError(errno.ENOSYS): Operation not implemented
Lookup Count:
Does not affect lookup count
"""Write data to an open file.
async def write(self, fh: FileHandleT, off: int, buf: bytes) -> int:
"""
Write data to file.
Args:
fh: File handle from open/create
off: Offset to write at (0-based byte offset)
buf: Data to write
Returns:
Number of bytes written
Notes:
- Unless mounted with direct_io, must write all data
- Must return len(buf) unless direct_io is enabled
- Returning < len(buf) treated as error (unless direct_io)
- off may be beyond current file size (sparse files)
- Update file size if write extends file
- Update mtime and ctime
- Not required to check permissions (done at open time)
- May be called concurrently for same fh
- With writeback cache, kernel may combine writes
Raises:
FUSEError(errno.EBADF): Invalid file handle
FUSEError(errno.ENOSPC): No space available
FUSEError(errno.EFBIG): File too large
FUSEError(errno.EIO): I/O error
FUSEError(errno.ENOSYS): Operation not implemented
Lookup Count:
Does not affect lookup count
"""Handle close() syscall.
async def flush(self, fh: FileHandleT) -> None:
"""
Handle close() syscall.
Args:
fh: File handle from open/create
Notes:
- Called whenever a file descriptor is closed
- May be called multiple times for same file (duplicated fds)
- Called for each close(), even if fd was dup()'d
- Called before release() (possibly multiple times)
- Should flush any cached writes to storage
- Essential when enable_writeback_cache=True
- Errors returned to close() syscall
Raises:
FUSEError(errno.EBADF): Invalid file handle
FUSEError(errno.EIO): I/O error
FUSEError(errno.ENOSPC): No space available (for delayed write errors)
FUSEError(errno.ENOSYS): Operation not implemented
Lookup Count:
Does not affect lookup count
"""Release an open file when last fd is closed.
async def release(self, fh: FileHandleT) -> None:
"""
Release open file.
Args:
fh: File handle from open/create
Notes:
- Called when last file descriptor closed
- Called exactly once per open/create
- No future requests for fh until value reused by another open
- May return error but it will be discarded
- Should free resources associated with fh
- Errors not reported to application (too late)
- Not called for files that were never successfully opened
Raises:
Exceptions logged but not reported to application
Lookup Count:
Does not affect lookup count
"""Flush file buffers to storage.
async def fsync(self, fh: FileHandleT, datasync: bool) -> None:
"""
Flush buffers for open file.
Args:
fh: File handle from open/create
datasync: If True, only flush data (not metadata); if False, flush everything
Notes:
- Ensure all buffered writes committed to storage
- datasync=True: flush data only (size, contents), not metadata (times, etc.)
- datasync=False: flush data and metadata
- Essential for data durability guarantees
- May be expensive operation (physical disk sync)
Raises:
FUSEError(errno.EBADF): Invalid file handle
FUSEError(errno.EIO): I/O error
FUSEError(errno.ENOSPC): No space available
FUSEError(errno.ENOSYS): Operation not implemented
Lookup Count:
Does not affect lookup count
"""Create a file and open it atomically.
async def create(self, parent_inode: InodeT, name: FileNameT, mode: ModeT, flags: FlagT, ctx: RequestContext) -> Tuple[FileInfo, EntryAttributes]:
"""
Create a file with permissions and open it.
Args:
parent_inode: Parent directory inode
name: Name for new file (bytes)
mode: File mode (permissions, not including type)
flags: Open flags (O_RDONLY, O_WRONLY, O_RDWR, O_APPEND, etc.)
ctx: Request context with caller information
Returns:
Tuple of (FileInfo with file handle, EntryAttributes)
Notes:
- Successful execution increases lookup count by one
- Atomic create and open (no race condition)
- Apply ctx.umask to mode: final_mode = mode & ~ctx.umask
- File type is always S_IFREG (regular file)
- Set owner to ctx.uid and ctx.gid
- Update parent directory mtime and ctime
- File handle must be unique
- If entry exists, behavior depends on flags (O_EXCL)
Raises:
FUSEError(errno.ENOENT): Parent directory not found
FUSEError(errno.ENOTDIR): parent_inode not a directory
FUSEError(errno.EEXIST): Entry exists and O_EXCL specified
FUSEError(errno.EACCES): Permission denied
FUSEError(errno.ENOSPC): No space available
FUSEError(errno.ENFILE): Too many open files
FUSEError(errno.ENOSYS): Operation not implemented
Lookup Count:
Increases by 1 on success
"""Open a directory and return a handle.
async def opendir(self, inode: InodeT, ctx: RequestContext) -> FileHandleT:
"""
Open directory.
Args:
inode: Directory inode
ctx: Request context with caller information
Returns:
Integer file handle for directory
Notes:
- Handle passed to readdir, fsyncdir, releasedir
- Check read permission on directory
- Handle must be unique (not reused while dir open)
- Can return inode as handle if no per-open state needed
Raises:
FUSEError(errno.ENOENT): Directory not found
FUSEError(errno.ENOTDIR): Not a directory
FUSEError(errno.EACCES): Permission denied
FUSEError(errno.ENFILE): Too many open files
FUSEError(errno.ENOSYS): Operation not implemented
Lookup Count:
Does not affect lookup count
"""List directory contents by calling readdir_reply for each entry.
async def readdir(self, fh: FileHandleT, start_id: int, token: ReaddirToken) -> None:
"""
Read entries in open directory.
Args:
fh: Directory handle from opendir
start_id: Entry ID to start listing from (0 for beginning)
token: Token to pass to readdir_reply
Notes:
- Must call readdir_reply() for each entry
- If readdir_reply returns True, increase lookup count and continue
- If readdir_reply returns False, stop without increasing lookup count
- start_id is 0 or a value from previous next_id parameter
- next_id must be unique and consistent across calls
- Added/removed entries may or may not appear
- Must not skip entries or return duplicates
- Can report . and .. but don't increase lookup counts for them
- If supports_dot_lookup=True, should report . and ..
- Entries can be returned in any order
- start_id allows resuming from previous call (pagination)
Raises:
FUSEError(errno.EBADF): Invalid file handle
FUSEError(errno.ENOTDIR): Not a directory
FUSEError(errno.EIO): I/O error
FUSEError(errno.ENOSYS): Operation not implemented
Lookup Count:
Increases by 1 per entry (except . and ..) via readdir_reply
"""Release an open directory.
async def releasedir(self, fh: FileHandleT) -> None:
"""
Release open directory.
Args:
fh: Directory handle from opendir
Notes:
- Called exactly once per opendir
- No further readdir requests after release
- Should free resources associated with fh
- Errors not reported to application
Raises:
Exceptions logged but not reported to application
Lookup Count:
Does not affect lookup count
"""Flush directory buffers to storage.
async def fsyncdir(self, fh: FileHandleT, datasync: bool) -> None:
"""
Flush buffers for open directory.
Args:
fh: Directory handle from opendir
datasync: If True, only flush contents (not metadata)
Notes:
- Ensure directory entries committed to storage
- datasync=True: flush entries only
- datasync=False: flush entries and metadata
Raises:
FUSEError(errno.EBADF): Invalid file handle
FUSEError(errno.EIO): I/O error
FUSEError(errno.ENOSYS): Operation not implemented
Lookup Count:
Does not affect lookup count
"""Return filesystem statistics.
async def statfs(self, ctx: RequestContext) -> StatvfsData:
"""
Get file system statistics.
Args:
ctx: Request context with caller information
Returns:
StatvfsData with filesystem statistics
Notes:
- Called by df, statvfs, etc.
- Return current statistics (not cached)
- All sizes in f_frsize units
- Set f_namemax to maximum filename length
Raises:
FUSEError(errno.EIO): I/O error
FUSEError(errno.ENOSYS): Operation not implemented
Lookup Count:
Does not affect lookup count
"""Set an extended attribute on an inode.
async def setxattr(self, inode: InodeT, name: XAttrNameT, value: bytes, ctx: RequestContext) -> None:
"""
Set extended attribute.
Args:
inode: Inode number
name: Attribute name (bytes, no zero-bytes, with namespace prefix)
value: Attribute value (bytes)
ctx: Request context with caller information
Notes:
- Attribute may or may not exist already
- name includes namespace prefix (e.g., b'user.comment')
- name is guaranteed not to contain zero-bytes
- value can be any bytes (including zero-bytes)
- Update inode ctime
Raises:
FUSEError(errno.ENOENT): Inode not found
FUSEError(errno.ENOSPC): No space available
FUSEError(errno.EACCES): Permission denied
FUSEError(errno.ENOSYS): Operation not implemented
Lookup Count:
Does not affect lookup count
"""Get an extended attribute from an inode.
async def getxattr(self, inode: InodeT, name: XAttrNameT, ctx: RequestContext) -> bytes:
"""
Get extended attribute.
Args:
inode: Inode number
name: Attribute name (bytes, no zero-bytes, with namespace prefix)
ctx: Request context with caller information
Returns:
Attribute value (bytes)
Notes:
- name includes namespace prefix (e.g., b'user.comment')
- name is guaranteed not to contain zero-bytes
- Raise FUSEError(ENOATTR) if attribute doesn't exist
- pyfuse3.ENOATTR is the correct errno to use
Raises:
FUSEError(errno.ENOENT): Inode not found
FUSEError(pyfuse3.ENOATTR): Attribute not found
FUSEError(errno.EACCES): Permission denied
FUSEError(errno.ENOSYS): Operation not implemented
Lookup Count:
Does not affect lookup count
"""List all extended attributes for an inode.
async def listxattr(self, inode: InodeT, ctx: RequestContext) -> Sequence[XAttrNameT]:
"""
Get list of extended attributes.
Args:
inode: Inode number
ctx: Request context with caller information
Returns:
Sequence of attribute names (bytes, no zero-bytes, with namespace prefixes)
Notes:
- Return full attribute names (e.g., [b'user.comment', b'user.author'])
- Names must not contain zero-bytes
- Return empty sequence if no attributes
- Include namespace prefixes
Raises:
FUSEError(errno.ENOENT): Inode not found
FUSEError(errno.EACCES): Permission denied
FUSEError(errno.ENOSYS): Operation not implemented
Lookup Count:
Does not affect lookup count
"""Remove an extended attribute from an inode.
async def removexattr(self, inode: InodeT, name: XAttrNameT, ctx: RequestContext) -> None:
"""
Remove extended attribute.
Args:
inode: Inode number
name: Attribute name (bytes, no zero-bytes, with namespace prefix)
ctx: Request context with caller information
Notes:
- name includes namespace prefix (e.g., b'user.comment')
- name is guaranteed not to contain zero-bytes
- Raise FUSEError(ENOATTR) if attribute doesn't exist
- Update inode ctime
Raises:
FUSEError(errno.ENOENT): Inode not found
FUSEError(pyfuse3.ENOATTR): Attribute not found
FUSEError(errno.EACCES): Permission denied
FUSEError(errno.ENOSYS): Operation not implemented
Lookup Count:
Does not affect lookup count
"""Check access permissions for an inode.
async def access(self, inode: InodeT, mode: ModeT, ctx: RequestContext) -> bool:
"""
Check if requesting process has mode rights on inode.
Args:
inode: Inode number
mode: Access mode to check (os.R_OK, os.W_OK, os.X_OK, or combination)
ctx: Request context with caller information
Returns:
True if access allowed, False otherwise
Notes:
- Not called if default_permissions mount option given
- Use get_sup_groups() to get supplementary groups
- Check owner, group, and other permissions
- ctx.uid is user ID
- ctx.gid is primary group ID
- get_sup_groups(ctx.pid) returns supplementary groups
- mode is bitwise OR of os.R_OK, os.W_OK, os.X_OK
- Return True if all requested modes granted
Raises:
FUSEError(errno.ENOENT): Inode not found
FUSEError(errno.EIO): I/O error
FUSEError(errno.ENOSYS): Operation not implemented
Lookup Count:
Does not affect lookup count
"""Print stack traces for debugging.
def stacktrace(self) -> None:
"""
Asynchronous debugging.
Called when fuse_stacktrace extended attribute is set on mountpoint.
Default implementation logs stack traces of all Python threads.
Useful for debugging filesystem deadlocks.
Notes:
- This is a synchronous method (not async)
- Triggered by: setfattr -n user.fuse_stacktrace -v 1 /mountpoint
- Override to provide custom debugging information
- Should not raise exceptions
Example:
setfattr -n user.fuse_stacktrace -v 1 /mnt/myfs
"""import pyfuse3
import errno
import stat
import os
import time
class MyFS(pyfuse3.Operations):
def __init__(self):
super().__init__()
self.next_inode = pyfuse3.ROOT_INODE + 1
self.inodes = {}
self.lookup_counts = {}
self.fd_map = {}
self.next_fh = 1
def init(self):
# Initialize root inode
self.inodes[pyfuse3.ROOT_INODE] = {
'mode': stat.S_IFDIR | 0o755,
'nlink': 2,
'uid': os.getuid(),
'gid': os.getgid(),
'size': 0,
'atime': time.time_ns(),
'mtime': time.time_ns(),
'ctime': time.time_ns(),
'entries': {}, # name -> inode
}
self.lookup_counts[pyfuse3.ROOT_INODE] = 1
async def lookup(self, parent_inode, name, ctx):
if parent_inode not in self.inodes:
raise pyfuse3.FUSEError(errno.ENOENT)
parent = self.inodes[parent_inode]
if not stat.S_ISDIR(parent['mode']):
raise pyfuse3.FUSEError(errno.ENOTDIR)
if name in parent['entries']:
inode = parent['entries'][name]
self.lookup_counts[inode] = self.lookup_counts.get(inode, 0) + 1
return await self.getattr(inode, ctx)
raise pyfuse3.FUSEError(errno.ENOENT)
async def getattr(self, inode, ctx):
if inode not in self.inodes:
raise pyfuse3.FUSEError(errno.ENOENT)
data = self.inodes[inode]
entry = pyfuse3.EntryAttributes()
entry.st_ino = inode
entry.st_mode = data['mode']
entry.st_nlink = data['nlink']
entry.st_uid = data['uid']
entry.st_gid = data['gid']
entry.st_size = data['size']
entry.st_atime_ns = data['atime']
entry.st_mtime_ns = data['mtime']
entry.st_ctime_ns = data['ctime']
entry.entry_timeout = 1.0
entry.attr_timeout = 1.0
return entry
async def forget(self, inode_list):
for inode, nlookup in inode_list:
if inode in self.lookup_counts:
self.lookup_counts[inode] -= nlookup
if self.lookup_counts[inode] <= 0:
# Can delete inode if nlink is also 0
del self.lookup_counts[inode]
if inode in self.inodes and self.inodes[inode]['nlink'] == 0:
del self.inodes[inode]
# Implement other operations as needed...class FileHandleFS(pyfuse3.Operations):
def __init__(self):
super().__init__()
self.fh_map = {} # fh -> (inode, flags)
self.next_fh = 1
self.fh_lock = asyncio.Lock()
async def open(self, inode, flags, ctx):
async with self.fh_lock:
fh = self.next_fh
self.next_fh += 1
self.fh_map[fh] = (inode, flags)
return pyfuse3.FileInfo(fh=fh)
async def release(self, fh):
async with self.fh_lock:
if fh in self.fh_map:
del self.fh_map[fh]class LookupCountFS(pyfuse3.Operations):
def __init__(self):
super().__init__()
self.lookup_counts = {}
self.inodes = {}
def _increase_lookup(self, inode):
self.lookup_counts[inode] = self.lookup_counts.get(inode, 0) + 1
def _decrease_lookup(self, inode, nlookup):
if inode in self.lookup_counts:
self.lookup_counts[inode] -= nlookup
if self.lookup_counts[inode] <= 0:
del self.lookup_counts[inode]
# Delete inode if nlink also 0
if self.inodes[inode]['nlink'] == 0:
self._delete_inode(inode)
async def lookup(self, parent_inode, name, ctx):
# ... find inode ...
self._increase_lookup(inode)
return await self.getattr(inode, ctx)
async def forget(self, inode_list):
for inode, nlookup in inode_list:
self._decrease_lookup(inode, nlookup)class PermissionFS(pyfuse3.Operations):
def _check_permission(self, inode_data, mode, ctx):
"""Check if ctx has permission for mode on inode."""
# Owner has access based on owner permissions
if ctx.uid == inode_data['uid']:
owner_perms = (inode_data['mode'] >> 6) & 0o7
return (mode & owner_perms) == mode
# Group access
if ctx.gid == inode_data['gid']:
group_perms = (inode_data['mode'] >> 3) & 0o7
return (mode & group_perms) == mode
# Check supplementary groups
sup_groups = pyfuse3.get_sup_groups(ctx.pid)
if inode_data['gid'] in sup_groups:
group_perms = (inode_data['mode'] >> 3) & 0o7
return (mode & group_perms) == mode
# Others access
other_perms = inode_data['mode'] & 0o7
return (mode & other_perms) == mode
async def open(self, inode, flags, ctx):
data = self.inodes[inode]
# Check read permission
if flags & (os.O_RDONLY | os.O_RDWR):
if not self._check_permission(data, os.R_OK, ctx):
raise pyfuse3.FUSEError(errno.EACCES)
# Check write permission
if flags & (os.O_WRONLY | os.O_RDWR):
if not self._check_permission(data, os.W_OK, ctx):
raise pyfuse3.FUSEError(errno.EACCES)
# ... allocate file handle ...Always raise FUSEError, never other exceptions:
async def lookup(self, parent_inode, name, ctx):
try:
# ... lookup logic ...
pass
except KeyError:
# Convert to FUSEError
raise pyfuse3.FUSEError(errno.ENOENT)
except PermissionError:
raise pyfuse3.FUSEError(errno.EACCES)
except Exception as e:
# Log unexpected errors
logger.error(f"Unexpected error in lookup: {e}", exc_info=True)
# Return generic I/O error
raise pyfuse3.FUSEError(errno.EIO)