mirror of
https://github.com/borgbackup/borg.git
synced 2024-12-25 01:06:50 +00:00
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.
This commit is contained in:
parent
a1840e9238
commit
0943b322e3
6 changed files with 52 additions and 5 deletions
|
@ -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 {}.
|
||||||
|
|
||||||
|
|
|
@ -726,6 +726,11 @@ def handle_error(unpacked):
|
||||||
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)')
|
||||||
|
|
|
@ -30,6 +30,8 @@
|
||||||
|
|
||||||
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 AlreadyExists(Error):
|
||||||
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 StorageQuotaExceeded(Error):
|
||||||
"""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 @@ def __init__(self, path, create=False, exclusive=False, lock_wait=None, lock=Tru
|
||||||
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 @@ def open(self, path, exclusive, lock_wait=None, lock=True):
|
||||||
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 @@ def segment_exists(self, segment):
|
||||||
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*.
|
||||||
|
|
|
@ -55,6 +55,7 @@
|
||||||
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 @@ def test_extract_hardlinks(self):
|
||||||
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):
|
||||||
|
|
|
@ -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
|
with pytest.raises(Repository.AtticRepository):
|
||||||
assert not repo_valid(repo_path)
|
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
|
with pytest.raises(Repository.AtticRepository):
|
||||||
assert not repo_valid(repo_path)
|
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'))
|
||||||
|
|
|
@ -19,6 +19,7 @@
|
||||||
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):
|
||||||
|
|
Loading…
Reference in a new issue