Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
6 changes: 6 additions & 0 deletions docs/installation.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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``::

Expand Down
6 changes: 4 additions & 2 deletions src/borg/archiver/mount_cmds.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,12 @@ 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, 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**")
Expand Down
15 changes: 9 additions & 6 deletions src/borg/fuse_impl.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,16 @@
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
Expand All @@ -30,8 +33,8 @@
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
Expand All @@ -43,8 +46,8 @@
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
Expand Down
15 changes: 9 additions & 6 deletions src/borg/helpers/misc.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,19 +36,22 @@ 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

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}]"
from ..fuse_impl import hlfuse, llfuse, BORG_FUSE_IMPL, fuse_import_errors

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)))
if linux_distribution is not None:
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()))
Expand Down
34 changes: 34 additions & 0 deletions src/borg/testsuite/fuse_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,11 @@
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

Expand Down Expand Up @@ -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
Loading