mirror of
https://github.com/borgbackup/borg.git
synced 2025-01-01 12:45:34 +00:00
fuse: EIO on damaged files unless told not to (-o allow_damaged_files)
This commit is contained in:
parent
ad1be9dcd4
commit
cb4a52eb84
4 changed files with 66 additions and 11 deletions
|
@ -1267,6 +1267,13 @@ def build_parser(self, args=None, prog=None):
|
|||
browsing an archive or restoring individual files. Unless the ``--foreground``
|
||||
option is given the command will run in the background until the filesystem
|
||||
is ``umounted``.
|
||||
|
||||
For mount options, see the fuse(8) manual page. Additional mount options
|
||||
supported by borg:
|
||||
|
||||
- allow_damaged_files: by default damaged files (where missing chunks were
|
||||
replaced with runs of zeros by borg check --repair) are not readable and
|
||||
return EIO (I/O error). Set this option to read such files.
|
||||
""")
|
||||
subparser = subparsers.add_parser('mount', parents=[common_parser],
|
||||
description=self.do_mount.__doc__,
|
||||
|
|
25
borg/fuse.py
25
borg/fuse.py
|
@ -6,11 +6,16 @@
|
|||
import stat
|
||||
import tempfile
|
||||
import time
|
||||
from .archive import Archive
|
||||
from .helpers import daemonize, bigint_to_int
|
||||
from distutils.version import LooseVersion
|
||||
|
||||
import msgpack
|
||||
|
||||
from .archive import Archive
|
||||
from .helpers import daemonize, bigint_to_int, remove_surrogates
|
||||
from .logger import create_logger
|
||||
logger = create_logger()
|
||||
|
||||
|
||||
# Does this version of llfuse support ns precision?
|
||||
have_fuse_xtime_ns = hasattr(llfuse.EntryAttributes, 'st_mtime_ns')
|
||||
|
||||
|
@ -42,6 +47,8 @@ def get(self, inode):
|
|||
class FuseOperations(llfuse.Operations):
|
||||
"""Export archive as a fuse filesystem
|
||||
"""
|
||||
allow_damaged_files = True
|
||||
|
||||
def __init__(self, key, repository, manifest, archive, cached_repo):
|
||||
super().__init__()
|
||||
self._inode_count = 0
|
||||
|
@ -225,6 +232,15 @@ def lookup(self, parent_inode, name, ctx=None):
|
|||
return self.getattr(inode)
|
||||
|
||||
def open(self, inode, flags, ctx=None):
|
||||
if not self.allow_damaged_files:
|
||||
item = self.get_item(inode)
|
||||
if b'chunks_healthy' in item:
|
||||
# Processed archive items don't carry the path anymore; for converting the inode
|
||||
# to the path we'd either have to store the inverse of the current structure,
|
||||
# or search the entire archive. So we just don't print it. It's easy to correlate anyway.
|
||||
logger.warning('File has damaged (all-zero) chunks. Try running borg check --repair. '
|
||||
'Mount with allow_damaged_files to read damaged files.')
|
||||
raise llfuse.FUSEError(errno.EIO)
|
||||
return inode
|
||||
|
||||
def opendir(self, inode, ctx=None):
|
||||
|
@ -261,6 +277,11 @@ def mount(self, mountpoint, extra_options, foreground=False):
|
|||
options = ['fsname=borgfs', 'ro']
|
||||
if extra_options:
|
||||
options.extend(extra_options.split(','))
|
||||
try:
|
||||
options.remove('allow_damaged_files')
|
||||
self.allow_damaged_files = True
|
||||
except ValueError:
|
||||
self.allow_damaged_files = False
|
||||
llfuse.init(self, mountpoint, options)
|
||||
if not foreground:
|
||||
daemonize()
|
||||
|
|
|
@ -94,9 +94,12 @@ def _assert_dirs_equal_cmp(self, diff):
|
|||
self._assert_dirs_equal_cmp(sub_diff)
|
||||
|
||||
@contextmanager
|
||||
def fuse_mount(self, location, mountpoint):
|
||||
def fuse_mount(self, location, mountpoint, mount_options=None):
|
||||
os.mkdir(mountpoint)
|
||||
self.cmd('mount', location, mountpoint, fork=True)
|
||||
args = ['mount', location, mountpoint]
|
||||
if mount_options:
|
||||
args += '-o', mount_options
|
||||
self.cmd(*args, fork=True)
|
||||
self.wait_for_mount(mountpoint)
|
||||
yield
|
||||
if sys.platform.startswith('linux'):
|
||||
|
|
|
@ -234,6 +234,13 @@ def cmd(self, *args, **kw):
|
|||
def create_src_archive(self, name):
|
||||
self.cmd('create', self.repository_location + '::' + name, src_dir)
|
||||
|
||||
def open_archive(self, name):
|
||||
repository = Repository(self.repository_path)
|
||||
with repository:
|
||||
manifest, key = Manifest.load(repository)
|
||||
archive = Archive(repository, key, manifest, name)
|
||||
return archive, repository
|
||||
|
||||
|
||||
class ArchiverTestCase(ArchiverTestCaseBase):
|
||||
|
||||
|
@ -1037,6 +1044,30 @@ def test_fuse(self):
|
|||
sto = os.stat(out_fn)
|
||||
assert stat.S_ISFIFO(sto.st_mode)
|
||||
|
||||
@unittest.skipUnless(has_llfuse, 'llfuse not installed')
|
||||
def test_fuse_allow_damaged_files(self):
|
||||
self.cmd('init', self.repository_location)
|
||||
self.create_src_archive('archive')
|
||||
# Get rid of a chunk and repair it
|
||||
archive, repository = self.open_archive('archive')
|
||||
with repository:
|
||||
for item in archive.iter_items():
|
||||
if item[b'path'].endswith('testsuite/archiver.py'):
|
||||
repository.delete(item[b'chunks'][-1][0])
|
||||
path = item[b'path'] # store full path for later
|
||||
break
|
||||
else:
|
||||
assert False # missed the file
|
||||
repository.commit()
|
||||
self.cmd('check', '--repair', self.repository_location, exit_code=0)
|
||||
|
||||
mountpoint = os.path.join(self.tmpdir, 'mountpoint')
|
||||
with self.fuse_mount(self.repository_location + '::archive', mountpoint):
|
||||
with pytest.raises(OSError):
|
||||
open(os.path.join(mountpoint, path))
|
||||
with self.fuse_mount(self.repository_location + '::archive', mountpoint, 'allow_damaged_files'):
|
||||
open(os.path.join(mountpoint, path)).close()
|
||||
|
||||
def verify_aes_counter_uniqueness(self, method):
|
||||
seen = set() # Chunks already seen
|
||||
used = set() # counter values already used
|
||||
|
@ -1117,13 +1148,6 @@ def setUp(self):
|
|||
self.create_src_archive('archive1')
|
||||
self.create_src_archive('archive2')
|
||||
|
||||
def open_archive(self, name):
|
||||
repository = Repository(self.repository_path)
|
||||
with repository:
|
||||
manifest, key = Manifest.load(repository)
|
||||
archive = Archive(repository, key, manifest, name)
|
||||
return archive, repository
|
||||
|
||||
def test_check_usage(self):
|
||||
output = self.cmd('check', '-v', self.repository_location, exit_code=0)
|
||||
self.assert_in('Starting repository check', output)
|
||||
|
|
Loading…
Reference in a new issue