Skip to content

Commit 4a1d0eb

Browse files
Merge pull request #9213 from ThomasWaldmann/mfusepy
integrate highlevel fuse lib mfusepy
2 parents a0e250d + 661d2b6 commit 4a1d0eb

File tree

14 files changed

+836
-57
lines changed

14 files changed

+836
-57
lines changed

.github/workflows/ci.yml

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -136,19 +136,19 @@ jobs:
136136
"include": [
137137
{"os": "ubuntu-22.04", "python-version": "3.10", "toxenv": "mypy"},
138138
{"os": "ubuntu-22.04", "python-version": "3.11", "toxenv": "docs"},
139-
{"os": "ubuntu-22.04", "python-version": "3.10", "toxenv": "py310-fuse2"},
140-
{"os": "ubuntu-24.04", "python-version": "3.14", "toxenv": "py314-fuse3"}
139+
{"os": "ubuntu-22.04", "python-version": "3.10", "toxenv": "py310-llfuse"},
140+
{"os": "ubuntu-24.04", "python-version": "3.14", "toxenv": "py314-mfusepy"}
141141
]
142142
}' || '{
143143
"include": [
144144
{"os": "ubuntu-22.04", "python-version": "3.10", "toxenv": "mypy"},
145145
{"os": "ubuntu-22.04", "python-version": "3.11", "toxenv": "docs"},
146-
{"os": "ubuntu-22.04", "python-version": "3.10", "toxenv": "py310-fuse2"},
147-
{"os": "ubuntu-22.04", "python-version": "3.11", "toxenv": "py311-fuse2", "binary": "borg-linux-glibc235-x86_64-gh"},
148-
{"os": "ubuntu-22.04-arm", "python-version": "3.11", "toxenv": "py311-fuse2", "binary": "borg-linux-glibc235-arm64-gh"},
149-
{"os": "ubuntu-24.04", "python-version": "3.12", "toxenv": "py312-fuse3"},
150-
{"os": "ubuntu-24.04", "python-version": "3.13", "toxenv": "py313-fuse3"},
151-
{"os": "ubuntu-24.04", "python-version": "3.14", "toxenv": "py314-fuse3"},
146+
{"os": "ubuntu-22.04", "python-version": "3.10", "toxenv": "py310-llfuse"},
147+
{"os": "ubuntu-22.04", "python-version": "3.11", "toxenv": "py311-llfuse", "binary": "borg-linux-glibc235-x86_64-gh"},
148+
{"os": "ubuntu-22.04-arm", "python-version": "3.11", "toxenv": "py311-llfuse", "binary": "borg-linux-glibc235-arm64-gh"},
149+
{"os": "ubuntu-24.04", "python-version": "3.12", "toxenv": "py312-pyfuse3"},
150+
{"os": "ubuntu-24.04", "python-version": "3.13", "toxenv": "py313-pyfuse3"},
151+
{"os": "ubuntu-24.04", "python-version": "3.14", "toxenv": "py314-mfusepy"},
152152
{"os": "macos-15-intel", "python-version": "3.11", "toxenv": "py311-none", "binary": "borg-macos-15-x86_64-gh"},
153153
{"os": "macos-15", "python-version": "3.11", "toxenv": "py311-none", "binary": "borg-macos-15-arm64-gh"}
154154
]
@@ -190,9 +190,9 @@ jobs:
190190
sudo apt-get install -y libssl-dev libacl1-dev libxxhash-dev liblz4-dev libzstd-dev
191191
sudo apt-get install -y bash zsh fish # for shell completion tests
192192
sudo apt-get install -y rclone openssh-server curl
193-
if [[ "$TOXENV" == *"fuse2"* ]]; then
193+
if [[ "$TOXENV" == *"llfuse"* ]]; then
194194
sudo apt-get install -y libfuse-dev fuse # Required for Python llfuse module
195-
elif [[ "$TOXENV" == *"fuse3"* ]]; then
195+
elif [[ "$TOXENV" == *"pyfuse3"* || "$TOXENV" == *"mfusepy"* ]]; then
196196
sudo apt-get install -y libfuse3-dev fuse3 # Required for Python pyfuse3 module
197197
fi
198198
@@ -266,10 +266,12 @@ jobs:
266266
267267
- name: Install borgbackup
268268
run: |
269-
if [[ "$TOXENV" == *"fuse2"* ]]; then
269+
if [[ "$TOXENV" == *"llfuse"* ]]; then
270270
pip install -ve ".[llfuse]"
271-
elif [[ "$TOXENV" == *"fuse3"* ]]; then
271+
elif [[ "$TOXENV" == *"pyfuse3"* ]]; then
272272
pip install -ve ".[pyfuse3]"
273+
elif [[ "$TOXENV" == *"mfusepy"* ]]; then
274+
pip install -ve ".[mfusepy]"
273275
else
274276
pip install -ve .
275277
fi
@@ -423,8 +425,8 @@ jobs:
423425
pip -V
424426
python -m pip install --upgrade pip wheel
425427
pip install -r requirements.d/development.txt
426-
pip install -e ".[llfuse]"
427-
tox -e py311-fuse2
428+
pip install -e ".[mfusepy]"
429+
tox -e py311-mfusepy
428430
429431
if [[ "${{ matrix.do_binaries }}" == "true" && "${{ startsWith(github.ref, 'refs/tags/') }}" == "true" ]]; then
430432
python -m pip install 'pyinstaller==6.14.2'

Vagrantfile

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -373,7 +373,7 @@ Vagrant.configure(2) do |config|
373373
b.vm.provision "install borg", :type => :shell, :privileged => false, :inline => install_borg("llfuse")
374374
b.vm.provision "install pyinstaller", :type => :shell, :privileged => false, :inline => install_pyinstaller()
375375
b.vm.provision "build binary with pyinstaller", :type => :shell, :privileged => false, :inline => build_binary_with_pyinstaller("freebsd13")
376-
b.vm.provision "run tests", :type => :shell, :privileged => false, :inline => run_tests("freebsd13", ".*(fuse3|none).*")
376+
b.vm.provision "run tests", :type => :shell, :privileged => false, :inline => run_tests("freebsd13", ".*(pyfuse3|none).*")
377377
end
378378

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

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

docs/global.rst.inc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
.. _msgpack: https://msgpack.org/
2424
.. _`msgpack-python`: https://pypi.python.org/pypi/msgpack-python/
2525
.. _llfuse: https://pypi.python.org/pypi/llfuse/
26+
.. _mfusepy: https://pypi.python.org/pypi/mfusepy/
2627
.. _pyfuse3: https://pypi.python.org/pypi/pyfuse3/
2728
.. _userspace filesystems: https://en.wikipedia.org/wiki/Filesystem_in_Userspace
2829
.. _Cython: http://cython.org/

docs/installation.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,7 @@ development header files (sometimes in a separate `-dev` or `-devel` package).
175175
* Optionally, if you wish to mount an archive as a FUSE filesystem, you need
176176
a FUSE implementation for Python:
177177

178+
- mfusepy_ >= 3.1.0 (for fuse 2 and fuse 3, use `pip install borgbackup[mfusepy]`), or
178179
- pyfuse3_ >= 3.1.1 (for fuse 3, use `pip install borgbackup[pyfuse3]`), or
179180
- llfuse_ >= 1.3.8 (for fuse 2, use `pip install borgbackup[llfuse]`).
180181
- Additionally, your OS will need to have FUSE support installed

docs/usage/general/environment.rst.inc

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,8 +88,9 @@ General:
8888
This is a comma-separated list of implementation names, they are tried in the
8989
given order, e.g.:
9090
91-
- ``pyfuse3,llfuse``: default, first try to load pyfuse3, then try to load llfuse.
91+
- ``mfusepy,pyfuse3,llfuse``: default, first try to load mfusepy, then pyfuse3, then llfuse.
9292
- ``llfuse,pyfuse3``: first try to load llfuse, then try to load pyfuse3.
93+
- ``mfusepy``: only try to load mfusepy
9394
- ``pyfuse3``: only try to load pyfuse3
9495
- ``llfuse``: only try to load llfuse
9596
- ``none``: do not try to load an implementation

pyproject.toml

Lines changed: 35 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,10 @@ dependencies = [
4141
]
4242

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

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

183-
[tool.tox.env.py310-fuse2]
185+
[tool.tox.env.py310-llfuse]
184186
set_env = {BORG_FUSE_IMPL = "llfuse"}
185187
extras = ["llfuse", "sftp", "s3"]
186188

187-
[tool.tox.env.py310-fuse3]
189+
[tool.tox.env.py310-pyfuse3]
188190
set_env = {BORG_FUSE_IMPL = "pyfuse3"}
189191
extras = ["pyfuse3", "sftp", "s3"]
190192

193+
[tool.tox.env.py310-mfusepy]
194+
set_env = {BORG_FUSE_IMPL = "mfusepy"}
195+
extras = ["mfusepy", "sftp", "s3"]
196+
191197
[tool.tox.env.py311-none]
192198

193-
[tool.tox.env.py311-fuse2]
199+
[tool.tox.env.py311-llfuse]
194200
set_env = {BORG_FUSE_IMPL = "llfuse"}
195201
extras = ["llfuse", "sftp", "s3"]
196202

197-
[tool.tox.env.py311-fuse3]
203+
[tool.tox.env.py311-pyfuse3]
198204
set_env = {BORG_FUSE_IMPL = "pyfuse3"}
199205
extras = ["pyfuse3", "sftp", "s3"]
200206

207+
[tool.tox.env.py311-mfusepy]
208+
set_env = {BORG_FUSE_IMPL = "mfusepy"}
209+
extras = ["mfusepy", "sftp", "s3"]
210+
201211
[tool.tox.env.py312-none]
202212

203-
[tool.tox.env.py312-fuse2]
213+
[tool.tox.env.py312-llfuse]
204214
set_env = {BORG_FUSE_IMPL = "llfuse"}
205215
extras = ["llfuse", "sftp", "s3"]
206216

207-
[tool.tox.env.py312-fuse3]
217+
[tool.tox.env.py312-pyfuse3]
208218
set_env = {BORG_FUSE_IMPL = "pyfuse3"}
209219
extras = ["pyfuse3", "sftp", "s3"]
210220

221+
[tool.tox.env.py312-mfusepy]
222+
set_env = {BORG_FUSE_IMPL = "mfusepy"}
223+
extras = ["mfusepy", "sftp", "s3"]
224+
211225
[tool.tox.env.py313-none]
212226

213-
[tool.tox.env.py313-fuse2]
227+
[tool.tox.env.py313-llfuse]
214228
set_env = {BORG_FUSE_IMPL = "llfuse"}
215229
extras = ["llfuse", "sftp", "s3"]
216230

217-
[tool.tox.env.py313-fuse3]
231+
[tool.tox.env.py313-pyfuse3]
218232
set_env = {BORG_FUSE_IMPL = "pyfuse3"}
219233
extras = ["pyfuse3", "sftp", "s3"]
220234

235+
[tool.tox.env.py313-mfusepy]
236+
set_env = {BORG_FUSE_IMPL = "mfusepy"}
237+
extras = ["mfusepy", "sftp", "s3"]
238+
221239
[tool.tox.env.py314-none]
222240

223-
[tool.tox.env.py314-fuse2]
241+
[tool.tox.env.py314-llfuse]
224242
set_env = {BORG_FUSE_IMPL = "llfuse"}
225243
extras = ["llfuse", "sftp", "s3"]
226244

227-
[tool.tox.env.py314-fuse3]
245+
[tool.tox.env.py314-pyfuse3]
228246
set_env = {BORG_FUSE_IMPL = "pyfuse3"}
229247
extras = ["pyfuse3", "sftp", "s3"]
230248

249+
[tool.tox.env.py314-mfusepy]
250+
set_env = {BORG_FUSE_IMPL = "mfusepy"}
251+
extras = ["mfusepy", "sftp", "s3"]
252+
231253
[tool.tox.env.ruff]
232254
skip_install = true
233255
deps = ["ruff"]

src/borg/archiver/mount_cmds.py

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,9 @@ def do_mount(self, args):
1919
"""Mounts an archive or an entire repository as a FUSE filesystem."""
2020
# Perform these checks before opening the repository and asking for a passphrase.
2121

22-
from ..fuse_impl import llfuse, BORG_FUSE_IMPL
22+
from ..fuse_impl import llfuse, has_mfusepy, BORG_FUSE_IMPL
2323

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

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

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

39-
with cache_if_remote(repository, decrypted_cache=manifest.repo_objs) as cached_repo:
40-
operations = FuseOperations(manifest, args, cached_repo)
39+
if has_mfusepy:
40+
# Use mfusepy implementation
41+
from ..hlfuse import borgfs
42+
43+
operations = borgfs(manifest, args, repository)
4144
logger.info("Mounting filesystem")
4245
try:
4346
operations.mount(args.mountpoint, args.options, args.foreground, args.show_rc)
4447
except RuntimeError:
4548
# Relevant error message already printed to stderr by FUSE
4649
raise RTError("FUSE mount failed")
50+
else:
51+
# Use llfuse/pyfuse3 implementation
52+
from ..fuse import FuseOperations
53+
54+
with cache_if_remote(repository, decrypted_cache=manifest.repo_objs) as cached_repo:
55+
operations = FuseOperations(manifest, args, cached_repo)
56+
logger.info("Mounting filesystem")
57+
try:
58+
operations.mount(args.mountpoint, args.options, args.foreground, args.show_rc)
59+
except RuntimeError:
60+
# Relevant error message already printed to stderr by FUSE
61+
raise RTError("FUSE mount failed")
4762

4863
def do_umount(self, args):
4964
"""Unmounts the FUSE filesystem."""

src/borg/conftest.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
setup_logging()
1313

1414
from borg.archiver import Archiver # noqa: E402
15-
from borg.testsuite import has_lchflags, has_llfuse, has_pyfuse3 # noqa: E402
15+
from borg.testsuite import has_lchflags, has_llfuse, has_pyfuse3, has_mfusepy # noqa: E402
1616
from borg.testsuite import are_symlinks_supported, are_hardlinks_supported, is_utime_fully_supported # noqa: E402
1717
from borg.testsuite.archiver import BORG_EXES
1818
from borg.testsuite.platform.platform_test import fakeroot_detected # noqa: E402
@@ -37,8 +37,9 @@ def clean_env(tmpdir_factory, monkeypatch):
3737
def pytest_report_header(config, start_path):
3838
tests = {
3939
"BSD flags": has_lchflags,
40-
"fuse2": has_llfuse,
41-
"fuse3": has_pyfuse3,
40+
"llfuse": has_llfuse,
41+
"pyfuse3": has_pyfuse3,
42+
"mfusepy": has_mfusepy,
4243
"root": not fakeroot_detected(),
4344
"symlinks": are_symlinks_supported(),
4445
"hardlinks": are_hardlinks_supported(),

src/borg/fuse.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,17 @@
99
import time
1010
from collections import defaultdict, Counter
1111
from signal import SIGINT
12+
from typing import TYPE_CHECKING
1213

1314
from .constants import ROBJ_FILE_STREAM, zeros
14-
from .fuse_impl import llfuse, has_pyfuse3
15-
from .platform import ENOATTR
15+
16+
if TYPE_CHECKING:
17+
# For type checking, assume llfuse is available
18+
# This allows mypy to understand llfuse.Operations
19+
import llfuse
20+
from .fuse_impl import has_pyfuse3, ENOATTR
21+
else:
22+
from .fuse_impl import llfuse, has_pyfuse3, ENOATTR
1623

1724
if has_pyfuse3:
1825
import trio

src/borg/fuse_impl.py

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,63 @@
11
"""
2-
Loads the library for the low-level FUSE implementation.
2+
Loads the library for the FUSE implementation.
33
"""
44

55
import os
6+
import types
67

7-
BORG_FUSE_IMPL = os.environ.get("BORG_FUSE_IMPL", "pyfuse3,llfuse")
8+
from .platform import ENOATTR # noqa
9+
10+
BORG_FUSE_IMPL = os.environ.get("BORG_FUSE_IMPL", "mfusepy,pyfuse3,llfuse")
11+
12+
hlfuse: types.ModuleType | None = None
13+
llfuse: types.ModuleType | None = None
814

915
for FUSE_IMPL in BORG_FUSE_IMPL.split(","):
1016
FUSE_IMPL = FUSE_IMPL.strip()
1117
if FUSE_IMPL == "pyfuse3":
1218
try:
13-
import pyfuse3 as llfuse
19+
import pyfuse3
1420
except ImportError:
1521
pass
1622
else:
23+
llfuse = pyfuse3
1724
has_llfuse = False
1825
has_pyfuse3 = True
26+
has_mfusepy = False
27+
has_any_fuse = True
28+
hlfuse = None # noqa
1929
break
2030
elif FUSE_IMPL == "llfuse":
2131
try:
22-
import llfuse
32+
import llfuse as llfuse_module
2333
except ImportError:
2434
pass
2535
else:
36+
llfuse = llfuse_module
2637
has_llfuse = True
2738
has_pyfuse3 = False
39+
has_mfusepy = False
40+
has_any_fuse = True
41+
hlfuse = None # noqa
42+
break
43+
elif FUSE_IMPL == "mfusepy":
44+
try:
45+
import mfusepy
46+
except ImportError:
47+
pass
48+
else:
49+
hlfuse = mfusepy
50+
has_llfuse = False
51+
has_pyfuse3 = False
52+
has_mfusepy = True
53+
has_any_fuse = True
2854
break
2955
elif FUSE_IMPL == "none":
3056
pass
3157
else:
3258
raise RuntimeError("Unknown FUSE implementation in BORG_FUSE_IMPL: '%s'." % BORG_FUSE_IMPL)
3359
else:
34-
llfuse = None # noqa
3560
has_llfuse = False
3661
has_pyfuse3 = False
62+
has_mfusepy = False
63+
has_any_fuse = False

0 commit comments

Comments
 (0)