mirror of
https://github.com/borgbackup/borg.git
synced 2025-02-21 21:57:36 +00:00
Cache: Wipe cache if compatibility is not sure
Add detection of possibly incompatible combinations of the borg versions maintaining the cache and the featues used.
This commit is contained in:
parent
005068dd6d
commit
b8ad8b84da
2 changed files with 65 additions and 1 deletions
|
@ -15,8 +15,9 @@
|
||||||
from .hashindex import ChunkIndex, ChunkIndexEntry
|
from .hashindex import ChunkIndex, ChunkIndexEntry
|
||||||
from .helpers import Location
|
from .helpers import Location
|
||||||
from .helpers import Error
|
from .helpers import Error
|
||||||
|
from .helpers import Manifest
|
||||||
from .helpers import get_cache_dir, get_security_dir
|
from .helpers import get_cache_dir, get_security_dir
|
||||||
from .helpers import int_to_bigint, bigint_to_int, bin_to_hex
|
from .helpers import int_to_bigint, bigint_to_int, bin_to_hex, parse_stringified_list
|
||||||
from .helpers import format_file_size
|
from .helpers import format_file_size
|
||||||
from .helpers import safe_ns
|
from .helpers import safe_ns
|
||||||
from .helpers import yes, hostname_is_unique
|
from .helpers import yes, hostname_is_unique
|
||||||
|
@ -257,6 +258,8 @@ def load(self):
|
||||||
self.manifest_id = unhexlify(self._config.get('cache', 'manifest'))
|
self.manifest_id = unhexlify(self._config.get('cache', 'manifest'))
|
||||||
self.timestamp = self._config.get('cache', 'timestamp', fallback=None)
|
self.timestamp = self._config.get('cache', 'timestamp', fallback=None)
|
||||||
self.key_type = self._config.get('cache', 'key_type', fallback=None)
|
self.key_type = self._config.get('cache', 'key_type', fallback=None)
|
||||||
|
self.ignored_features = set(parse_stringified_list(self._config.get('cache', 'ignored_features', fallback='')))
|
||||||
|
self.mandatory_features = set(parse_stringified_list(self._config.get('cache', 'mandatory_features', fallback='')))
|
||||||
try:
|
try:
|
||||||
self.integrity = dict(self._config.items('integrity'))
|
self.integrity = dict(self._config.items('integrity'))
|
||||||
if self._config.get('cache', 'manifest') != self.integrity.pop('manifest'):
|
if self._config.get('cache', 'manifest') != self.integrity.pop('manifest'):
|
||||||
|
@ -281,6 +284,8 @@ def save(self, manifest=None, key=None):
|
||||||
if manifest:
|
if manifest:
|
||||||
self._config.set('cache', 'manifest', manifest.id_str)
|
self._config.set('cache', 'manifest', manifest.id_str)
|
||||||
self._config.set('cache', 'timestamp', manifest.timestamp)
|
self._config.set('cache', 'timestamp', manifest.timestamp)
|
||||||
|
self._config.set('cache', 'ignored_features', ','.join(self.ignored_features))
|
||||||
|
self._config.set('cache', 'mandatory_features', ','.join(self.mandatory_features))
|
||||||
if not self._config.has_section('integrity'):
|
if not self._config.has_section('integrity'):
|
||||||
self._config.add_section('integrity')
|
self._config.add_section('integrity')
|
||||||
for file, integrity_data in self.integrity.items():
|
for file, integrity_data in self.integrity.items():
|
||||||
|
@ -370,6 +375,12 @@ def __init__(self, repository, key, manifest, path=None, sync=True, do_files=Fal
|
||||||
self.open()
|
self.open()
|
||||||
try:
|
try:
|
||||||
self.security_manager.assert_secure(manifest, key, cache_config=self.cache_config)
|
self.security_manager.assert_secure(manifest, key, cache_config=self.cache_config)
|
||||||
|
|
||||||
|
if not self.check_cache_compatibility():
|
||||||
|
self.wipe_cache()
|
||||||
|
|
||||||
|
self.update_compatibility()
|
||||||
|
|
||||||
if sync and self.manifest.id != self.cache_config.manifest_id:
|
if sync and self.manifest.id != self.cache_config.manifest_id:
|
||||||
self.sync()
|
self.sync()
|
||||||
self.commit()
|
self.commit()
|
||||||
|
@ -678,6 +689,43 @@ def legacy_cleanup():
|
||||||
self.do_cache = os.path.isdir(archive_path)
|
self.do_cache = os.path.isdir(archive_path)
|
||||||
self.chunks = create_master_idx(self.chunks)
|
self.chunks = create_master_idx(self.chunks)
|
||||||
|
|
||||||
|
def check_cache_compatibility(self):
|
||||||
|
my_features = Manifest.SUPPORTED_REPO_FEATURES
|
||||||
|
if self.cache_config.ignored_features & my_features:
|
||||||
|
# The cache might not contain references of chunks that need a feature that is mandatory for some operation
|
||||||
|
# and which this version supports. To avoid corruption while executing that operation force rebuild.
|
||||||
|
return False
|
||||||
|
if not self.cache_config.mandatory_features <= my_features:
|
||||||
|
# The cache was build with consideration to at least one feature that this version does not understand.
|
||||||
|
# This client might misinterpret the cache. Thus force a rebuild.
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
def wipe_cache(self):
|
||||||
|
logger.warning("Discarding incompatible cache and forcing a cache rebuild")
|
||||||
|
archive_path = os.path.join(self.path, 'chunks.archive.d')
|
||||||
|
if os.path.isdir(archive_path):
|
||||||
|
shutil.rmtree(os.path.join(self.path, 'chunks.archive.d'))
|
||||||
|
os.makedirs(os.path.join(self.path, 'chunks.archive.d'))
|
||||||
|
self.chunks = ChunkIndex()
|
||||||
|
with open(os.path.join(self.path, 'files'), 'wb'):
|
||||||
|
pass # empty file
|
||||||
|
self.cache_config.manifest_id = ''
|
||||||
|
self.cache_config._config.set('cache', 'manifest', '')
|
||||||
|
|
||||||
|
self.cache_config.ignored_features = set()
|
||||||
|
self.cache_config.mandatory_features = set()
|
||||||
|
|
||||||
|
def update_compatibility(self):
|
||||||
|
operation_to_features_map = self.manifest.get_all_mandatory_features()
|
||||||
|
my_features = Manifest.SUPPORTED_REPO_FEATURES
|
||||||
|
repo_features = set()
|
||||||
|
for operation, features in operation_to_features_map.items():
|
||||||
|
repo_features.update(features)
|
||||||
|
|
||||||
|
self.cache_config.ignored_features.update(repo_features - my_features)
|
||||||
|
self.cache_config.mandatory_features.update(repo_features & my_features)
|
||||||
|
|
||||||
def add_chunk(self, id, chunk, stats, overwrite=False, wait=True):
|
def add_chunk(self, id, chunk, stats, overwrite=False, wait=True):
|
||||||
if not self.txn_active:
|
if not self.txn_active:
|
||||||
self.begin_txn()
|
self.begin_txn()
|
||||||
|
|
|
@ -325,6 +325,17 @@ def check_repository_compatibility(self, operations):
|
||||||
if unsupported:
|
if unsupported:
|
||||||
raise MandatoryFeatureUnsupported([f.decode() for f in unsupported])
|
raise MandatoryFeatureUnsupported([f.decode() for f in unsupported])
|
||||||
|
|
||||||
|
def get_all_mandatory_features(self):
|
||||||
|
result = {}
|
||||||
|
feature_flags = self.config.get(b'feature_flags', None)
|
||||||
|
if feature_flags is None:
|
||||||
|
return result
|
||||||
|
|
||||||
|
for operation, requirements in feature_flags.items():
|
||||||
|
if b'mandatory' in requirements:
|
||||||
|
result[operation.decode()] = set([feature.decode() for feature in requirements[b'mandatory']])
|
||||||
|
return result
|
||||||
|
|
||||||
def write(self):
|
def write(self):
|
||||||
from .item import ManifestItem
|
from .item import ManifestItem
|
||||||
if self.key.tam_required:
|
if self.key.tam_required:
|
||||||
|
@ -823,6 +834,11 @@ def bin_to_hex(binary):
|
||||||
return hexlify(binary).decode('ascii')
|
return hexlify(binary).decode('ascii')
|
||||||
|
|
||||||
|
|
||||||
|
def parse_stringified_list(s):
|
||||||
|
l = re.split(" *, *", s)
|
||||||
|
return [item for item in l if item != '']
|
||||||
|
|
||||||
|
|
||||||
class Location:
|
class Location:
|
||||||
"""Object representing a repository / archive location
|
"""Object representing a repository / archive location
|
||||||
"""
|
"""
|
||||||
|
|
Loading…
Reference in a new issue