From 0943b322e3ee286e7672859fe995506c589bcb4e Mon Sep 17 00:00:00 2001 From: Lukas Fleischer Date: Sun, 6 Aug 2017 16:46:08 +0200 Subject: [PATCH] Detect non-upgraded Attic repositories When opening a repository, always try to read the magic number of the latest segment and compare it to the Attic segment magic (unless the repository is opened for upgrading). If an Attic segment is detected, raise a dedicated exception, telling the user to upgrade the repository first. Fixes #1933. --- docs/internals/frontends.rst | 2 ++ src/borg/remote.py | 5 +++++ src/borg/repository.py | 19 ++++++++++++++++++- src/borg/testsuite/archiver.py | 22 ++++++++++++++++++++++ src/borg/testsuite/upgrader.py | 8 ++++---- src/borg/upgrader.py | 1 + 6 files changed, 52 insertions(+), 5 deletions(-) diff --git a/docs/internals/frontends.rst b/docs/internals/frontends.rst index c41d427eb..677db738d 100644 --- a/docs/internals/frontends.rst +++ b/docs/internals/frontends.rst @@ -499,6 +499,8 @@ Errors Insufficient free space to complete transaction (required: {}, available: {}). Repository.InvalidRepository {} is not a valid repository. Check repo config. + Repository.AtticRepository + Attic repository detected. Please run "borg upgrade {}". Repository.ObjectNotFound Object with key {} not found in repository {}. diff --git a/src/borg/remote.py b/src/borg/remote.py index 1cc455c2e..3e0942dde 100644 --- a/src/borg/remote.py +++ b/src/borg/remote.py @@ -726,6 +726,11 @@ This problem will go away as soon as the server has been upgraded to 1.0.7+. raise IntegrityError('(not available)') else: raise IntegrityError(args[0].decode()) + elif error == 'AtticRepository': + if old_server: + raise Repository.AtticRepository('(not available)') + else: + raise Repository.AtticRepository(args[0].decode()) elif error == 'PathNotAllowed': if old_server: raise PathNotAllowed('(unknown)') diff --git a/src/borg/repository.py b/src/borg/repository.py index b443bdb4e..ab4734838 100644 --- a/src/borg/repository.py +++ b/src/borg/repository.py @@ -30,6 +30,8 @@ logger = create_logger(__name__) MAGIC = b'BORG_SEG' MAGIC_LEN = len(MAGIC) +ATTIC_MAGIC = b'ATTICSEG' +assert len(ATTIC_MAGIC) == MAGIC_LEN TAG_PUT = 0 TAG_DELETE = 1 TAG_COMMIT = 2 @@ -116,6 +118,9 @@ class Repository: class InvalidRepository(Error): """{} is not a valid repository. Check repo config.""" + class AtticRepository(Error): + """Attic repository detected. Please run "borg upgrade {}".""" + class CheckNeeded(ErrorWithTraceback): """Inconsistency detected. Please run "borg check {}".""" @@ -134,7 +139,7 @@ class Repository: """The storage quota ({}) has been exceeded ({}). Try deleting some archives.""" def __init__(self, path, create=False, exclusive=False, lock_wait=None, lock=True, - append_only=False, storage_quota=None): + append_only=False, storage_quota=None, check_segment_magic=True): self.path = os.path.abspath(path) self._location = Location('file://%s' % self.path) self.io = None # type: LoggedIO @@ -154,6 +159,7 @@ class Repository: self.storage_quota = storage_quota self.storage_quota_use = 0 self.transaction_doomed = None + self.check_segment_magic = check_segment_magic def __del__(self): if self.lock: @@ -370,6 +376,12 @@ class Repository: self.storage_quota = self.config.getint('repository', 'storage_quota', fallback=0) self.id = unhexlify(self.config.get('repository', 'id').strip()) self.io = LoggedIO(self.path, self.max_segment_size, self.segments_per_dir) + if self.check_segment_magic: + # read a segment and check whether we are dealing with a non-upgraded Attic repository + segment = self.io.get_latest_segment() + if segment is not None and self.io.get_segment_magic(segment) == ATTIC_MAGIC: + self.close() + raise self.AtticRepository(path) def close(self): if self.lock: @@ -1245,6 +1257,11 @@ class LoggedIO: def segment_size(self, segment): return os.path.getsize(self.segment_filename(segment)) + def get_segment_magic(self, segment): + fd = self.get_fd(segment) + fd.seek(0) + return fd.read(MAGIC_LEN) + def iter_objects(self, segment, offset=0, include_data=False, read_data=True): """ Return object iterator for *segment*. diff --git a/src/borg/testsuite/archiver.py b/src/borg/testsuite/archiver.py index 0d28eb310..ac7eacf5a 100644 --- a/src/borg/testsuite/archiver.py +++ b/src/borg/testsuite/archiver.py @@ -55,6 +55,7 @@ from . import has_lchflags, has_llfuse from . import BaseTestCase, changedir, environment_variable, no_selinux from . import are_symlinks_supported, are_hardlinks_supported, are_fifos_supported, is_utime_fully_supported from .platform import fakeroot_detected +from .upgrader import attic_repo from . import key @@ -2725,6 +2726,27 @@ id: 2 / e29442 3506da 4e1ea7 / 25f62a 5a3d41 - 02 assert os.stat('input/dir1/aaaa').st_nlink == 2 assert os.stat('input/dir1/source2').st_nlink == 2 + def test_detect_attic_repo(self): + path = attic_repo(self.repository_path) + cmds = [ + ['create', path + '::test', self.tmpdir], + ['extract', path + '::test'], + ['check', path], + ['rename', path + '::test', 'newname'], + ['list', path], + ['delete', path], + ['prune', path], + ['info', path + '::test'], + ['mount', path, self.tmpdir], + ['key', 'export', path, 'exported'], + ['key', 'import', path, 'import'], + ['change-passphrase', path], + ['break-lock', path], + ] + for args in cmds: + output = self.cmd(*args, fork=True, exit_code=2) + assert 'Attic repository detected.' in output + @unittest.skipUnless('binary' in BORG_EXES, 'no borg.exe available') class ArchiverTestCaseBinary(ArchiverTestCase): diff --git a/src/borg/testsuite/upgrader.py b/src/borg/testsuite/upgrader.py index 3fd7500c3..08c0693bc 100644 --- a/src/borg/testsuite/upgrader.py +++ b/src/borg/testsuite/upgrader.py @@ -85,8 +85,8 @@ def test_convert_segments(attic_repo, inplace): :param attic_repo: a populated attic repository (fixture) """ repo_path = attic_repo - # check should fail because of magic number - assert not repo_valid(repo_path) + with pytest.raises(Repository.AtticRepository): + repo_valid(repo_path) repository = AtticRepositoryUpgrader(repo_path, create=False) with repository: segments = [filename for i, filename in repository.io.segment_iterator()] @@ -149,8 +149,8 @@ def test_convert_all(attic_repo, attic_key_file, inplace): """ repo_path = attic_repo - # check should fail because of magic number - assert not repo_valid(repo_path) + with pytest.raises(Repository.AtticRepository): + repo_valid(repo_path) def stat_segment(path): return os.stat(os.path.join(path, 'data', '0', '0')) diff --git a/src/borg/upgrader.py b/src/borg/upgrader.py index 0b92ce8e2..1044f649e 100644 --- a/src/borg/upgrader.py +++ b/src/borg/upgrader.py @@ -19,6 +19,7 @@ ATTIC_MAGIC = b'ATTICSEG' class AtticRepositoryUpgrader(Repository): def __init__(self, *args, **kw): kw['lock'] = False # do not create borg lock files (now) in attic repo + kw['check_segment_magic'] = False # skip the Attic check when upgrading super().__init__(*args, **kw) def upgrade(self, dryrun=True, inplace=False, progress=False):