FUSE: support pyfuse3 additionally to llfuse, fixes #5407

FUSE implementation can be switched via env var BORG_FUSE_IMPL.
This commit is contained in:
Thomas Waldmann 2020-10-10 23:12:47 +02:00
parent d56c816cf2
commit 49b1421682
16 changed files with 215 additions and 126 deletions

View File

@ -9,44 +9,40 @@ matrix:
include: include:
- python: "3.6" - python: "3.6"
os: linux os: linux
dist: trusty dist: bionic
env: TOXENV=py36 env: TOXENV=py36-fuse2
- python: "3.7" - python: "3.7"
os: linux os: linux
dist: xenial dist: bionic
env: TOXENV=py37 env: TOXENV=py37-fuse2
- python: "3.7-dev"
os: linux
dist: xenial
env: TOXENV=py37
- python: "3.8" - python: "3.8"
os: linux os: linux
dist: xenial dist: focal
env: TOXENV=py38 env: TOXENV=py38-fuse2
- python: "3.8-dev" - python: "3.8-dev"
os: linux os: linux
dist: xenial dist: focal
env: TOXENV=py38 env: TOXENV=py38-fuse3
- python: "3.9-dev"
os: linux
dist: xenial
env: TOXENV=py39
- python: "3.9-dev" - python: "3.9-dev"
os: linux os: linux
dist: focal dist: focal
env: TOXENV=py39 env: TOXENV=py39-fuse2
- python: "3.6" - python: "3.9-dev"
os: linux os: linux
dist: xenial dist: focal
env: TOXENV=py39-fuse3
- python: "3.8"
os: linux
dist: focal
env: TOXENV=flake8 env: TOXENV=flake8
- language: generic - language: generic
os: osx os: osx
osx_image: xcode8.3 # This is the latest working xcode image with osxfuse compatibility; later images come with an OS X version which doesn't allow kernel extensions osx_image: xcode8.3 # This is the latest working xcode image with osxfuse compatibility; later images come with an OS X version which doesn't allow kernel extensions
env: TOXENV=py36 env: TOXENV=py36-fuse2
- language: generic - language: generic
os: osx os: osx
osx_image: xcode11.3 osx_image: xcode11.3
env: TOXENV=py37 SKIPFUSE=true env: TOXENV=py37 # No FUSE testing, because recent versions of macOS don't allow kernel extensions of osxfuse.
allow_failures: allow_failures:
- os: osx # OS X builds often take too long and time out, even though tests don't actually fail - os: osx # OS X builds often take too long and time out, even though tests don't actually fail

View File

@ -48,7 +48,8 @@ then
#sudo apt-get install -y liblz4-dev # Too old on trusty and xenial, but might be useful in future versions #sudo apt-get install -y liblz4-dev # Too old on trusty and xenial, but might be useful in future versions
#sudo apt-get install -y libzstd-dev # Too old on trusty and xenial, but might be useful in future versions #sudo apt-get install -y libzstd-dev # Too old on trusty and xenial, but might be useful in future versions
sudo apt-get install -y libacl1-dev sudo apt-get install -y libacl1-dev
sudo apt-get install -y libfuse-dev fuse # Required for Python llfuse module sudo apt-get install -y libfuse-dev fuse || true # Required for Python llfuse module
sudo apt-get install -y libfuse3-dev fuse3 || true # Required for Python pyfuse3 module
else else
@ -67,12 +68,3 @@ pip install -r requirements.d/development.txt
pip install codecov pip install codecov
python setup.py --version python setup.py --version
# Recent versions of OS X don't allow kernel extensions which makes the osxfuse tests fail; those versions are marked with SKIPFUSE=true in .travis.yml
if [ "${SKIPFUSE}" = "true" ]
then
truncate -s 0 requirements.d/fuse.txt
pip install -e .
else
pip install -e .[fuse]
fi

46
Vagrantfile vendored
View File

@ -15,7 +15,9 @@ def packages_debianoid(user)
apt-get -y -qq update apt-get -y -qq update
apt-get -y -qq dist-upgrade apt-get -y -qq dist-upgrade
# for building borgbackup and dependencies: # for building borgbackup and dependencies:
apt install -y libssl-dev libacl1-dev liblz4-dev libzstd-dev libfuse-dev fuse pkg-config apt install -y libssl-dev libacl1-dev liblz4-dev libzstd-dev pkg-config
apt install -y libfuse-dev fuse || true
apt install -y libfuse3-dev fuse3 || true
usermod -a -G fuse #{user} usermod -a -G fuse #{user}
chgrp fuse /dev/fuse chgrp fuse /dev/fuse
chmod 666 /dev/fuse chmod 666 /dev/fuse
@ -43,7 +45,9 @@ def packages_freebsd
# install all the (security and other) updates, base system # install all the (security and other) updates, base system
freebsd-update --not-running-from-cron fetch install freebsd-update --not-running-from-cron fetch install
# for building borgbackup and dependencies: # for building borgbackup and dependencies:
pkg install -y liblz4 zstd fusefs-libs pkgconf pkg install -y liblz4 zstd pkgconf
pkg install -y fusefs-libs || true
pkg install -y fusefs-libs3 || true
pkg install -y git bash # fakeroot causes lots of troubles on freebsd pkg install -y git bash # fakeroot causes lots of troubles on freebsd
# for building python: # for building python:
pkg install -y python37 py37-sqlite3 py37-virtualenv py37-pip pkg install -y python37 py37-sqlite3 py37-virtualenv py37-pip
@ -160,7 +164,7 @@ def build_pyenv_venv(boxname)
end end
def install_borg(fuse) def install_borg(fuse)
script = <<-EOF return <<-EOF
. ~/.bash_profile . ~/.bash_profile
cd /vagrant/borg cd /vagrant/borg
. borg-env/bin/activate . borg-env/bin/activate
@ -168,20 +172,8 @@ def install_borg(fuse)
cd borg cd borg
pip install -r requirements.d/development.txt pip install -r requirements.d/development.txt
python setup.py clean python setup.py clean
pip install -e .[#{fuse}]
EOF EOF
if fuse
script += <<-EOF
# by using [fuse], setup.py can handle different FUSE requirements:
pip install -e .[fuse]
EOF
else
script += <<-EOF
pip install -e .
# do not install llfuse into the virtualenvs built by tox:
sed -i.bak '/fuse.txt/d' tox.ini
EOF
end
return script
end end
def install_pyinstaller() def install_pyinstaller()
@ -221,10 +213,10 @@ def run_tests(boxname)
# otherwise: just use the system python # otherwise: just use the system python
if which fakeroot 2> /dev/null; then if which fakeroot 2> /dev/null; then
echo "Running tox WITH fakeroot -u" echo "Running tox WITH fakeroot -u"
fakeroot -u tox --skip-missing-interpreters -e py36,py37,py38,py39 fakeroot -u tox --skip-missing-interpreters
else else
echo "Running tox WITHOUT fakeroot -u" echo "Running tox WITHOUT fakeroot -u"
tox --skip-missing-interpreters -e py36,py37,py38,py39 tox --skip-missing-interpreters
fi fi
EOF EOF
end end
@ -263,7 +255,7 @@ Vagrant.configure(2) do |config|
b.vm.provision "fs init", :type => :shell, :inline => fs_init("vagrant") b.vm.provision "fs init", :type => :shell, :inline => fs_init("vagrant")
b.vm.provision "packages debianoid", :type => :shell, :inline => packages_debianoid("vagrant") b.vm.provision "packages debianoid", :type => :shell, :inline => packages_debianoid("vagrant")
b.vm.provision "build env", :type => :shell, :privileged => false, :inline => build_sys_venv("focal64") b.vm.provision "build env", :type => :shell, :privileged => false, :inline => build_sys_venv("focal64")
b.vm.provision "install borg", :type => :shell, :privileged => false, :inline => install_borg(true) b.vm.provision "install borg", :type => :shell, :privileged => false, :inline => install_borg("llfuse")
b.vm.provision "run tests", :type => :shell, :privileged => false, :inline => run_tests("focal64") b.vm.provision "run tests", :type => :shell, :privileged => false, :inline => run_tests("focal64")
end end
@ -275,7 +267,7 @@ Vagrant.configure(2) do |config|
b.vm.provision "fs init", :type => :shell, :inline => fs_init("vagrant") b.vm.provision "fs init", :type => :shell, :inline => fs_init("vagrant")
b.vm.provision "packages debianoid", :type => :shell, :inline => packages_debianoid("vagrant") b.vm.provision "packages debianoid", :type => :shell, :inline => packages_debianoid("vagrant")
b.vm.provision "build env", :type => :shell, :privileged => false, :inline => build_sys_venv("bionic64") b.vm.provision "build env", :type => :shell, :privileged => false, :inline => build_sys_venv("bionic64")
b.vm.provision "install borg", :type => :shell, :privileged => false, :inline => install_borg(true) b.vm.provision "install borg", :type => :shell, :privileged => false, :inline => install_borg("llfuse")
b.vm.provision "run tests", :type => :shell, :privileged => false, :inline => run_tests("bionic64") b.vm.provision "run tests", :type => :shell, :privileged => false, :inline => run_tests("bionic64")
end end
@ -289,7 +281,7 @@ Vagrant.configure(2) do |config|
b.vm.provision "install pyenv", :type => :shell, :privileged => false, :inline => install_pyenv("buster64") b.vm.provision "install pyenv", :type => :shell, :privileged => false, :inline => install_pyenv("buster64")
b.vm.provision "install pythons", :type => :shell, :privileged => false, :inline => install_pythons("buster64") b.vm.provision "install pythons", :type => :shell, :privileged => false, :inline => install_pythons("buster64")
b.vm.provision "build env", :type => :shell, :privileged => false, :inline => build_pyenv_venv("buster64") b.vm.provision "build env", :type => :shell, :privileged => false, :inline => build_pyenv_venv("buster64")
b.vm.provision "install borg", :type => :shell, :privileged => false, :inline => install_borg(true) 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 "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("buster64") b.vm.provision "build binary with pyinstaller", :type => :shell, :privileged => false, :inline => build_binary_with_pyinstaller("buster64")
b.vm.provision "run tests", :type => :shell, :privileged => false, :inline => run_tests("buster64") b.vm.provision "run tests", :type => :shell, :privileged => false, :inline => run_tests("buster64")
@ -305,7 +297,7 @@ Vagrant.configure(2) do |config|
b.vm.provision "install pyenv", :type => :shell, :privileged => false, :inline => install_pyenv("stretch64") b.vm.provision "install pyenv", :type => :shell, :privileged => false, :inline => install_pyenv("stretch64")
b.vm.provision "install pythons", :type => :shell, :privileged => false, :inline => install_pythons("stretch64") b.vm.provision "install pythons", :type => :shell, :privileged => false, :inline => install_pythons("stretch64")
b.vm.provision "build env", :type => :shell, :privileged => false, :inline => build_pyenv_venv("stretch64") b.vm.provision "build env", :type => :shell, :privileged => false, :inline => build_pyenv_venv("stretch64")
b.vm.provision "install borg", :type => :shell, :privileged => false, :inline => install_borg(true) 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 "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("stretch64") b.vm.provision "build binary with pyinstaller", :type => :shell, :privileged => false, :inline => build_binary_with_pyinstaller("stretch64")
b.vm.provision "run tests", :type => :shell, :privileged => false, :inline => run_tests("stretch64") b.vm.provision "run tests", :type => :shell, :privileged => false, :inline => run_tests("stretch64")
@ -319,7 +311,7 @@ Vagrant.configure(2) do |config|
b.vm.provision "fs init", :type => :shell, :inline => fs_init("vagrant") b.vm.provision "fs init", :type => :shell, :inline => fs_init("vagrant")
b.vm.provision "packages arch", :type => :shell, :privileged => true, :inline => packages_arch b.vm.provision "packages arch", :type => :shell, :privileged => true, :inline => packages_arch
b.vm.provision "build env", :type => :shell, :privileged => false, :inline => build_sys_venv("arch64") b.vm.provision "build env", :type => :shell, :privileged => false, :inline => build_sys_venv("arch64")
b.vm.provision "install borg", :type => :shell, :privileged => false, :inline => install_borg(true) b.vm.provision "install borg", :type => :shell, :privileged => false, :inline => install_borg("llfuse")
b.vm.provision "run tests", :type => :shell, :privileged => false, :inline => run_tests("arch64") b.vm.provision "run tests", :type => :shell, :privileged => false, :inline => run_tests("arch64")
end end
@ -334,7 +326,7 @@ Vagrant.configure(2) do |config|
b.vm.provision "install pyenv", :type => :shell, :privileged => false, :inline => install_pyenv("freebsd64") b.vm.provision "install pyenv", :type => :shell, :privileged => false, :inline => install_pyenv("freebsd64")
b.vm.provision "install pythons", :type => :shell, :privileged => false, :inline => install_pythons("freebsd64") b.vm.provision "install pythons", :type => :shell, :privileged => false, :inline => install_pythons("freebsd64")
b.vm.provision "build env", :type => :shell, :privileged => false, :inline => build_pyenv_venv("freebsd64") b.vm.provision "build env", :type => :shell, :privileged => false, :inline => build_pyenv_venv("freebsd64")
b.vm.provision "install borg", :type => :shell, :privileged => false, :inline => install_borg(true) 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 "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("freebsd64") b.vm.provision "build binary with pyinstaller", :type => :shell, :privileged => false, :inline => build_binary_with_pyinstaller("freebsd64")
b.vm.provision "run tests", :type => :shell, :privileged => false, :inline => run_tests("freebsd64") b.vm.provision "run tests", :type => :shell, :privileged => false, :inline => run_tests("freebsd64")
@ -349,7 +341,7 @@ Vagrant.configure(2) do |config|
b.vm.provision "fs init", :type => :shell, :inline => fs_init("vagrant") b.vm.provision "fs init", :type => :shell, :inline => fs_init("vagrant")
b.vm.provision "packages openbsd", :type => :shell, :inline => packages_openbsd b.vm.provision "packages openbsd", :type => :shell, :inline => packages_openbsd
b.vm.provision "build env", :type => :shell, :privileged => false, :inline => build_sys_venv("openbsd64") b.vm.provision "build env", :type => :shell, :privileged => false, :inline => build_sys_venv("openbsd64")
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("openbsd64") b.vm.provision "run tests", :type => :shell, :privileged => false, :inline => run_tests("openbsd64")
end end
@ -373,7 +365,7 @@ Vagrant.configure(2) do |config|
b.vm.provision "fix pyenv", :type => :shell, :privileged => false, :inline => fix_pyenv_darwin("darwin64") b.vm.provision "fix pyenv", :type => :shell, :privileged => false, :inline => fix_pyenv_darwin("darwin64")
b.vm.provision "install pythons", :type => :shell, :privileged => false, :inline => install_pythons("darwin64") b.vm.provision "install pythons", :type => :shell, :privileged => false, :inline => install_pythons("darwin64")
b.vm.provision "build env", :type => :shell, :privileged => false, :inline => build_pyenv_venv("darwin64") b.vm.provision "build env", :type => :shell, :privileged => false, :inline => build_pyenv_venv("darwin64")
b.vm.provision "install borg", :type => :shell, :privileged => false, :inline => install_borg(true) 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 "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("darwin64") b.vm.provision "build binary with pyinstaller", :type => :shell, :privileged => false, :inline => build_binary_with_pyinstaller("darwin64")
b.vm.provision "run tests", :type => :shell, :privileged => false, :inline => run_tests("darwin64") b.vm.provision "run tests", :type => :shell, :privileged => false, :inline => run_tests("darwin64")
@ -389,7 +381,7 @@ Vagrant.configure(2) do |config|
b.vm.provision "fs init", :type => :shell, :inline => fs_init("vagrant") b.vm.provision "fs init", :type => :shell, :inline => fs_init("vagrant")
b.vm.provision "packages openindiana", :type => :shell, :inline => packages_openindiana b.vm.provision "packages openindiana", :type => :shell, :inline => packages_openindiana
b.vm.provision "build env", :type => :shell, :privileged => false, :inline => build_sys_venv("openindiana64") b.vm.provision "build env", :type => :shell, :privileged => false, :inline => build_sys_venv("openindiana64")
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("openindiana64") b.vm.provision "run tests", :type => :shell, :privileged => false, :inline => run_tests("openindiana64")
end end

View File

@ -21,7 +21,7 @@ from borg.logger import setup_logging
# Ensure that the loggers exist for all tests # Ensure that the loggers exist for all tests
setup_logging() setup_logging()
from borg.testsuite import has_lchflags, has_llfuse from borg.testsuite import has_lchflags, has_llfuse, has_pyfuse3
from borg.testsuite import are_symlinks_supported, are_hardlinks_supported, is_utime_fully_supported from borg.testsuite import are_symlinks_supported, are_hardlinks_supported, is_utime_fully_supported
from borg.testsuite.platform import fakeroot_detected, are_acls_working from borg.testsuite.platform import fakeroot_detected, are_acls_working
from borg import xattr from borg import xattr
@ -33,7 +33,8 @@ def clean_env(tmpdir_factory, monkeypatch):
monkeypatch.setenv('XDG_CONFIG_HOME', str(tmpdir_factory.mktemp('xdg-config-home'))) monkeypatch.setenv('XDG_CONFIG_HOME', str(tmpdir_factory.mktemp('xdg-config-home')))
monkeypatch.setenv('XDG_CACHE_HOME', str(tmpdir_factory.mktemp('xdg-cache-home'))) monkeypatch.setenv('XDG_CACHE_HOME', str(tmpdir_factory.mktemp('xdg-cache-home')))
# also avoid to use anything from the outside environment: # also avoid to use anything from the outside environment:
keys = [key for key in os.environ if key.startswith('BORG_')] keys = [key for key in os.environ
if key.startswith('BORG_') and key not in ('BORG_FUSE_IMPL', )]
for key in keys: for key in keys:
monkeypatch.delenv(key, raising=False) monkeypatch.delenv(key, raising=False)
@ -41,7 +42,8 @@ def clean_env(tmpdir_factory, monkeypatch):
def pytest_report_header(config, startdir): def pytest_report_header(config, startdir):
tests = { tests = {
"BSD flags": has_lchflags, "BSD flags": has_lchflags,
"fuse": has_llfuse, "fuse2": has_llfuse,
"fuse3": has_pyfuse3,
"root": not fakeroot_detected(), "root": not fakeroot_detected(),
"symlinks": are_symlinks_supported(), "symlinks": are_symlinks_supported(),
"hardlinks": are_hardlinks_supported(), "hardlinks": are_hardlinks_supported(),

View File

@ -288,7 +288,7 @@ Usage::
Creating standalone binaries Creating standalone binaries
---------------------------- ----------------------------
Make sure you have everything built and installed (including llfuse and fuse). Make sure you have everything built and installed (including fuse stuff).
When using the Vagrant VMs, pyinstaller will already be installed. When using the Vagrant VMs, pyinstaller will already be installed.
With virtual env activated:: With virtual env activated::

View File

@ -22,6 +22,7 @@
.. _msgpack: https://msgpack.org/ .. _msgpack: https://msgpack.org/
.. _`msgpack-python`: https://pypi.python.org/pypi/msgpack-python/ .. _`msgpack-python`: https://pypi.python.org/pypi/msgpack-python/
.. _llfuse: https://pypi.python.org/pypi/llfuse/ .. _llfuse: https://pypi.python.org/pypi/llfuse/
.. _pyfuse3: https://pypi.python.org/pypi/pyfuse3/
.. _userspace filesystems: https://en.wikipedia.org/wiki/Filesystem_in_Userspace .. _userspace filesystems: https://en.wikipedia.org/wiki/Filesystem_in_Userspace
.. _Cython: http://cython.org/ .. _Cython: http://cython.org/
.. _virtualenv: https://pypi.python.org/pypi/virtualenv/ .. _virtualenv: https://pypi.python.org/pypi/virtualenv/

View File

@ -159,8 +159,12 @@ following dependencies first:
it will fall back to using the bundled code, see above). it will fall back to using the bundled code, see above).
These must be present before invoking setup.py! These must be present before invoking setup.py!
* some other Python dependencies, pip will automatically install them for you. * some other Python dependencies, pip will automatically install them for you.
* optionally, the llfuse_ Python package is required if you wish to mount an * optionally, if you wish to mount an archive as a FUSE filesystem, you need
archive as a FUSE filesystem. See setup.py about the version requirements. a FUSE implementation for Python:
- Either pyfuse3_ (preferably, newer and maintained) or llfuse_ (older,
unmaintained now). See also the BORG_FUSE_IMPL env variable.
- See setup.py about the version requirements.
If you have troubles finding the right package names, have a look at the If you have troubles finding the right package names, have a look at the
distribution specific sections below or the Vagrantfile in the git repository, distribution specific sections below or the Vagrantfile in the git repository,
@ -186,7 +190,8 @@ Install the dependencies with development headers::
liblz4-dev libzstd-dev \ liblz4-dev libzstd-dev \
build-essential \ build-essential \
pkg-config python3-pkgconfig pkg-config python3-pkgconfig
sudo apt-get install libfuse-dev fuse # optional, for FUSE support sudo apt-get install libfuse-dev fuse # needed for llfuse
sudo apt-get install libfuse3-dev fuse3 # needed for pyfuse3
In case you get complaints about permission denied on ``/etc/fuse.conf``: on In case you get complaints about permission denied on ``/etc/fuse.conf``: on
Ubuntu this means your user is not in the ``fuse`` group. Add yourself to that Ubuntu this means your user is not in the ``fuse`` group. Add yourself to that
@ -203,7 +208,8 @@ Install the dependencies with development headers::
lz4-devel libzstd-devel \ lz4-devel libzstd-devel \
pkgconf python3-pkgconfig pkgconf python3-pkgconfig
sudo dnf install gcc gcc-c++ redhat-rpm-config sudo dnf install gcc gcc-c++ redhat-rpm-config
sudo dnf install fuse-devel fuse # optional, for FUSE support sudo dnf install fuse-devel fuse # needed for llfuse
sudo dnf install fuse3-devel fuse3 # needed for pyfuse3
openSUSE Tumbleweed / Leap openSUSE Tumbleweed / Leap
++++++++++++++++++++++++++ ++++++++++++++++++++++++++
@ -218,7 +224,8 @@ Alternatively, you can enumerate all build dependencies in the command line::
libacl-devel openssl-devel \ libacl-devel openssl-devel \
python3-Cython python3-Sphinx python3-msgpack-python \ python3-Cython python3-Sphinx python3-msgpack-python \
python3-pytest python3-setuptools python3-setuptools_scm \ python3-pytest python3-setuptools python3-setuptools_scm \
python3-sphinx_rtd_theme python3-llfuse gcc gcc-c++ python3-sphinx_rtd_theme gcc gcc-c++
sudo zypper install python3-llfuse # llfuse
Mac OS X Mac OS X
++++++++ ++++++++
@ -234,7 +241,7 @@ For FUSE support to mount the backup archives, you need at least version 3.0 of
FUSE for OS X, which is available via `github FUSE for OS X, which is available via `github
<https://github.com/osxfuse/osxfuse/releases/latest>`__, or via Homebrew:: <https://github.com/osxfuse/osxfuse/releases/latest>`__, or via Homebrew::
brew cask install osxfuse brew cask install osxfuse # needed for llfuse
FreeBSD FreeBSD
@ -248,7 +255,7 @@ and commands to make FUSE work for using the mount command.
pkg install -y python3 pkgconf pkg install -y python3 pkgconf
pkg install openssl pkg install openssl
pkg install liblz4 zstd pkg install liblz4 zstd
pkg install fusefs-libs pkg install fusefs-libs # needed for llfuse
pkg install -y git pkg install -y git
python3.5 -m ensurepip # to install pip for Python3 python3.5 -m ensurepip # to install pip for Python3
To use the mount command: To use the mount command:
@ -308,15 +315,17 @@ This will use ``pip`` to install the latest release from PyPi::
# might be required if your tools are outdated # might be required if your tools are outdated
pip install -U pip setuptools wheel pip install -U pip setuptools wheel
# install Borg + Python dependencies into virtualenv # install Borg + Python dependencies into virtualenv
pip install borgbackup pip install borgbackup
# or alternatively (if you want FUSE support): # or alternatively (if you want FUSE support):
pip install borgbackup[fuse] pip install borgbackup[llfuse] # to use llfuse
pip install borgbackup[pyfuse3] # to use pyfuse3
To upgrade Borg to a new version later, run the following after To upgrade Borg to a new version later, run the following after
activating your virtual environment:: activating your virtual environment::
pip install -U borgbackup # or ... borgbackup[fuse] pip install -U borgbackup # or ... borgbackup[llfuse/pyfuse3]
.. _git-installation: .. _git-installation:
@ -339,8 +348,12 @@ While we try not to break master, there are no guarantees on anything.
cd borg cd borg
pip install -r requirements.d/development.txt pip install -r requirements.d/development.txt
pip install -r requirements.d/docs.txt # optional, to build the docs pip install -r requirements.d/docs.txt # optional, to build the docs
pip install -r requirements.d/fuse.txt # optional, for FUSE support
pip install -e . # in-place editable mode pip install -e . # in-place editable mode
or
pip install -e .[pyfuse3] # in-place editable mode, use pyfuse3
or
pip install -e .[llfuse] # in-place editable mode, use llfuse
# optional: run all the tests, on all supported Python versions # optional: run all the tests, on all supported Python versions
# requires fakeroot, available through your package manager # requires fakeroot, available through your package manager

View File

@ -64,6 +64,16 @@ General:
When set to no (default: yes), system information (like OS, Python version, ...) in When set to no (default: yes), system information (like OS, Python version, ...) in
exceptions is not shown. exceptions is not shown.
Please only use for good reasons as it makes issues harder to analyze. Please only use for good reasons as it makes issues harder to analyze.
BORG_FUSE_IMPL
Choose the lowlevel FUSE implementation borg shall use for ``borg mount``.
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.
- ``llfuse,pyfuse3``: first try to load llfuse, then try to load pyfuse3.
- ``pyfuse3``: only try to load pyfuse3
- ``llfuse``: only try to load llfuse
- ``none``: do not try to load an implementation
BORG_WORKAROUNDS BORG_WORKAROUNDS
A list of comma separated strings that trigger workarounds in borg, A list of comma separated strings that trigger workarounds in borg,
e.g. to work around bugs in other software. e.g. to work around bugs in other software.

View File

@ -1,4 +0,0 @@
# low-level FUSE support library for "borg mount"
# please see the comments in setup.py about llfuse.
llfuse >=1.3.4, <1.3.7; python_version <"3.9" # broken on py39
llfuse >=1.3.7, <2.0; python_version >="3.9" # broken on freebsd

View File

@ -78,14 +78,17 @@ install_requires = [
] ]
# note for package maintainers: if you package borgbackup for distribution, # note for package maintainers: if you package borgbackup for distribution,
# please add llfuse as a *requirement* on all platforms that have a working # please (if available) add pyfuse3 (preferably) or llfuse (not maintained any more)
# llfuse package. "borg mount" needs llfuse to work. # as a *requirement*. "borg mount" needs one of them to work.
# if you do not have llfuse, do not require it, most of borgbackup will work. # if neither is available, do not require it, most of borgbackup will work.
extras_require = { extras_require = {
'fuse': [ 'llfuse': [
'llfuse >=1.3.4, <1.3.7; python_version <"3.9"', # broken on py39 'llfuse >= 1.3.8',
'llfuse >=1.3.7, <2.0; python_version >="3.9"', # broken on freebsd
], ],
'pyfuse3': [
'pyfuse3 >= 3.1.1',
],
'nofuse': [],
} }
compress_source = 'src/borg/compress.pyx' compress_source = 'src/borg/compress.pyx'

View File

@ -1258,10 +1258,9 @@ class Archiver:
"""Mount archive or an entire repository as a FUSE filesystem""" """Mount archive or an entire repository as a FUSE filesystem"""
# Perform these checks before opening the repository and asking for a passphrase. # Perform these checks before opening the repository and asking for a passphrase.
try: from .fuse_impl import llfuse, BORG_FUSE_IMPL
import borg.fuse if llfuse is None:
except ImportError as e: self.print_error('borg mount not available: no FUSE support, BORG_FUSE_IMPL=%s.' % BORG_FUSE_IMPL)
self.print_error('borg mount not available: loading FUSE support failed [ImportError: %s]' % str(e))
return self.exit_code return self.exit_code
if not os.path.isdir(args.mountpoint) or not os.access(args.mountpoint, os.R_OK | os.W_OK | os.X_OK): if not os.path.isdir(args.mountpoint) or not os.access(args.mountpoint, os.R_OK | os.W_OK | os.X_OK):

View File

@ -1,4 +1,5 @@
import errno import errno
import functools
import io import io
import os import os
import stat import stat
@ -9,7 +10,23 @@ import time
from collections import defaultdict from collections import defaultdict
from signal import SIGINT from signal import SIGINT
import llfuse from .fuse_impl import llfuse, has_pyfuse3
if has_pyfuse3:
import trio
def async_wrapper(fn):
@functools.wraps(fn)
async def wrapper(*args, **kwargs):
return fn(*args, **kwargs)
return wrapper
else:
trio = None
def async_wrapper(fn):
return fn
from .logger import create_logger from .logger import create_logger
logger = create_logger() logger = create_logger()
@ -26,7 +43,15 @@ from .remote import RemoteRepository
def fuse_main(): def fuse_main():
return llfuse.main(workers=1) if has_pyfuse3:
try:
trio.run(llfuse.main)
except:
return 1 # TODO return signal number if it was killed by signal
else:
return None
else:
return llfuse.main(workers=1)
# size of some LRUCaches (1 element per simultaneously open file) # size of some LRUCaches (1 element per simultaneously open file)
@ -533,6 +558,7 @@ class FuseOperations(llfuse.Operations, FuseBackend):
finally: finally:
llfuse.close(umount) llfuse.close(umount)
@async_wrapper
def statfs(self, ctx=None): def statfs(self, ctx=None):
stat_ = llfuse.StatvfsData() stat_ = llfuse.StatvfsData()
stat_.f_bsize = 512 stat_.f_bsize = 512
@ -546,7 +572,7 @@ class FuseOperations(llfuse.Operations, FuseBackend):
stat_.f_namemax = 255 # == NAME_MAX (depends on archive source OS / FS) stat_.f_namemax = 255 # == NAME_MAX (depends on archive source OS / FS)
return stat_ return stat_
def getattr(self, inode, ctx=None): def _getattr(self, inode, ctx=None):
item = self.get_item(inode) item = self.get_item(inode)
entry = llfuse.EntryAttributes() entry = llfuse.EntryAttributes()
entry.st_ino = inode entry.st_ino = inode
@ -568,10 +594,16 @@ class FuseOperations(llfuse.Operations, FuseBackend):
entry.st_birthtime_ns = item.get('birthtime', mtime_ns) entry.st_birthtime_ns = item.get('birthtime', mtime_ns)
return entry return entry
@async_wrapper
def getattr(self, inode, ctx=None):
return self._getattr(inode, ctx=ctx)
@async_wrapper
def listxattr(self, inode, ctx=None): def listxattr(self, inode, ctx=None):
item = self.get_item(inode) item = self.get_item(inode)
return item.get('xattrs', {}).keys() return item.get('xattrs', {}).keys()
@async_wrapper
def getxattr(self, inode, name, ctx=None): def getxattr(self, inode, name, ctx=None):
item = self.get_item(inode) item = self.get_item(inode)
try: try:
@ -579,6 +611,7 @@ class FuseOperations(llfuse.Operations, FuseBackend):
except KeyError: except KeyError:
raise llfuse.FUSEError(llfuse.ENOATTR) from None raise llfuse.FUSEError(llfuse.ENOATTR) from None
@async_wrapper
def lookup(self, parent_inode, name, ctx=None): def lookup(self, parent_inode, name, ctx=None):
self.check_pending_archive(parent_inode) self.check_pending_archive(parent_inode)
if name == b'.': if name == b'.':
@ -589,8 +622,9 @@ class FuseOperations(llfuse.Operations, FuseBackend):
inode = self.contents[parent_inode].get(name) inode = self.contents[parent_inode].get(name)
if not inode: if not inode:
raise llfuse.FUSEError(errno.ENOENT) raise llfuse.FUSEError(errno.ENOENT)
return self.getattr(inode) return self._getattr(inode)
@async_wrapper
def open(self, inode, flags, ctx=None): def open(self, inode, flags, ctx=None):
if not self.allow_damaged_files: if not self.allow_damaged_files:
item = self.get_item(inode) item = self.get_item(inode)
@ -601,12 +635,14 @@ class FuseOperations(llfuse.Operations, FuseBackend):
logger.warning('File has damaged (all-zero) chunks. Try running borg check --repair. ' logger.warning('File has damaged (all-zero) chunks. Try running borg check --repair. '
'Mount with allow_damaged_files to read damaged files.') 'Mount with allow_damaged_files to read damaged files.')
raise llfuse.FUSEError(errno.EIO) raise llfuse.FUSEError(errno.EIO)
return inode return llfuse.FileInfo(fh=inode) if has_pyfuse3 else inode
@async_wrapper
def opendir(self, inode, ctx=None): def opendir(self, inode, ctx=None):
self.check_pending_archive(inode) self.check_pending_archive(inode)
return inode return inode
@async_wrapper
def read(self, fh, offset, size): def read(self, fh, offset, size):
parts = [] parts = []
item = self.get_item(fh) item = self.get_item(fh)
@ -650,12 +686,25 @@ class FuseOperations(llfuse.Operations, FuseBackend):
break break
return b''.join(parts) return b''.join(parts)
def readdir(self, fh, off): # note: we can't have a generator (with yield) and not a generator (async) in the same method
entries = [(b'.', fh), (b'..', self.parent[fh])] if has_pyfuse3:
entries.extend(self.contents[fh].items()) async def readdir(self, fh, off, token):
for i, (name, inode) in enumerate(entries[off:], off): entries = [(b'.', fh), (b'..', self.parent[fh])]
yield name, self.getattr(inode), i + 1 entries.extend(self.contents[fh].items())
for i, (name, inode) in enumerate(entries[off:], off):
attrs = self._getattr(inode)
if not llfuse.readdir_reply(token, name, attrs, i + 1):
break
else:
def readdir(self, fh, off):
entries = [(b'.', fh), (b'..', self.parent[fh])]
entries.extend(self.contents[fh].items())
for i, (name, inode) in enumerate(entries[off:], off):
attrs = self._getattr(inode)
yield name, attrs, i + 1
@async_wrapper
def readlink(self, inode, ctx=None): def readlink(self, inode, ctx=None):
item = self.get_item(inode) item = self.get_item(inode)
return os.fsencode(item.source) return os.fsencode(item.source)

36
src/borg/fuse_impl.py Normal file
View File

@ -0,0 +1,36 @@
"""
load library for lowlevel FUSE implementation
"""
import os
BORG_FUSE_IMPL = os.environ.get('BORG_FUSE_IMPL', 'pyfuse3,llfuse')
for FUSE_IMPL in BORG_FUSE_IMPL.split(','):
FUSE_IMPL = FUSE_IMPL.strip()
if FUSE_IMPL == 'pyfuse3':
try:
import pyfuse3 as llfuse
except ImportError:
pass
else:
has_llfuse = False
has_pyfuse3 = True
break
elif FUSE_IMPL == 'llfuse':
try:
import llfuse
except ImportError:
pass
else:
has_llfuse = True
has_pyfuse3 = False
break
elif FUSE_IMPL == 'none':
pass
else:
raise RuntimeError("unknown fuse implementation in BORG_FUSE_IMPL: '%s'" % BORG_FUSE_IMPL)
else:
llfuse = None
has_llfuse = False
has_pyfuse3 = False

View File

@ -23,12 +23,10 @@ from .. import platform
# Note: this is used by borg.selftest, do not use or import py.test functionality here. # Note: this is used by borg.selftest, do not use or import py.test functionality here.
try: from ..fuse_impl import llfuse, has_pyfuse3, has_llfuse
import llfuse
# Does this version of llfuse support ns precision? # Does this version of llfuse support ns precision?
have_fuse_mtime_ns = hasattr(llfuse.EntryAttributes, 'st_mtime_ns') have_fuse_mtime_ns = hasattr(llfuse.EntryAttributes, 'st_mtime_ns') if llfuse else False
except ImportError:
have_fuse_mtime_ns = False
try: try:
from pytest import raises from pytest import raises
@ -42,12 +40,6 @@ try:
except OSError: except OSError:
has_lchflags = False has_lchflags = False
try:
import llfuse
has_llfuse = True or llfuse # avoids "unused import"
except ImportError:
has_llfuse = False
# The mtime get/set precision varies on different OS and Python versions # The mtime get/set precision varies on different OS and Python versions
if posix and 'HAVE_FUTIMENS' in getattr(posix, '_have_functions', []): if posix and 'HAVE_FUTIMENS' in getattr(posix, '_have_functions', []):
st_mtime_ns_round = 0 st_mtime_ns_round = 0

View File

@ -26,11 +26,6 @@ from unittest.mock import patch
import pytest import pytest
try:
import llfuse
except ImportError:
pass
import borg import borg
from .. import xattr, helpers, platform from .. import xattr, helpers, platform
from ..archive import Archive, ChunkBuffer from ..archive import Archive, ChunkBuffer
@ -55,7 +50,7 @@ from ..locking import LockFailed
from ..logger import setup_logging from ..logger import setup_logging
from ..remote import RemoteRepository, PathNotAllowed from ..remote import RemoteRepository, PathNotAllowed
from ..repository import Repository from ..repository import Repository
from . import has_lchflags, has_llfuse from . import has_lchflags, llfuse
from . import BaseTestCase, changedir, environment_variable, no_selinux from . import BaseTestCase, changedir, environment_variable, no_selinux
from . import are_symlinks_supported, are_hardlinks_supported, are_fifos_supported, is_utime_fully_supported, is_birthtime_fully_supported from . import are_symlinks_supported, are_hardlinks_supported, are_fifos_supported, is_utime_fully_supported, is_birthtime_fully_supported
from .platform import fakeroot_detected from .platform import fakeroot_detected
@ -798,7 +793,7 @@ class ArchiverTestCase(ArchiverTestCaseBase):
requires_hardlinks = pytest.mark.skipif(not are_hardlinks_supported(), reason='hardlinks not supported') requires_hardlinks = pytest.mark.skipif(not are_hardlinks_supported(), reason='hardlinks not supported')
@requires_hardlinks @requires_hardlinks
@unittest.skipUnless(has_llfuse, 'llfuse not installed') @unittest.skipUnless(llfuse, 'llfuse not installed')
def test_fuse_mount_hardlinks(self): def test_fuse_mount_hardlinks(self):
self._extract_hardlinks_setup() self._extract_hardlinks_setup()
mountpoint = os.path.join(self.tmpdir, 'mountpoint') mountpoint = os.path.join(self.tmpdir, 'mountpoint')
@ -1661,7 +1656,7 @@ class ArchiverTestCase(ArchiverTestCaseBase):
# verify that command works with read-only repo when using --bypass-lock # verify that command works with read-only repo when using --bypass-lock
self.cmd('list', self.repository_location, '--bypass-lock') self.cmd('list', self.repository_location, '--bypass-lock')
@unittest.skipUnless(has_llfuse, 'llfuse not installed') @unittest.skipUnless(llfuse, 'llfuse not installed')
def test_readonly_mount(self): def test_readonly_mount(self):
self.cmd('init', '--encryption=repokey', self.repository_location) self.cmd('init', '--encryption=repokey', self.repository_location)
self.create_src_archive('test') self.create_src_archive('test')
@ -1754,7 +1749,7 @@ class ArchiverTestCase(ArchiverTestCaseBase):
# delete of the whole repository ignores features # delete of the whole repository ignores features
self.cmd('delete', self.repository_location) self.cmd('delete', self.repository_location)
@unittest.skipUnless(has_llfuse, 'llfuse not installed') @unittest.skipUnless(llfuse, 'llfuse not installed')
def test_unknown_feature_on_mount(self): def test_unknown_feature_on_mount(self):
self.cmd('init', '--encryption=repokey', self.repository_location) self.cmd('init', '--encryption=repokey', self.repository_location)
self.cmd('create', self.repository_location + '::test', 'input') self.cmd('create', self.repository_location + '::test', 'input')
@ -2322,7 +2317,7 @@ class ArchiverTestCase(ArchiverTestCaseBase):
assert 'positional arguments' not in self.cmd('help', 'init', '--epilog-only') assert 'positional arguments' not in self.cmd('help', 'init', '--epilog-only')
assert 'This command initializes' not in self.cmd('help', 'init', '--usage-only') assert 'This command initializes' not in self.cmd('help', 'init', '--usage-only')
@unittest.skipUnless(has_llfuse, 'llfuse not installed') @unittest.skipUnless(llfuse, 'llfuse not installed')
def test_fuse(self): def test_fuse(self):
def has_noatime(some_file): def has_noatime(some_file):
atime_before = os.stat(some_file).st_atime_ns atime_before = os.stat(some_file).st_atime_ns
@ -2423,7 +2418,7 @@ class ArchiverTestCase(ArchiverTestCaseBase):
else: else:
raise raise
@unittest.skipUnless(has_llfuse, 'llfuse not installed') @unittest.skipUnless(llfuse, 'llfuse not installed')
def test_fuse_versions_view(self): def test_fuse_versions_view(self):
self.cmd('init', '--encryption=repokey', self.repository_location) self.cmd('init', '--encryption=repokey', self.repository_location)
self.create_regular_file('test', contents=b'first') self.create_regular_file('test', contents=b'first')
@ -2455,7 +2450,7 @@ class ArchiverTestCase(ArchiverTestCaseBase):
assert os.stat(hl2).st_ino == os.stat(hl3).st_ino assert os.stat(hl2).st_ino == os.stat(hl3).st_ino
assert open(hl3, 'rb').read() == b'123456' assert open(hl3, 'rb').read() == b'123456'
@unittest.skipUnless(has_llfuse, 'llfuse not installed') @unittest.skipUnless(llfuse, 'llfuse not installed')
def test_fuse_allow_damaged_files(self): def test_fuse_allow_damaged_files(self):
self.cmd('init', '--encryption=repokey', self.repository_location) self.cmd('init', '--encryption=repokey', self.repository_location)
self.create_src_archive('archive') self.create_src_archive('archive')
@ -2480,7 +2475,7 @@ class ArchiverTestCase(ArchiverTestCaseBase):
with self.fuse_mount(self.repository_location + '::archive', mountpoint, '-o', 'allow_damaged_files'): with self.fuse_mount(self.repository_location + '::archive', mountpoint, '-o', 'allow_damaged_files'):
open(os.path.join(mountpoint, path)).close() open(os.path.join(mountpoint, path)).close()
@unittest.skipUnless(has_llfuse, 'llfuse not installed') @unittest.skipUnless(llfuse, 'llfuse not installed')
def test_fuse_mount_options(self): def test_fuse_mount_options(self):
self.cmd('init', '--encryption=repokey', self.repository_location) self.cmd('init', '--encryption=repokey', self.repository_location)
self.create_src_archive('arch11') self.create_src_archive('arch11')
@ -2502,7 +2497,7 @@ class ArchiverTestCase(ArchiverTestCaseBase):
with self.fuse_mount(self.repository_location, mountpoint, '--prefix=nope'): with self.fuse_mount(self.repository_location, mountpoint, '--prefix=nope'):
assert sorted(os.listdir(os.path.join(mountpoint))) == [] assert sorted(os.listdir(os.path.join(mountpoint))) == []
@unittest.skipUnless(has_llfuse, 'llfuse not installed') @unittest.skipUnless(llfuse, 'llfuse not installed')
def test_migrate_lock_alive(self): def test_migrate_lock_alive(self):
"""Both old_id and new_id must not be stale during lock migration / daemonization.""" """Both old_id and new_id must not be stale during lock migration / daemonization."""
from functools import wraps from functools import wraps

19
tox.ini
View File

@ -2,16 +2,29 @@
# fakeroot -u tox --recreate # fakeroot -u tox --recreate
[tox] [tox]
envlist = py{36,37,38,39},flake8 envlist = py{36,37,38,39}-fuse{2,3}, flake8
[testenv] [testenv]
deps = deps =
-rrequirements.d/development.txt -rrequirements.d/development.txt
-rrequirements.d/fuse.txt
commands = py.test -v -n {env:XDISTN:1} -rs --cov=borg --cov-config=.coveragerc --benchmark-skip --pyargs {posargs:borg.testsuite} commands = py.test -v -n {env:XDISTN:1} -rs --cov=borg --cov-config=.coveragerc --benchmark-skip --pyargs {posargs:borg.testsuite}
# fakeroot -u needs some env vars: # fakeroot -u needs some env vars:
passenv = * passenv = *
[testenv:py{36,37,38,39}-fuse2]
setenv =
BORG_FUSE_IMPL=llfuse
deps =
llfuse
{[testenv]deps}
[testenv:py{36,37,38,39}-fuse3]
setenv =
BORG_FUSE_IMPL=pyfuse3
deps =
pyfuse3
{[testenv]deps}
[testenv:flake8] [testenv:flake8]
changedir = changedir =
deps = deps =