Browse Source

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.
Lukas Fleischer 7 years ago
parent
commit
0943b322e3

+ 2 - 0
docs/internals/frontends.rst

@@ -499,6 +499,8 @@ Errors
         Insufficient free space to complete transaction (required: {}, available: {}).
         Insufficient free space to complete transaction (required: {}, available: {}).
     Repository.InvalidRepository
     Repository.InvalidRepository
         {} is not a valid repository. Check repo config.
         {} is not a valid repository. Check repo config.
+    Repository.AtticRepository
+        Attic repository detected. Please run "borg upgrade {}".
     Repository.ObjectNotFound
     Repository.ObjectNotFound
         Object with key {} not found in repository {}.
         Object with key {} not found in repository {}.
 
 

+ 5 - 0
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)')
                     raise IntegrityError('(not available)')
                 else:
                 else:
                     raise IntegrityError(args[0].decode())
                     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':
             elif error == 'PathNotAllowed':
                 if old_server:
                 if old_server:
                     raise PathNotAllowed('(unknown)')
                     raise PathNotAllowed('(unknown)')

+ 18 - 1
src/borg/repository.py

@@ -30,6 +30,8 @@ logger = create_logger(__name__)
 
 
 MAGIC = b'BORG_SEG'
 MAGIC = b'BORG_SEG'
 MAGIC_LEN = len(MAGIC)
 MAGIC_LEN = len(MAGIC)
+ATTIC_MAGIC = b'ATTICSEG'
+assert len(ATTIC_MAGIC) == MAGIC_LEN
 TAG_PUT = 0
 TAG_PUT = 0
 TAG_DELETE = 1
 TAG_DELETE = 1
 TAG_COMMIT = 2
 TAG_COMMIT = 2
@@ -116,6 +118,9 @@ class Repository:
     class InvalidRepository(Error):
     class InvalidRepository(Error):
         """{} is not a valid repository. Check repo config."""
         """{} is not a valid repository. Check repo config."""
 
 
+    class AtticRepository(Error):
+        """Attic repository detected. Please run "borg upgrade {}"."""
+
     class CheckNeeded(ErrorWithTraceback):
     class CheckNeeded(ErrorWithTraceback):
         """Inconsistency detected. Please run "borg check {}"."""
         """Inconsistency detected. Please run "borg check {}"."""
 
 
@@ -134,7 +139,7 @@ class Repository:
         """The storage quota ({}) has been exceeded ({}). Try deleting some archives."""
         """The storage quota ({}) has been exceeded ({}). Try deleting some archives."""
 
 
     def __init__(self, path, create=False, exclusive=False, lock_wait=None, lock=True,
     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.path = os.path.abspath(path)
         self._location = Location('file://%s' % self.path)
         self._location = Location('file://%s' % self.path)
         self.io = None  # type: LoggedIO
         self.io = None  # type: LoggedIO
@@ -154,6 +159,7 @@ class Repository:
         self.storage_quota = storage_quota
         self.storage_quota = storage_quota
         self.storage_quota_use = 0
         self.storage_quota_use = 0
         self.transaction_doomed = None
         self.transaction_doomed = None
+        self.check_segment_magic = check_segment_magic
 
 
     def __del__(self):
     def __del__(self):
         if self.lock:
         if self.lock:
@@ -370,6 +376,12 @@ class Repository:
             self.storage_quota = self.config.getint('repository', 'storage_quota', fallback=0)
             self.storage_quota = self.config.getint('repository', 'storage_quota', fallback=0)
         self.id = unhexlify(self.config.get('repository', 'id').strip())
         self.id = unhexlify(self.config.get('repository', 'id').strip())
         self.io = LoggedIO(self.path, self.max_segment_size, self.segments_per_dir)
         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):
     def close(self):
         if self.lock:
         if self.lock:
@@ -1245,6 +1257,11 @@ class LoggedIO:
     def segment_size(self, segment):
     def segment_size(self, segment):
         return os.path.getsize(self.segment_filename(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):
     def iter_objects(self, segment, offset=0, include_data=False, read_data=True):
         """
         """
         Return object iterator for *segment*.
         Return object iterator for *segment*.

+ 22 - 0
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 BaseTestCase, changedir, environment_variable, no_selinux
 from . import are_symlinks_supported, are_hardlinks_supported, are_fifos_supported, is_utime_fully_supported
 from . import are_symlinks_supported, are_hardlinks_supported, are_fifos_supported, is_utime_fully_supported
 from .platform import fakeroot_detected
 from .platform import fakeroot_detected
+from .upgrader import attic_repo
 from . import key
 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/aaaa').st_nlink == 2
             assert os.stat('input/dir1/source2').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')
 @unittest.skipUnless('binary' in BORG_EXES, 'no borg.exe available')
 class ArchiverTestCaseBinary(ArchiverTestCase):
 class ArchiverTestCaseBinary(ArchiverTestCase):

+ 4 - 4
src/borg/testsuite/upgrader.py

@@ -85,8 +85,8 @@ def test_convert_segments(attic_repo, inplace):
     :param attic_repo: a populated attic repository (fixture)
     :param attic_repo: a populated attic repository (fixture)
     """
     """
     repo_path = attic_repo
     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)
     repository = AtticRepositoryUpgrader(repo_path, create=False)
     with repository:
     with repository:
         segments = [filename for i, filename in repository.io.segment_iterator()]
         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
     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):
     def stat_segment(path):
         return os.stat(os.path.join(path, 'data', '0', '0'))
         return os.stat(os.path.join(path, 'data', '0', '0'))

+ 1 - 0
src/borg/upgrader.py

@@ -19,6 +19,7 @@ ATTIC_MAGIC = b'ATTICSEG'
 class AtticRepositoryUpgrader(Repository):
 class AtticRepositoryUpgrader(Repository):
     def __init__(self, *args, **kw):
     def __init__(self, *args, **kw):
         kw['lock'] = False  # do not create borg lock files (now) in attic repo
         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)
         super().__init__(*args, **kw)
 
 
     def upgrade(self, dryrun=True, inplace=False, progress=False):
     def upgrade(self, dryrun=True, inplace=False, progress=False):