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:
Confirm 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:
FreeBSD 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:
The 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:
To build and test an individual concurrency mode on Linux, use the same feature selection as CI:
Replace 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:
[]
= { = "0.7", = ["parallel"] }
= "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:
use *;
use ;
use ;
use delegate_fs;
use PathBuf;
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 *;
use MirrorFs;
use ;
use delegate_fs;
use PathBuf;
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 *;
use MirrorFs;
use UnimplementedFuseHandler;
use delegate_fs_sync_to_async;
use PathBuf;
Example with MirrorFsAsync
use *;
use ;
use ;
use async_trait;
use PathBuf;
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:
[]
= { = "0.7", = false, = ["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:
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.