1
0
Fork 0
mirror of https://github.com/borgbackup/borg.git synced 2024-12-28 10:49:16 +00:00

Merge pull request #1303 from enkore/issue/1302

fuse: EIO on damaged files unless told not to (-o allow_damaged_files)
This commit is contained in:
enkore 2016-07-10 17:43:54 +02:00 committed by GitHub
commit 2639b5d0ff
4 changed files with 89 additions and 31 deletions

View file

@ -1268,6 +1268,13 @@ def build_parser(self, args=None, prog=None):
browsing an archive or restoring individual files. Unless the ``--foreground`` browsing an archive or restoring individual files. Unless the ``--foreground``
option is given the command will run in the background until the filesystem option is given the command will run in the background until the filesystem
is ``umounted``. 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], subparser = subparsers.add_parser('mount', parents=[common_parser],
description=self.do_mount.__doc__, description=self.do_mount.__doc__,

View file

@ -6,10 +6,15 @@
import stat import stat
import tempfile import tempfile
import time import time
from distutils.version import LooseVersion
import msgpack
from .archive import Archive from .archive import Archive
from .helpers import daemonize, bigint_to_int from .helpers import daemonize, bigint_to_int
from distutils.version import LooseVersion from .logger import create_logger
import msgpack logger = create_logger()
# Does this version of llfuse support ns precision? # Does this version of llfuse support ns precision?
have_fuse_xtime_ns = hasattr(llfuse.EntryAttributes, 'st_mtime_ns') have_fuse_xtime_ns = hasattr(llfuse.EntryAttributes, 'st_mtime_ns')
@ -42,6 +47,9 @@ def get(self, inode):
class FuseOperations(llfuse.Operations): class FuseOperations(llfuse.Operations):
"""Export archive as a fuse filesystem """Export archive as a fuse filesystem
""" """
allow_damaged_files = False
def __init__(self, key, repository, manifest, archive, cached_repo): def __init__(self, key, repository, manifest, archive, cached_repo):
super().__init__() super().__init__()
self._inode_count = 0 self._inode_count = 0
@ -68,6 +76,32 @@ def __init__(self, key, repository, manifest, archive, cached_repo):
self.contents[1][os.fsencode(archive_name)] = archive_inode self.contents[1][os.fsencode(archive_name)] = archive_inode
self.pending_archives[archive_inode] = Archive(repository, key, manifest, archive_name) self.pending_archives[archive_inode] = Archive(repository, key, manifest, archive_name)
def mount(self, mountpoint, mount_options, foreground=False):
"""Mount filesystem on *mountpoint* with *mount_options*."""
options = ['fsname=borgfs', 'ro']
if mount_options:
options.extend(mount_options.split(','))
try:
options.remove('allow_damaged_files')
self.allow_damaged_files = True
except ValueError:
pass
llfuse.init(self, mountpoint, options)
if not foreground:
daemonize()
# If the file system crashes, we do not want to umount because in that
# case the mountpoint suddenly appears to become empty. This can have
# nasty consequences, imagine the user has e.g. an active rsync mirror
# job - seeing the mountpoint empty, rsync would delete everything in the
# mirror.
umount = False
try:
signal = fuse_main()
umount = (signal is None) # no crash and no signal -> umount request
finally:
llfuse.close(umount)
def process_archive(self, archive, prefix=[]): def process_archive(self, archive, prefix=[]):
"""Build fuse inode hierarchy from archive metadata """Build fuse inode hierarchy from archive metadata
""" """
@ -225,6 +259,15 @@ def lookup(self, parent_inode, name, ctx=None):
return self.getattr(inode) return self.getattr(inode)
def open(self, inode, flags, ctx=None): 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 return inode
def opendir(self, inode, ctx=None): def opendir(self, inode, ctx=None):
@ -256,23 +299,3 @@ def readdir(self, fh, off):
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[b'source']) return os.fsencode(item[b'source'])
def mount(self, mountpoint, extra_options, foreground=False):
options = ['fsname=borgfs', 'ro']
if extra_options:
options.extend(extra_options.split(','))
llfuse.init(self, mountpoint, options)
if not foreground:
daemonize()
# If the file system crashes, we do not want to umount because in that
# case the mountpoint suddenly appears to become empty. This can have
# nasty consequences, imagine the user has e.g. an active rsync mirror
# job - seeing the mountpoint empty, rsync would delete everything in the
# mirror.
umount = False
try:
signal = fuse_main()
umount = (signal is None) # no crash and no signal -> umount request
finally:
llfuse.close(umount)

View file

@ -94,9 +94,12 @@ def _assert_dirs_equal_cmp(self, diff):
self._assert_dirs_equal_cmp(sub_diff) self._assert_dirs_equal_cmp(sub_diff)
@contextmanager @contextmanager
def fuse_mount(self, location, mountpoint): def fuse_mount(self, location, mountpoint, mount_options=None):
os.mkdir(mountpoint) 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) self.wait_for_mount(mountpoint)
yield yield
if sys.platform.startswith('linux'): if sys.platform.startswith('linux'):

View file

@ -234,6 +234,13 @@ def cmd(self, *args, **kw):
def create_src_archive(self, name): def create_src_archive(self, name):
self.cmd('create', self.repository_location + '::' + name, src_dir) 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): class ArchiverTestCase(ArchiverTestCaseBase):
@ -1037,6 +1044,31 @@ def test_fuse(self):
sto = os.stat(out_fn) sto = os.stat(out_fn)
assert stat.S_ISFIFO(sto.st_mode) 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) as excinfo:
open(os.path.join(mountpoint, path))
assert excinfo.value.errno == errno.EIO
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): def verify_aes_counter_uniqueness(self, method):
seen = set() # Chunks already seen seen = set() # Chunks already seen
used = set() # counter values already used used = set() # counter values already used
@ -1117,13 +1149,6 @@ def setUp(self):
self.create_src_archive('archive1') self.create_src_archive('archive1')
self.create_src_archive('archive2') 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): def test_check_usage(self):
output = self.cmd('check', '-v', self.repository_location, exit_code=0) output = self.cmd('check', '-v', self.repository_location, exit_code=0)
self.assert_in('Starting repository check', output) self.assert_in('Starting repository check', output)