Antonio S. Musumeci 947024914a Replace std::filesystem::path with custom fs::relpath
std::filesystem::path is wrong for mergerfs's hot path:
- `branch->path / fusepath_` heap-allocates per FUSE op (3 allocs each
  on libstdc++; the SSO buffer is 15 bytes and any joined path exceeds it).
- The implicit `operator string_type()` adds another std::string when
  passing to `fs::*` wrappers that take `const std::string&`.
- Per-branch loops rebuild fullpath from scratch every iteration.

The new fs::relpath is a fixed 8192-byte (2 * PATH_MAX) inline buffer,
NUL-terminated, never heap-allocates. The rel data is anchored at offset
REL_OFFSET = 4096; the 4096 bytes before the anchor form the "prefix
area" so a long base/prefix can be written there in place via
set_prefix() without disturbing the rel. The remaining ~4096 bytes
after the rel are available for trailing append (filenames in
readdir-style loops, etc.).

set_prefix's cost is O(prefix size) -- only the actually-used bytes
are touched -- so the buffer size is a stack-footprint question, not a
hot-path cost. FUSE worker threads use the pthread default stack
(~8 MB on Linux); a 4-relpath frame in fuse_link/fuse_rename is 32 KB,
~0.4% of the stack budget.

The per-branch hot loop:

  fs::relpath fullpath = fusepath_;       // one copy of the rel
  for (auto branch : branches) {
    fullpath.set_prefix(branch->path);   // writes only [start, 4096)
    fs::syscall(fullpath);
  }

set_prefix only rewrites the prefix area; the rel stays anchored. A
cached _rel_size means no recomputation across iterations.

Microbenchmark (8 branches, 30-char fusepath, -O3, 4 M path builds):

  variant                                 ns/build   allocs/build
  --------------------------------------  --------   ------------
  origin/master (std::filesystem::path)   158        3.00
  fs::relpath assign_concat (single-shot) 9.1        0
  fs::relpath set_prefix (loop-optimized) 4.5        0

35x faster, zero allocations in the hot path.

Contract: paths flow through fs::relpath in canonical form -- prefix
without trailing '/', rel without leading '/'. No sanitization;
violations yield double slashes. Mergerfs satisfies this by
construction:
- libfuse's try_get_path() walks leaf-to-root via prepend(), then
  calls consolidate_to_rel(strip=true) which drops the leading '/'
  that prepend produces.
- branch paths are realpath-canonicalized at config load so they
  never have a trailing slash.

Mergerfs has exactly one path-manipulation type: a relative fusepath.
Branch paths and other absolute paths are stored as std::string -- they
are spliced into the prefix area of an fs::relpath at syscall time but
are never themselves mutated as path objects.

Major changes:
- vendored/libfuse/{include/fs_path.hpp,lib/fs_path.cpp}: new
  fs::relpath class (8 KB inline, REL anchor at 4096, set_prefix /
  clear_prefix / consolidate_to_rel, _rel_size cache, self-aliasing-
  safe assign_concat, sticky ENAMETOOLONG via errnum()).
- 30+ FUSE-op call sites converted from `fullpath = branch / fusepath`
  to the `set_prefix` loop pattern (fuse_unlink, fuse_chmod,
  fuse_chown, fuse_listxattr, fuse_truncate, fuse_utimens,
  fuse_setxattr, fuse_rmdir, fuse_removexattr, fuse_link's
  preserve-path and create-path loops, fuse_rename's create-path
  and preserve-path loops, fuse_statfs FULL mode, fs::findallfiles,
  fs::findonfs).
- fs::* syscall wrappers that previously only took
  `const std::string&` got `const char*` overloads so fs::relpath's
  implicit `operator const char*()` lands cleanly (fs::link,
  fs::lutimens, fs::umount_lazy, fs::open_dir_ro, fs::readlink,
  fs::lremovexattr, fs::lstatvfs, fs::info, fs::statvfs_cache).
- fs::exists gained a (fs::relpath&, basepath, ...) overload that
  calls set_prefix internally so per-branch policy fan-out loops
  share a single fullpath, rewriting only the prefix area on each
  iteration.
- All policies (epall, epff, eplfs, eplus, epmfs, eppfrd, ff, lup,
  msplfs, msplus, mspmfs, msppfrd, newest) refactored to hoist the
  fs::relpath fullpath construction out of the per-branch loop and
  use the set_prefix-based fs::exists overload.
- fs::inode::ReaddirCalc::_fusepath migrated from std::string to
  fs::relpath with the same resize/append API.
- std::filesystem removed entirely from the project: branches.cpp
  and option_parser.cpp's exists/is_directory/equivalent calls now
  use direct ::stat(); mergerfs_fsck.cpp's recursive_directory_iterator
  was replaced with an opendir-based walk.

Tests (tests/tests.cpp): 27 new fs::relpath test cases covering
construction, set_prefix in/out of REL anchor, prefix()/rel()
accessors, clear_prefix, concat canonical form, self-aliasing
(`p /= "x"`), operator/=, resize zero-fill, swap, copy/move,
equality, implicit conversions, lexically_relative, remove_filename,
_rel_size invariant across every mutator, capacity bounds.

Full suite: 213/213 pass.
2026-05-23 21:15:09 -04:00
2026-03-02 01:09:16 -05:00
2025-08-04 23:39:52 -05:00
2025-11-18 08:35:27 -06:00

mergerfs - a featureful union filesystem

mergerfs is a FUSE based union filesystem geared towards simplifying storage and management of files across numerous commodity storage devices. It is similar to mhddfs, unionfs, aufs, DrivePool, etc.

Features

  • Logically combine numerous filesystems/paths into a single mount point (JBOFS: Just a Bunch of FileSystems)
  • Combine paths of the same or different filesystems
  • Ability to add or remove filesystems/paths without impacting the rest of the data
  • Unaffected by individual filesystem failure
  • Configurable file selection and creation placement
  • File IO passthrough for near native IO performance (where supported)
  • Works with filesystems of any size
  • Works with filesystems of almost any type
  • Ignore read-only filesystems when creating files
  • Hard links
  • Hard link copy-on-write / CoW
  • Runtime configurable
  • Support for extended attributes (xattrs)
  • Support for file attributes (chattr)
  • Support for POSIX ACLs

Non-features

  • Read/write overlay on top of read-only filesystem like OverlayFS
  • File whiteout
  • RAID like parity calculation (see SnapRAID)
  • Redundancy
  • Splitting of files across branches

How it works

mergerfs logically merges multiple filesystem paths together. Not block devices, not filesystem mounts, just paths. It acts as a proxy to the underlying filesystem paths. Combining the behaviors of some functions and being a selector for others.

When the contents of a directory are requested mergerfs combines the list of files from each directory, deduplicating entries, and returns that list.

When a file or directory is created a policy is first run to determine which branch will be selected for the creation.

For functions which change attributes or remove the file the behavior may be applied to all instances found.

The way in which mergerfs behaves is controlled by the config/options/settings. More specifically by policies.

Visualization

A         +      B        =       C
/disk1           /disk2           /merged
|                |                |
+-- /dir1        +-- /dir1        +-- /dir1
|   |            |   |            |   |
|   +-- file1    |   |            |   +-- file1
|                |   +-- file2    |   +-- file2
|                |   +-- file3    |   +-- file3
|                |                |
+-- /dir2        |                +-- /dir2
|   |            |                |   |
|   +-- file4    |                |   +-- file4
|                |                |
|                +-- /dir3        +-- /dir3
|                |   |            |   |
|                |   +-- file5    |   +-- file5
|                |                |
+-- file6        |                +-- file6
+-- file7        +-- file7        +-- file7

QuickStart

https://trapexit.github.io/mergerfs/latest/quickstart/

Documentation

https://trapexit.github.io/mergerfs

Support

https://trapexit.github.io/mergerfs/latest/support/

Sponsorship and Donations

https://github.com/trapexit/support

Development and support of a project like mergerfs requires a significant amount of time and effort. The software is released under the very liberal ISC license and is therefore free to use for personal or commercial uses.

If you are a non-commercial user and find mergerfs and its support valuable and would like to support the project financially it would be very much appreciated.

If you are using mergerfs commercially please consider sponsoring the project to ensure it continues to be maintained and receive updates. If custom features are needed feel free to contact me directly.

S
Description
No description provided
Readme ISC
46 MiB
Languages
C++ 87%
C 8.3%
Python 3.9%
Shell 0.4%
Makefile 0.4%