fuse: EIO on damaged files unless told not to (-o allow_damaged_files)

This commit is contained in:
Marian Beermann 2016-07-10 02:19:27 +02:00
parent ad1be9dcd4
commit cb4a52eb84
No known key found for this signature in database
GPG Key ID: 9B8450B91D1362C1
4 changed files with 66 additions and 11 deletions

View File

@ -1267,6 +1267,13 @@ class Archiver:
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__,

View File

@ -6,11 +6,16 @@ import os
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 @@ class ItemCache:
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 @@ class FuseOperations(llfuse.Operations):
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 @@ class FuseOperations(llfuse.Operations):
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()

View File

@ -94,9 +94,12 @@ class BaseTestCase(unittest.TestCase):
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'):

View File

@ -234,6 +234,13 @@ class ArchiverTestCaseBase(BaseTestCase):
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 @@ class ArchiverTestCase(ArchiverTestCaseBase):
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 @@ class ArchiverCheckTestCase(ArchiverTestCaseBase):
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)