Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
c21c42b
mount2/umount2: mfusepy-based alternative FUSE fs implementation
ThomasWaldmann Nov 23, 2025
2d1772f
fuse2: versions view + test
ThomasWaldmann Nov 24, 2025
f545ebb
fuse2: optimize/generalize debug and test logging
ThomasWaldmann Nov 24, 2025
6632b11
fuse2: remove unused fuse_main
ThomasWaldmann Nov 24, 2025
9651b08
Optimize fuse2 memory usage with centralized packed item storage
ThomasWaldmann Nov 24, 2025
72befa1
fuse2: rename Node -> DirEntry, inode_count -> current_ino
ThomasWaldmann Nov 24, 2025
a842234
fuse2: add __slots__ to DirEntry for memory optimization
ThomasWaldmann Nov 24, 2025
8eb676b
fuse2: remove path from packed inodes to save memory
ThomasWaldmann Nov 24, 2025
3393c0f
fuse2: implement lazy children dict allocation for DirEntry
ThomasWaldmann Nov 24, 2025
0aa7c16
fuse2: improve comments
ThomasWaldmann Nov 25, 2025
71416d7
fuse2: getattr: prefer fh lookup if possible
ThomasWaldmann Nov 25, 2025
ead93b6
integrate mount2/umount2 into mount/umount, use BORG_FUSE_IMPL
ThomasWaldmann Nov 25, 2025
176dec8
integrate mount2_cmds_test into mount_cmds_test
ThomasWaldmann Nov 25, 2025
43c7878
docs: update installation requirements and BORG_FUSE_IMPL about mfusepy
ThomasWaldmann Dec 5, 2025
3e676e9
pytest report header: report llfuse/pyfuse3/mfusepy
ThomasWaldmann Dec 5, 2025
7590d1e
add has_any_fuse flag
ThomasWaldmann Dec 5, 2025
562bb27
fix hardlink inode issue
ThomasWaldmann Dec 5, 2025
2e567d9
fuse_impl.ENOATTR (import from borg.platform)
ThomasWaldmann Dec 6, 2025
cc18e3f
rename fuse2 -> hlfuse
ThomasWaldmann Dec 6, 2025
334f73d
use hlfuse similar to llfuse, move import
ThomasWaldmann Dec 6, 2025
7ccbb33
rename tox envs after fuse lib name
ThomasWaldmann Dec 6, 2025
0053bcb
mfusepy: add alternative extra to install from project's master branch
ThomasWaldmann Dec 6, 2025
23bbc19
pyproject.toml: add comments to fuse options
ThomasWaldmann Dec 23, 2025
2e9ebe4
docs: add missing link definition for mfusepy
ThomasWaldmann Dec 6, 2025
c9ebeba
make mypy happy
ThomasWaldmann Dec 6, 2025
7278a1d
ENOATTR import cleanups
ThomasWaldmann Dec 20, 2025
ee2fcef
hlfuse: fix getxattr, raise ENOATTR
ThomasWaldmann Dec 20, 2025
661d2b6
docs: mfusepy >= 3.1.0 is required
ThomasWaldmann Dec 23, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 16 additions & 14 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -136,19 +136,19 @@ jobs:
"include": [
{"os": "ubuntu-22.04", "python-version": "3.10", "toxenv": "mypy"},
{"os": "ubuntu-22.04", "python-version": "3.11", "toxenv": "docs"},
{"os": "ubuntu-22.04", "python-version": "3.10", "toxenv": "py310-fuse2"},
{"os": "ubuntu-24.04", "python-version": "3.14", "toxenv": "py314-fuse3"}
{"os": "ubuntu-22.04", "python-version": "3.10", "toxenv": "py310-llfuse"},
{"os": "ubuntu-24.04", "python-version": "3.14", "toxenv": "py314-mfusepy"}
]
}' || '{
"include": [
{"os": "ubuntu-22.04", "python-version": "3.10", "toxenv": "mypy"},
{"os": "ubuntu-22.04", "python-version": "3.11", "toxenv": "docs"},
{"os": "ubuntu-22.04", "python-version": "3.10", "toxenv": "py310-fuse2"},
{"os": "ubuntu-22.04", "python-version": "3.11", "toxenv": "py311-fuse2", "binary": "borg-linux-glibc235-x86_64-gh"},
{"os": "ubuntu-22.04-arm", "python-version": "3.11", "toxenv": "py311-fuse2", "binary": "borg-linux-glibc235-arm64-gh"},
{"os": "ubuntu-24.04", "python-version": "3.12", "toxenv": "py312-fuse3"},
{"os": "ubuntu-24.04", "python-version": "3.13", "toxenv": "py313-fuse3"},
{"os": "ubuntu-24.04", "python-version": "3.14", "toxenv": "py314-fuse3"},
{"os": "ubuntu-22.04", "python-version": "3.10", "toxenv": "py310-llfuse"},
{"os": "ubuntu-22.04", "python-version": "3.11", "toxenv": "py311-llfuse", "binary": "borg-linux-glibc235-x86_64-gh"},
{"os": "ubuntu-22.04-arm", "python-version": "3.11", "toxenv": "py311-llfuse", "binary": "borg-linux-glibc235-arm64-gh"},
{"os": "ubuntu-24.04", "python-version": "3.12", "toxenv": "py312-pyfuse3"},
{"os": "ubuntu-24.04", "python-version": "3.13", "toxenv": "py313-pyfuse3"},
{"os": "ubuntu-24.04", "python-version": "3.14", "toxenv": "py314-mfusepy"},
{"os": "macos-15-intel", "python-version": "3.11", "toxenv": "py311-none", "binary": "borg-macos-15-x86_64-gh"},
{"os": "macos-15", "python-version": "3.11", "toxenv": "py311-none", "binary": "borg-macos-15-arm64-gh"}
]
Expand Down Expand Up @@ -190,9 +190,9 @@ jobs:
sudo apt-get install -y libssl-dev libacl1-dev libxxhash-dev liblz4-dev libzstd-dev
sudo apt-get install -y bash zsh fish # for shell completion tests
sudo apt-get install -y rclone openssh-server curl
if [[ "$TOXENV" == *"fuse2"* ]]; then
if [[ "$TOXENV" == *"llfuse"* ]]; then
sudo apt-get install -y libfuse-dev fuse # Required for Python llfuse module
elif [[ "$TOXENV" == *"fuse3"* ]]; then
elif [[ "$TOXENV" == *"pyfuse3"* || "$TOXENV" == *"mfusepy"* ]]; then
sudo apt-get install -y libfuse3-dev fuse3 # Required for Python pyfuse3 module
fi

Expand Down Expand Up @@ -266,10 +266,12 @@ jobs:

- name: Install borgbackup
run: |
if [[ "$TOXENV" == *"fuse2"* ]]; then
if [[ "$TOXENV" == *"llfuse"* ]]; then
pip install -ve ".[llfuse]"
elif [[ "$TOXENV" == *"fuse3"* ]]; then
elif [[ "$TOXENV" == *"pyfuse3"* ]]; then
pip install -ve ".[pyfuse3]"
elif [[ "$TOXENV" == *"mfusepy"* ]]; then
pip install -ve ".[mfusepy]"
else
pip install -ve .
fi
Expand Down Expand Up @@ -423,8 +425,8 @@ jobs:
pip -V
python -m pip install --upgrade pip wheel
pip install -r requirements.d/development.txt
pip install -e ".[llfuse]"
tox -e py311-fuse2
pip install -e ".[mfusepy]"
tox -e py311-mfusepy

if [[ "${{ matrix.do_binaries }}" == "true" && "${{ startsWith(github.ref, 'refs/tags/') }}" == "true" ]]; then
python -m pip install 'pyinstaller==6.14.2'
Expand Down
6 changes: 3 additions & 3 deletions Vagrantfile
Original file line number Diff line number Diff line change
Expand Up @@ -373,7 +373,7 @@ Vagrant.configure(2) do |config|
b.vm.provision "install borg", :type => :shell, :privileged => false, :inline => install_borg("llfuse")
b.vm.provision "install pyinstaller", :type => :shell, :privileged => false, :inline => install_pyinstaller()
b.vm.provision "build binary with pyinstaller", :type => :shell, :privileged => false, :inline => build_binary_with_pyinstaller("freebsd13")
b.vm.provision "run tests", :type => :shell, :privileged => false, :inline => run_tests("freebsd13", ".*(fuse3|none).*")
b.vm.provision "run tests", :type => :shell, :privileged => false, :inline => run_tests("freebsd13", ".*(pyfuse3|none).*")
end

config.vm.define "freebsd14" do |b|
Expand All @@ -390,7 +390,7 @@ Vagrant.configure(2) do |config|
b.vm.provision "install borg", :type => :shell, :privileged => false, :inline => install_borg("llfuse")
b.vm.provision "install pyinstaller", :type => :shell, :privileged => false, :inline => install_pyinstaller()
b.vm.provision "build binary with pyinstaller", :type => :shell, :privileged => false, :inline => build_binary_with_pyinstaller("freebsd14")
b.vm.provision "run tests", :type => :shell, :privileged => false, :inline => run_tests("freebsd14", ".*(fuse3|none).*")
b.vm.provision "run tests", :type => :shell, :privileged => false, :inline => run_tests("freebsd14", ".*(pyfuse3|none).*")
end

config.vm.define "openbsd7" do |b|
Expand All @@ -413,7 +413,7 @@ Vagrant.configure(2) do |config|
b.vm.provision "fs init", :type => :shell, :inline => fs_init("vagrant")
b.vm.provision "packages netbsd", :type => :shell, :inline => packages_netbsd
b.vm.provision "build env", :type => :shell, :privileged => false, :inline => build_sys_venv("netbsd9")
b.vm.provision "install borg", :type => :shell, :privileged => false, :inline => install_borg(false)
b.vm.provision "install borg", :type => :shell, :privileged => false, :inline => install_borg("nofuse")
b.vm.provision "run tests", :type => :shell, :privileged => false, :inline => run_tests("netbsd9", ".*fuse.*")
end

Expand Down
1 change: 1 addition & 0 deletions docs/global.rst.inc
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
.. _msgpack: https://msgpack.org/
.. _`msgpack-python`: https://pypi.python.org/pypi/msgpack-python/
.. _llfuse: https://pypi.python.org/pypi/llfuse/
.. _mfusepy: https://pypi.python.org/pypi/mfusepy/
.. _pyfuse3: https://pypi.python.org/pypi/pyfuse3/
.. _userspace filesystems: https://en.wikipedia.org/wiki/Filesystem_in_Userspace
.. _Cython: http://cython.org/
Expand Down
1 change: 1 addition & 0 deletions docs/installation.rst
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,7 @@ development header files (sometimes in a separate `-dev` or `-devel` package).
* Optionally, if you wish to mount an archive as a FUSE filesystem, you need
a FUSE implementation for Python:

- mfusepy_ >= 3.1.0 (for fuse 2 and fuse 3, use `pip install borgbackup[mfusepy]`), or
- pyfuse3_ >= 3.1.1 (for fuse 3, use `pip install borgbackup[pyfuse3]`), or
- llfuse_ >= 1.3.8 (for fuse 2, use `pip install borgbackup[llfuse]`).
- Additionally, your OS will need to have FUSE support installed
Expand Down
3 changes: 2 additions & 1 deletion docs/usage/general/environment.rst.inc
Original file line number Diff line number Diff line change
Expand Up @@ -88,8 +88,9 @@ General:
This is a comma-separated list of implementation names, they are tried in the
given order, e.g.:

- ``pyfuse3,llfuse``: default, first try to load pyfuse3, then try to load llfuse.
- ``mfusepy,pyfuse3,llfuse``: default, first try to load mfusepy, then pyfuse3, then llfuse.
- ``llfuse,pyfuse3``: first try to load llfuse, then try to load pyfuse3.
- ``mfusepy``: only try to load mfusepy
- ``pyfuse3``: only try to load pyfuse3
- ``llfuse``: only try to load llfuse
- ``none``: do not try to load an implementation
Expand Down
48 changes: 35 additions & 13 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,10 @@ dependencies = [
]

[project.optional-dependencies]
llfuse = ["llfuse >= 1.3.8"]
pyfuse3 = ["pyfuse3 >= 3.1.1"]
llfuse = ["llfuse >= 1.3.8"] # fuse 2, low-level
pyfuse3 = ["pyfuse3 >= 3.1.1"] # fuse 3, low-level, async
mfusepy = ["mfusepy >= 3.1.0, <4.0.0"] # fuse 2+3, high-level
mfusepym = ["mfusepy @ git+https://github.com/mxmlnkn/mfusepy.git@master"]
nofuse = []
s3 = ["borgstore[s3] ~= 0.3.0"]
sftp = ["borgstore[sftp] ~= 0.3.0"]
Expand Down Expand Up @@ -166,7 +168,7 @@ ignore_missing_imports = true
requires = ["tox>=4.19", "pkgconfig", "cython", "wheel", "setuptools_scm"]
# Important: when adding/removing Python versions here,
# also update the section "Test environments with different FUSE implementations" accordingly.
env_list = ["py{310,311,312,313,314}-{none,fuse2,fuse3}", "docs", "ruff", "mypy", "bandit"]
env_list = ["py{310,311,312,313,314}-{none,llfuse,pyfuse3,mfusepy}", "docs", "ruff", "mypy", "bandit"]

[tool.tox.env_run_base]
package = "editable-legacy" # without this it does not find setup_docs when running under fakeroot
Expand All @@ -180,54 +182,74 @@ pass_env = ["*"] # needed by tox4, so env vars are visible for building borg
# Test environments with different FUSE implementations
[tool.tox.env.py310-none]

[tool.tox.env.py310-fuse2]
[tool.tox.env.py310-llfuse]
set_env = {BORG_FUSE_IMPL = "llfuse"}
extras = ["llfuse", "sftp", "s3"]

[tool.tox.env.py310-fuse3]
[tool.tox.env.py310-pyfuse3]
set_env = {BORG_FUSE_IMPL = "pyfuse3"}
extras = ["pyfuse3", "sftp", "s3"]

[tool.tox.env.py310-mfusepy]
set_env = {BORG_FUSE_IMPL = "mfusepy"}
extras = ["mfusepy", "sftp", "s3"]

[tool.tox.env.py311-none]

[tool.tox.env.py311-fuse2]
[tool.tox.env.py311-llfuse]
set_env = {BORG_FUSE_IMPL = "llfuse"}
extras = ["llfuse", "sftp", "s3"]

[tool.tox.env.py311-fuse3]
[tool.tox.env.py311-pyfuse3]
set_env = {BORG_FUSE_IMPL = "pyfuse3"}
extras = ["pyfuse3", "sftp", "s3"]

[tool.tox.env.py311-mfusepy]
set_env = {BORG_FUSE_IMPL = "mfusepy"}
extras = ["mfusepy", "sftp", "s3"]

[tool.tox.env.py312-none]

[tool.tox.env.py312-fuse2]
[tool.tox.env.py312-llfuse]
set_env = {BORG_FUSE_IMPL = "llfuse"}
extras = ["llfuse", "sftp", "s3"]

[tool.tox.env.py312-fuse3]
[tool.tox.env.py312-pyfuse3]
set_env = {BORG_FUSE_IMPL = "pyfuse3"}
extras = ["pyfuse3", "sftp", "s3"]

[tool.tox.env.py312-mfusepy]
set_env = {BORG_FUSE_IMPL = "mfusepy"}
extras = ["mfusepy", "sftp", "s3"]

[tool.tox.env.py313-none]

[tool.tox.env.py313-fuse2]
[tool.tox.env.py313-llfuse]
set_env = {BORG_FUSE_IMPL = "llfuse"}
extras = ["llfuse", "sftp", "s3"]

[tool.tox.env.py313-fuse3]
[tool.tox.env.py313-pyfuse3]
set_env = {BORG_FUSE_IMPL = "pyfuse3"}
extras = ["pyfuse3", "sftp", "s3"]

[tool.tox.env.py313-mfusepy]
set_env = {BORG_FUSE_IMPL = "mfusepy"}
extras = ["mfusepy", "sftp", "s3"]

[tool.tox.env.py314-none]

[tool.tox.env.py314-fuse2]
[tool.tox.env.py314-llfuse]
set_env = {BORG_FUSE_IMPL = "llfuse"}
extras = ["llfuse", "sftp", "s3"]

[tool.tox.env.py314-fuse3]
[tool.tox.env.py314-pyfuse3]
set_env = {BORG_FUSE_IMPL = "pyfuse3"}
extras = ["pyfuse3", "sftp", "s3"]

[tool.tox.env.py314-mfusepy]
set_env = {BORG_FUSE_IMPL = "mfusepy"}
extras = ["mfusepy", "sftp", "s3"]

[tool.tox.env.ruff]
skip_install = true
deps = ["ruff"]
Expand Down
25 changes: 20 additions & 5 deletions src/borg/archiver/mount_cmds.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@ def do_mount(self, args):
"""Mounts an archive or an entire repository as a FUSE filesystem."""
# Perform these checks before opening the repository and asking for a passphrase.

from ..fuse_impl import llfuse, BORG_FUSE_IMPL
from ..fuse_impl import llfuse, has_mfusepy, BORG_FUSE_IMPL

if llfuse is None:
if llfuse is None and not has_mfusepy:
raise RTError("borg mount not available: no FUSE support, BORG_FUSE_IMPL=%s." % BORG_FUSE_IMPL)

if not os.path.isdir(args.mountpoint):
Expand All @@ -34,16 +34,31 @@ def do_mount(self, args):

@with_repository(compatibility=(Manifest.Operation.READ,))
def _do_mount(self, args, repository, manifest):
from ..fuse import FuseOperations
from ..fuse_impl import has_mfusepy

with cache_if_remote(repository, decrypted_cache=manifest.repo_objs) as cached_repo:
operations = FuseOperations(manifest, args, cached_repo)
if has_mfusepy:
# Use mfusepy implementation
from ..hlfuse import borgfs

operations = borgfs(manifest, args, repository)
logger.info("Mounting filesystem")
try:
operations.mount(args.mountpoint, args.options, args.foreground, args.show_rc)
except RuntimeError:
# Relevant error message already printed to stderr by FUSE
raise RTError("FUSE mount failed")
else:
# Use llfuse/pyfuse3 implementation
from ..fuse import FuseOperations

with cache_if_remote(repository, decrypted_cache=manifest.repo_objs) as cached_repo:
operations = FuseOperations(manifest, args, cached_repo)
logger.info("Mounting filesystem")
try:
operations.mount(args.mountpoint, args.options, args.foreground, args.show_rc)
except RuntimeError:
# Relevant error message already printed to stderr by FUSE
raise RTError("FUSE mount failed")

def do_umount(self, args):
"""Unmounts the FUSE filesystem."""
Expand Down
7 changes: 4 additions & 3 deletions src/borg/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
setup_logging()

from borg.archiver import Archiver # noqa: E402
from borg.testsuite import has_lchflags, has_llfuse, has_pyfuse3 # noqa: E402
from borg.testsuite import has_lchflags, has_llfuse, has_pyfuse3, has_mfusepy # noqa: E402
from borg.testsuite import are_symlinks_supported, are_hardlinks_supported, is_utime_fully_supported # noqa: E402
from borg.testsuite.archiver import BORG_EXES
from borg.testsuite.platform.platform_test import fakeroot_detected # noqa: E402
Expand All @@ -37,8 +37,9 @@ def clean_env(tmpdir_factory, monkeypatch):
def pytest_report_header(config, start_path):
tests = {
"BSD flags": has_lchflags,
"fuse2": has_llfuse,
"fuse3": has_pyfuse3,
"llfuse": has_llfuse,
"pyfuse3": has_pyfuse3,
"mfusepy": has_mfusepy,
"root": not fakeroot_detected(),
"symlinks": are_symlinks_supported(),
"hardlinks": are_hardlinks_supported(),
Expand Down
11 changes: 9 additions & 2 deletions src/borg/fuse.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,17 @@
import time
from collections import defaultdict, Counter
from signal import SIGINT
from typing import TYPE_CHECKING

from .constants import ROBJ_FILE_STREAM, zeros
from .fuse_impl import llfuse, has_pyfuse3
from .platform import ENOATTR

if TYPE_CHECKING:
# For type checking, assume llfuse is available
# This allows mypy to understand llfuse.Operations
import llfuse
from .fuse_impl import has_pyfuse3, ENOATTR
else:
from .fuse_impl import llfuse, has_pyfuse3, ENOATTR

if has_pyfuse3:
import trio
Expand Down
37 changes: 32 additions & 5 deletions src/borg/fuse_impl.py
Original file line number Diff line number Diff line change
@@ -1,36 +1,63 @@
"""
Loads the library for the low-level FUSE implementation.
Loads the library for the FUSE implementation.
"""

import os
import types

BORG_FUSE_IMPL = os.environ.get("BORG_FUSE_IMPL", "pyfuse3,llfuse")
from .platform import ENOATTR # noqa

BORG_FUSE_IMPL = os.environ.get("BORG_FUSE_IMPL", "mfusepy,pyfuse3,llfuse")

hlfuse: types.ModuleType | None = None
llfuse: types.ModuleType | None = None

for FUSE_IMPL in BORG_FUSE_IMPL.split(","):
FUSE_IMPL = FUSE_IMPL.strip()
if FUSE_IMPL == "pyfuse3":
try:
import pyfuse3 as llfuse
import pyfuse3
except ImportError:
pass
else:
llfuse = pyfuse3
has_llfuse = False
has_pyfuse3 = True
has_mfusepy = False
has_any_fuse = True
hlfuse = None # noqa
break
elif FUSE_IMPL == "llfuse":
try:
import llfuse
import llfuse as llfuse_module
except ImportError:
pass
else:
llfuse = llfuse_module
has_llfuse = True
has_pyfuse3 = False
has_mfusepy = False
has_any_fuse = True
hlfuse = None # noqa
break
elif FUSE_IMPL == "mfusepy":
try:
import mfusepy
except ImportError:
pass
else:
hlfuse = mfusepy
has_llfuse = False
has_pyfuse3 = False
has_mfusepy = True
has_any_fuse = True
break
elif FUSE_IMPL == "none":
pass
else:
raise RuntimeError("Unknown FUSE implementation in BORG_FUSE_IMPL: '%s'." % BORG_FUSE_IMPL)
else:
llfuse = None # noqa
has_llfuse = False
has_pyfuse3 = False
has_mfusepy = False
has_any_fuse = False
Loading
Loading