mirror of
https://github.com/borgbackup/borg.git
synced 2026-10-12 02:11:36 +00:00
mount: tell why FUSE support is unavailable, see #8657
- fuse_impl: record the import error of each FUSE implementation that failed to load. also catch non-ImportError failures: mfusepy raises OSError at import time if libfuse is missing (and AttributeError / NotImplementedError in other cases), which previously crashed borg with a traceback instead of a clean error. - borg mount: show the recorded errors, so users see the actual cause (e.g. a missing shared library) instead of just "no FUSE support". - sysinfo: report mfusepy as the active FUSE implementation (it showed "fuse: None" although mounting worked) and include the recorded import errors if no implementation loaded, so debug output pasted into issues shows why. - docs: note that third-party binaries might lack FUSE support. Co-Authored-By: Claude Fable 5 <[email protected]>
This commit is contained in:
co-authored by
Claude Fable 5
parent
d6e64fd98b
commit
2927349210
@@ -118,6 +118,12 @@ are available on the releases_ page for the following platforms:
|
||||
|
||||
ARM binaries are built by Johann Bauer, see: https://borg.bauerj.eu/
|
||||
|
||||
.. note:: ``borg mount`` only works if the binary was built with FUSE support
|
||||
(third-party binaries might lack it) and your OS has FUSE installed
|
||||
(e.g. the ``fuse3`` package on Linux, macFUSE on macOS).
|
||||
If ``borg mount`` fails, the error message will tell for each FUSE
|
||||
implementation why it could not be loaded.
|
||||
|
||||
To install such a binary, just drop it into a directory in your ``PATH``,
|
||||
make borg readable and executable for its users and then you can run ``borg``::
|
||||
|
||||
|
||||
@@ -18,10 +18,12 @@ class MountMixIn:
|
||||
"""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, has_mfusepy, BORG_FUSE_IMPL
|
||||
from ..fuse_impl import llfuse, has_mfusepy, BORG_FUSE_IMPL, fuse_import_errors
|
||||
|
||||
if llfuse is None and not has_mfusepy:
|
||||
raise RTError("borg mount not available: no FUSE support, BORG_FUSE_IMPL=%s." % BORG_FUSE_IMPL)
|
||||
msg = "borg mount not available: no FUSE support, BORG_FUSE_IMPL=%s." % BORG_FUSE_IMPL
|
||||
msg += "".join(f"\nimport of {impl} failed: {err}" for impl, err in fuse_import_errors.items())
|
||||
raise RTError(msg)
|
||||
|
||||
if not os.path.isdir(args.mountpoint):
|
||||
raise RTError(f"{args.mountpoint}: Mountpoint must be an **existing directory**")
|
||||
|
||||
@@ -12,13 +12,16 @@ BORG_FUSE_IMPL = os.environ.get("BORG_FUSE_IMPL", "mfusepy,pyfuse3,llfuse")
|
||||
hlfuse: types.ModuleType | None = None
|
||||
llfuse: types.ModuleType | None = None
|
||||
|
||||
# import failures by implementation name, see issue #8657
|
||||
fuse_import_errors: dict[str, str] = {}
|
||||
|
||||
for FUSE_IMPL in BORG_FUSE_IMPL.split(","):
|
||||
FUSE_IMPL = FUSE_IMPL.strip()
|
||||
if FUSE_IMPL == "pyfuse3":
|
||||
try:
|
||||
import pyfuse3
|
||||
except ImportError:
|
||||
pass
|
||||
except Exception as err:
|
||||
fuse_import_errors[FUSE_IMPL] = str(err)
|
||||
else:
|
||||
llfuse = pyfuse3
|
||||
has_llfuse = False
|
||||
@@ -30,8 +33,8 @@ for FUSE_IMPL in BORG_FUSE_IMPL.split(","):
|
||||
elif FUSE_IMPL == "llfuse":
|
||||
try:
|
||||
import llfuse as llfuse_module
|
||||
except ImportError:
|
||||
pass
|
||||
except Exception as err:
|
||||
fuse_import_errors[FUSE_IMPL] = str(err)
|
||||
else:
|
||||
llfuse = llfuse_module
|
||||
has_llfuse = True
|
||||
@@ -43,8 +46,8 @@ for FUSE_IMPL in BORG_FUSE_IMPL.split(","):
|
||||
elif FUSE_IMPL == "mfusepy":
|
||||
try:
|
||||
import mfusepy
|
||||
except ImportError:
|
||||
pass
|
||||
except Exception as err: # mfusepy raises OSError (not ImportError) if libfuse is missing
|
||||
fuse_import_errors[FUSE_IMPL] = str(err)
|
||||
else:
|
||||
hlfuse = mfusepy
|
||||
has_llfuse = False
|
||||
|
||||
@@ -36,11 +36,14 @@ def sysinfo():
|
||||
msgpack_version = ".".join(str(v) for v in msgpack.version)
|
||||
except: # noqa
|
||||
msgpack_version = "unknown"
|
||||
from ..fuse_impl import llfuse, BORG_FUSE_IMPL
|
||||
from ..fuse_impl import hlfuse, llfuse, BORG_FUSE_IMPL, fuse_import_errors
|
||||
|
||||
llfuse_name = llfuse.__name__ if llfuse else "None"
|
||||
llfuse_version = (" %s" % llfuse.__version__) if llfuse else ""
|
||||
llfuse_info = f"{llfuse_name}{llfuse_version} [{BORG_FUSE_IMPL}]"
|
||||
fuse_mod = hlfuse or llfuse
|
||||
fuse_name = fuse_mod.__name__ if fuse_mod else "None"
|
||||
fuse_version = getattr(fuse_mod, "__version__", "") if fuse_mod else ""
|
||||
fuse_version = " %s" % fuse_version if fuse_version else ""
|
||||
fuse_errors = "".join(f" {impl}: {err}." for impl, err in fuse_import_errors.items()) if not fuse_mod else ""
|
||||
fuse_info = f"{fuse_name}{fuse_version} [{BORG_FUSE_IMPL}]{fuse_errors}"
|
||||
info = []
|
||||
if uname is not None:
|
||||
info.append("Platform: {}".format(" ".join(uname)))
|
||||
@@ -48,7 +51,7 @@ def sysinfo():
|
||||
info.append("Linux: %s %s %s" % linux_distribution)
|
||||
info.append(
|
||||
"Borg: {} Python: {} {} msgpack: {} fuse: {}".format(
|
||||
borg_version, python_implementation, python_version, msgpack_version, llfuse_info
|
||||
borg_version, python_implementation, python_version, msgpack_version, fuse_info
|
||||
)
|
||||
)
|
||||
info.append("PID: %d CWD: %s" % (os.getpid(), os.getcwd()))
|
||||
|
||||
@@ -5,7 +5,11 @@ can not convert as a ValueError - both FUSE adapters have to turn these into the
|
||||
errno, which is what we check here without mounting anything.
|
||||
"""
|
||||
|
||||
import builtins
|
||||
import errno
|
||||
import importlib
|
||||
import os
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
@@ -126,3 +130,33 @@ def test_mfusepy_getxattr_broken_acl():
|
||||
with pytest.raises(hlfuse.FuseOSError) as excinfo:
|
||||
getxattr(ops, "/file", "system.posix_acl_access")
|
||||
assert excinfo.value.errno == errno.EIO
|
||||
|
||||
|
||||
def test_fuse_import_errors_recorded():
|
||||
"""A FUSE impl failing to import must not crash borg and must record why, see #8657."""
|
||||
import borg.fuse_impl
|
||||
|
||||
real_import = builtins.__import__
|
||||
|
||||
def failing_import(name, *args, **kwargs):
|
||||
if name == "mfusepy":
|
||||
raise OSError("Unable to find libfuse") # mfusepy raises OSError, not ImportError
|
||||
if name in ("pyfuse3", "llfuse"):
|
||||
raise ImportError(f"No module named '{name}'")
|
||||
return real_import(name, *args, **kwargs)
|
||||
|
||||
try:
|
||||
with (
|
||||
patch.object(builtins, "__import__", failing_import),
|
||||
patch.dict(os.environ, {"BORG_FUSE_IMPL": "mfusepy,pyfuse3,llfuse"}),
|
||||
):
|
||||
fuse_impl = importlib.reload(borg.fuse_impl)
|
||||
assert not fuse_impl.has_any_fuse
|
||||
assert fuse_impl.hlfuse is None and fuse_impl.llfuse is None
|
||||
assert fuse_impl.fuse_import_errors == {
|
||||
"mfusepy": "Unable to find libfuse",
|
||||
"pyfuse3": "No module named 'pyfuse3'",
|
||||
"llfuse": "No module named 'llfuse'",
|
||||
}
|
||||
finally:
|
||||
importlib.reload(borg.fuse_impl) # restore the real state
|
||||
|
||||
Reference in New Issue
Block a user