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
24 changes: 19 additions & 5 deletions src/borg/hlfuse.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,15 @@
BLOCK_SIZE = 512 # Standard filesystem block size for st_blocks and statfs


def fuse_options(options):
"""Convert a libfuse option list (e.g. ["ro", "fsname=borgfs"]) to mfusepy keyword arguments."""
kwargs = {}
for option in options:
key, sep, value = option.partition("=")
kwargs[key] = value if sep else True
return kwargs


class borgfs(hlfuse.Operations):
"""Export archive contents as a FUSE filesystem"""

Expand Down Expand Up @@ -64,9 +73,13 @@ def mount(self, mountpoint, mount_options, foreground=False, show_rc=False):
lock_refreshing_thread = LockRefresher(self._repository.info, sleep_interval=60, lock=self._repo_lock)
lock_refreshing_thread.start()
try:
# Run the FUSE main loop in foreground (we might be daemonized already or not)
# Run the FUSE main loop in foreground (we might be daemonized already or not).
# mfusepy takes the libfuse options as keyword arguments (a flag option as True);
# raw_fi=True makes it pass the fuse_file_info struct to open/read/release/getattr.
fuse_kwargs = fuse_options(options)
fuse_kwargs.update(raw_fi=True, foreground=True, use_ino=True)
with signal_handler("SIGUSR1", self.sig_info_handler), signal_handler("SIGINFO", self.sig_info_handler):
hlfuse.FUSE(self, mountpoint, options, foreground=True, use_ino=True)
hlfuse.FUSE(self, mountpoint, **fuse_kwargs)
finally:
lock_refreshing_thread.terminate()

Expand Down Expand Up @@ -128,9 +141,10 @@ def statfs(self, path):
"f_namemax": 255, # == NAME_MAX (depends on archive source OS / FS)
}

def getattr(self, path, fh=None):
# use the file handle if we have one, to avoid the path lookup
node = self._node_from_handle(fh) if fh is not None else self._find_node(path)
def getattr(self, path, fi=None):
# use the file handle (in the fuse_file_info struct, see raw_fi in mount) if we have one,
# to avoid the path lookup.
node = self._node_from_handle(fi.fh) if fi is not None else self._find_node(path)
return self._stat(node)

def listxattr(self, path):
Expand Down
25 changes: 25 additions & 0 deletions src/borg/testsuite/archiver/mount_cmds_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import os
import stat
import sys
import time

import pytest

Expand Down Expand Up @@ -332,6 +333,30 @@ def test_fuse_allow_damaged_files(archivers, request):
assert data.endswith(b"\0\0")


@pytest.mark.skipif(not has_any_fuse, reason="FUSE not available")
def test_fuse_read_to_eof_after_attr_timeout(archivers, request):
"""Reading up to EOF after the attribute timeout must work, and the mount must be read-only.

When a read reaches EOF after the (default 1 s) attribute timeout, the kernel asks for the
attributes of the *open* file first (fgetattr, with the file handle). This used to fail with
EINVAL on the mfusepy backend, and that backend also used to drop all libfuse mount options
(the mount was not even read-only), see the mfusepy FUSE() call in hlfuse.py.
"""
archiver = request.getfixturevalue(archivers)
cmd(archiver, "repo-create", RK_ENCRYPTION)
create_regular_file(archiver.input_path, "file", size=64 * 1024)
cmd(archiver, "create", "archive", "input")
mountpoint = os.path.join(archiver.tmpdir, "mountpoint")
with fuse_mount(archiver, mountpoint, "-a", "archive"):
assert os.statvfs(mountpoint).f_flag & os.ST_RDONLY, "libfuse options (ro) not applied"
with open(os.path.join(mountpoint, "archive", "input", "file"), "rb", buffering=0) as f:
assert len(f.read(4096)) == 4096
time.sleep(1.5) # longer than the default attr_timeout of 1 s
# a read reaching EOF makes the kernel revalidate the size of the *open* file (fgetattr).
# use a raw read: an fstat (as f.read() does) would refresh the attributes without a handle.
assert len(os.read(f.fileno(), 64 * 1024)) == 64 * 1024 - 4096


@pytest.mark.skipif(not has_any_fuse, reason="FUSE not available")
def test_fuse_mount_options(archivers, request):
archiver = request.getfixturevalue(archivers)
Expand Down
Loading