Expand description
§easy_fuser
[!IMPORTANT] The API is entering a stabilization phase. Breaking changes are still possible, though they’re expected to become less frequent and more limited in scope. See CHANGELOG.md for API changes.
The crate is maintained on a best-effort basis. The maintainer has limited capacity to provide user support or timely fixes.
§About
easy_fuser is a high-level, ergonomic wrapper around the fuser crate, designed to simplify
the process of implementing FUSE (Filesystem in Userspace) filesystems in Rust. It abstracts away
many of the complexities, offering a more intuitive and Rust-idiomatic approach to filesystem development.
§Key Features
-
Simplified API: Provides a higher-level interface compared to
fuser, reducing boilerplate and making filesystem implementation more straightforward. -
Flexible Concurrency Models: Offers three distinct concurrency models to suit different use cases and performance requirements.
-
Flexible File Identification: Supports both path-based and inode-based operations, allowing you to choose between
Inode,PathBuf, orMappedInodeas your file identifier type. This offers flexibility in how you represent and manage file identities, suitable for different filesystem structures and performance requirements. -
Error Handling: Provides a structured error handling system, facilitating the management of filesystem-specific errors.
-
Composable Presets and Examples: Includes pre-built, composable presets and a comprehensive examples folder to help you get started quickly, understand various implementation patterns, and easily combine different filesystem behaviors. These presets are designed to be mixed and matched via delegation, allowing for flexible and modular filesystem creation.
§File Identification Flexibility
easy_fuser supports three file identifier types:
PathBuf: Work with paths relative to the mount root, without a leading/. A simple choice when you do not need to track hard links.Inode: Work with inode numbers that you assign and manage yourself.MappedInode: Work with inode numbers assigned by easy_fuser, with access to paths for hard links created throughFuseHandler::link.
See the FileIdType documentation
for the trade-offs and edge cases of each choice.
§Object IDs and open resources
TId identifies a filesystem object. FileHandle is the owned resource or state for one specific
open instance. open and create return it; FuseDriver stores it and passes typed optional
access to later file operations. Use () when operations are stateless. If you delegate descriptor
operations to FileDescriptorHandler or MirrorFs, choose std::os::fd::OwnedFd; the helper
implements those callbacks for you, but it still needs the descriptor type. For your own per-open
state, define a type and use Option<&mut Self::FileHandle> in file callbacks. The typed handle docs
show a minimal example. The ZIP example uses a
Cursor<Vec<u8>> to read from the entry loaded by open.
§Platform requirements and setup
easy_fuser uses the host’s FUSE implementation; installing the Rust crate does
not install or enable FUSE in the operating system. Linux is the only platform
on which this crate is well tested. The source also contains platform-specific
implementations for macOS, FreeBSD, OpenBSD, and NetBSD, but treat those as
best-effort. The upstream fuser project
also describes its platform support and setup.
§For end users: running a filesystem
If you are running an already-built filesystem, you do not need Rust or Cargo. You need the host’s FUSE runtime, permission to mount, and an existing mountpoint. Some containers and managed hosts disable FUSE access; installing packages alone cannot enable it.
§Linux
Install the FUSE 3 runtime package using your distribution’s package manager. For Debian or Ubuntu:
sudo apt install fuse3Confirm that /dev/fuse is available and that your user is allowed to mount
FUSE filesystems.
§FreeBSD and other BSD systems
On FreeBSD, install the FUSE library package and load its kernel module:
sudo pkg install fusefs-libs
sudo kldload fusefsFreeBSD supports only one fuser reader thread for now.
The second command loads the module for the current boot. FreeBSD’s Handbook documents the module and how to load it at startup. Ensure your account and system policy allow the mount. The crate has code paths for FreeBSD, OpenBSD, and NetBSD, but FUSE setup and compatibility differ between them. The commands above are for FreeBSD only; OpenBSD and NetBSD have not been well tested with this crate. Consult the relevant system documentation before attempting a mount.
§macOS
Install macFUSE using its current installer and
complete any system approval or security prompts it presents. Ensure your user
is permitted to mount a filesystem. macOS support in easy_fuser is best-effort
and has not been well tested.
§Unmounting
Unmount with the utility provided by the host system. On Linux this is commonly
fusermount3 -u <mountpoint> (some systems provide fusermount); on BSD and
macOS use umount <mountpoint>. If the mount is busy, close processes using it
and retry. After a process crash, check whether the mount is still active before
trying to mount over the same path again.
§For developers: building and testing the crate
Building from source requires a Rust toolchain and Cargo. To build or run tests
that mount a filesystem, you also need the host runtime setup above. On Linux,
the integration tests need access to /dev/fuse and permission to mount; a
container or CI runner without that access cannot run mounted tests.
On Debian or Ubuntu, these packages mirror the Linux CI build dependencies:
sudo apt install pkg-config libfuse-devThe crate’s default Rust backend does not require libfuse development headers.
Install the matching libfuse development package and pkg-config only when
building with the optional libfuse feature. For example, on Debian or Ubuntu:
sudo apt install libfuse3-dev pkg-configTo build and test an individual concurrency mode on Linux, use the same feature selection as CI:
cargo build --no-default-features --features parallel
cargo test --no-default-features --features parallelReplace parallel with serial or async to check those modes. Full tests may
mount FUSE filesystems, so run them on a host where FUSE is enabled.
The macOS CI job installs macFUSE and compiles the serial plus libfuse feature,
but uses cargo test --no-run because its hosted runner cannot mount filesystems.
That verifies compilation, not runtime behavior. There is no equivalent tested
development recipe for OpenBSD or NetBSD; FreeBSD and macOS development should
also be treated as best-effort.
§Usage
The following quickstart mounts an existing directory read-only. It uses the parallel feature;
add these dependencies to your Cargo.toml:
[dependencies]
easy_fuser = { version = "0.7", features = ["parallel"] }
easy_fuser_macro = "0.1"The mount point must already exist and must be outside the source directory.
Run it with a source directory and mount point, for example:
cargo run -- /path/to/source /mnt/myfsuse easy_fuser::fuse_parallel::prelude::*;
use easy_fuser::fuse_presets::mirror_fs::{MirrorFsReadOnly, MirrorFsTrait};
use easy_fuser::fuse_presets::{StatelessHandler, UnimplementedFuseHandler};
use easy_fuser_macro::delegate_fs;
use std::path::PathBuf;
struct ReadOnlyFs {
mirror: MirrorFsReadOnly,
defaults: StatelessHandler<PathBuf>,
unsupported: UnimplementedFuseHandler<PathBuf>,
}
impl FuseHandler for ReadOnlyFs {
type TId = PathBuf;
type FileHandle = std::os::fd::OwnedFd;
delegate_fs! { mirror, [
access, flush, fsync, getattr, getxattr, listxattr, lookup, lseek,
open, read, readdir, readlink, release
] }
delegate_fs! { defaults, [ forget, fsyncdir, opendir, releasedir ] }
delegate_fs! { unsupported, [
bmap, copy_file_range, create, fallocate, getlk, ioctl, link, mkdir,
mknod, removexattr, rename, rmdir, setattr, setlk,
setxattr, statfs, symlink, unlink, write
] }
}
fn main() -> std::io::Result<()> {
let mut args = std::env::args_os().skip(1);
let source = PathBuf::from(args.next().expect("usage: app <SOURCE_DIR> <MOUNT_POINT>"));
let mountpoint = PathBuf::from(args.next().expect("usage: app <SOURCE_DIR> <MOUNT_POINT>"));
let fs = ReadOnlyFs {
mirror: MirrorFsReadOnly::new(source),
defaults: StatelessHandler::new(),
unsupported: UnimplementedFuseHandler::new(),
};
mount(fs, mountpoint, &[MountOption::RO], None)
}This example serves files from the source directory through the mount. For a custom filesystem,
implement the needed FuseHandler operations yourself and delegate only the remaining operations
to presets. See the hello filesystem for a small inode-based
implementation and the passthrough example for a fuller
mirror filesystem.
§Presets / Templates
A preset is a helper that provides implementations for common operations. Presets do not implement your FuseHandler; add them as fields on your own filesystem type and delegate only the operations you want to use. You can implement an operation yourself, or add custom logic before calling a preset from your implementation.
Presets can be composed with delegate_fs!, or with delegate_fs_async! and delegate_fs_sync_to_async! in async handlers.
§Available presets
easy_fuser provides a set of template implementations (presets) under the easy_fuser::fuse_presets module to help you get started quickly:
- UnimplementedFuseHandler: Returns
ENOSYSfor unsupported operations by default; panic mode is available for debugging. - StatelessHandler: Provides simple directory responses when no directory state is needed.
- FileDescriptorHandler: Supplies I/O methods for file handles backed by file descriptors; a read-only variant is also available.
- MirrorFs: Reads and writes through to an existing folder;
MirrorFsReadOnlyomits write methods. - OverlayFs: Combines files from source folders and saves changes in a separate writable folder. For setup, examples, customization, supported operations, and limits, see the detailed OverlayFs guide.
See each type’s documentation for more information about its usage.
§Composing presets
Store presets as fields on your filesystem. Each delegate_fs! list sends the named operations to
that field. Give each operation one owner: delegate it to a preset or implement it yourself. When
you add a custom method, remove it from the delegation list.
use easy_fuser::fuse_parallel::prelude::*;
use easy_fuser::fuse_presets::mirror_fs::MirrorFs;
use easy_fuser::fuse_presets::{StatelessHandler, UnimplementedFuseHandler};
use easy_fuser_macro::delegate_fs;
use std::path::PathBuf;
struct AppFs {
mirror: MirrorFs,
defaults: StatelessHandler<PathBuf>,
unsupported: UnimplementedFuseHandler<PathBuf>,
}
impl FuseHandler for AppFs {
type TId = PathBuf;
type FileHandle = std::os::fd::OwnedFd;
delegate_fs! { mirror, [ lookup, getattr, open, readdir, release ] }
delegate_fs! { defaults, [ forget, fsyncdir, opendir, releasedir ] }
delegate_fs! { unsupported, [ bmap, getlk, ioctl, readdirplus, setlk ] }
fn read(
&self,
req: &RequestInfo,
file_id: PathBuf,
file_handle: Option<&mut std::os::fd::OwnedFd>,
seek: SeekFrom,
size: u32,
flags: OpenFlags,
lock_owner: Option<u64>,
) -> FuseResult<Vec<u8>> {
if file_id == PathBuf::from("secret.txt") {
return Err(ErrorKind::PermissionDenied.to_error("this file is private"));
}
self.mirror
.read(req, file_id, file_handle, seek, size, flags, lock_owner)
}
}§Logging and diagnostics
easy_fuser uses the log facade. Add and initialize a logger in your
application (for example, env_logger) to see its messages. Mount and unmount lifecycle events
are logged at info; failed filesystem handler operations are logged at warn. A lookup for a
missing entry is expected during normal filesystem use and is logged at debug.
Successful filesystem requests are not logged, so normal reads and writes do not incur per-request
logging work. To include lookup misses while debugging, configure your logger to show debug for
easy_fuser, for example with RUST_LOG=easy_fuser=debug when using env_logger.
§Async
Linux filesystem calls used by the presets are generally blocking. The async mode is useful when your handler needs async method signatures or integrates with async-native I/O, but wrapping a blocking call does not make that call non-blocking. If the handler mainly performs blocking filesystem work, async mode offers no performance gain over the parallel mode; choose parallel when you want a worker pool for blocking callbacks.
§Choosing async handlers
Use async-native handlers when operations can await genuinely asynchronous work, such as an async I/O backend. That lets the runtime run other tasks while an operation is waiting. MirrorFsAsync and FileDescriptorHandlerAsync provide async signatures, but their default filesystem operations still call the same blocking unix_fs functions as the synchronous presets. The experimental Linux-only io_uring feature makes some file operations truly asynchronous; current benchmarks show significant performance issues, so it is not recommended. See the feature list below for details.
§Async delegation
When using the async concurrency model, the FuseHandler trait is decorated with #[async_trait]. Because outer attribute macros expand before inner macro invocations, a standard delegation macro like delegate_fs! cannot be desugared by #[async_trait].
easy_fuser provides two specialized async delegation macros that perform manual signature desugaring matching the expected output format of #[async_trait]:
delegate_fs_async!: Use this when delegating to a field/target that itself exposes asynchronous methods (returning Futures).delegate_fs_sync_to_async!: Use this when delegating to a field/target that exposes synchronous/blocking methods. The macro wraps the synchronous method call in a pinned async block, but does not move it to a blocking pool. The call runs when the future is polled and blocks that async runtime worker until it returns. Use this for convenient integration with async handler signatures when blocking work is acceptable; it does not make blocking I/O scalable or improve its performance.
§Example with a synchronous preset
use easy_fuser::fuse_async::prelude::*;
use easy_fuser::fuse_presets::mirror_fs::MirrorFs;
use easy_fuser::fuse_presets::UnimplementedFuseHandler;
use easy_fuser_macro::delegate_fs_sync_to_async;
use std::path::PathBuf;
struct MyAsyncFS {
// MirrorFs has standard synchronous/blocking methods
mirror_fs: MirrorFs,
unimplemented: UnimplementedFuseHandler<PathBuf>,
}
#[async_trait]
impl FuseHandler for MyAsyncFS {
type TId = PathBuf;
type FileHandle = std::os::fd::OwnedFd;
// Delegate to the synchronous MirrorFs target inside an async handler
delegate_fs_sync_to_async! { mirror_fs, [ read, write, getattr ] }
// These operations return ENOSYS until the filesystem supports them.
delegate_fs_sync_to_async! { unimplemented, [ statfs, link ] }
}§Example with MirrorFsAsync
use easy_fuser::fuse_async::prelude::*;
use easy_fuser::fuse_presets::{MirrorFsAsync, UnimplementedFuseHandler};
use easy_fuser_macro::{delegate_fs_async, delegate_fs_sync_to_async};
use async_trait::async_trait;
use std::path::PathBuf;
struct MyAsyncFS {
mirror_fs: MirrorFsAsync,
unimplemented: UnimplementedFuseHandler<PathBuf>,
}
#[async_trait]
impl FuseHandler for MyAsyncFS {
type TId = PathBuf;
delegate_fs_async! { mirror_fs, [ access, getattr, lookup, open, read, readdir, release ] }
delegate_fs_sync_to_async! { unimplemented, [ statfs, link ] }
}§Feature Flags
The default feature set enables serial, parallel, and async. These expose all three APIs; choose the matching FuseHandler prelude in your code. To build only one mode, disable default features and enable the mode you want.
serial: Runs callbacks serially; the thread-count setting is ignored.parallel: Runs callbacks on a worker thread pool. Usemountto configure FUSE reader and handler worker counts independently.async: Runs callbacks on a Tokio runtime. Async handlers useeasy_fuser::fuse_async::prelude::*and#[async_trait].deadlock_detection: Development aid for parallel mode. It checks for deadlocks periodically and logs detected thread backtraces; enable it while debugging, not as a normal production setting.io_uring: Experimental Linux-only option that also enablesasync. It is not recommended: current benchmarks show roughly 3–10× lower throughput than the async syscall implementation. It is disabled by default.libfuse: Buildsfuserwith its libfuse backend instead of the default direct kernel interface; it requires the system libfuse development files.
Example usage in Cargo.toml:
[dependencies]
easy_fuser = { version = "0.7", default-features = false, features = ["parallel"] }By leveraging easy_fuser, you can focus more on your filesystem’s logic and less on the
intricacies of FUSE implementation, making it easier to create robust, efficient, and
maintainable filesystem solutions in Rust.
§Examples
Please check the README inside the examples folder for additional details and references.
§Common Caveats
When working with FUSE filesystems, be aware of the following:
- Crashes & Proper Unmounting: If a program crashes or is stopped abruptly (e.g., using Ctrl+C), it may leave the mountpoint in an inconsistent state.
To properly unmount the filesystem and stop the program (or to resolve a bad state after a crash), use the following command:
fusermount -u <mountpoint>This is the preferred method for both unmounting and resolving any issues with the mountpoint. You will find more information in the documentation of mount and spawn_mount.
- Modifying the source directory while mounted: This is not well-supported behavior and can result in unexpected outcomes.
§Important Notes
libfuse and by extension fuser contains a lot of flags as arguments. We tried to identify them as much as possible, but cannot guarantee it due to the lack of clear documentation on this subject.
Re-exports§
pub use session::FusePruner;pub use session::FuseSession;
Modules§
- fuse_
async - fuse_
parallel - fuse_
presets - Reusable operations for composing a FUSE filesystem.
- fuse_
serial - inode_
mapper Deprecated - Compatibility path for the former mapper module.
- inode_
mapping - Maps FUSE inode numbers to directory entries and resolves handler IDs.
- session
- types
- Types and structures for FUSE filesystem operations.
- unix_fs
- POSIX Filesystem Operations Module compatible with FUSE signatures.
Macros§
- delegate_
fs - Synchronously delegates FUSE handler trait methods to a target field.
- delegate_
fs_ async - Asynchronously delegates FUSE handler trait methods to an asynchronous target field.
- delegate_
fs_ sync_ to_ async - Asynchronously delegates FUSE handler trait methods to a synchronous target field.