mirror of https://github.com/borgbackup/borg.git
Append-only mode for repositories
New repository config item, repository.append_only, causes Borg to never delete or append to existing data files. Hints and indices are handled as before, old ones are deleted, because they can get quite large, but are automatically reconstructed: no need to keep them. When append_only is activated a file /path/to/repo/transactions/<NUMBER> will be created for every commit. Deleting all segments <NUMBER+1> and higher will rollback to that commit. Note that this only influences Borg behaviour. Since repository config can't be altered remotely (except for repository.key) this can't be disabled when accessed remotely over SSH with "borg serve" as the forced command. This is only a feature to support the use case, and does not replace appropriate file system permissions or monitoring. Resolves #809
This commit is contained in:
parent
bf208479d7
commit
ffc8cf6c97
|
@ -84,6 +84,7 @@ class Repository:
|
||||||
config.set('repository', 'version', '1')
|
config.set('repository', 'version', '1')
|
||||||
config.set('repository', 'segments_per_dir', str(self.DEFAULT_SEGMENTS_PER_DIR))
|
config.set('repository', 'segments_per_dir', str(self.DEFAULT_SEGMENTS_PER_DIR))
|
||||||
config.set('repository', 'max_segment_size', str(self.DEFAULT_MAX_SEGMENT_SIZE))
|
config.set('repository', 'max_segment_size', str(self.DEFAULT_MAX_SEGMENT_SIZE))
|
||||||
|
config.set('repository', 'append_only', '0')
|
||||||
config.set('repository', 'id', hexlify(os.urandom(32)).decode('ascii'))
|
config.set('repository', 'id', hexlify(os.urandom(32)).decode('ascii'))
|
||||||
self.save_config(path, config)
|
self.save_config(path, config)
|
||||||
|
|
||||||
|
@ -105,6 +106,8 @@ class Repository:
|
||||||
def destroy(self):
|
def destroy(self):
|
||||||
"""Destroy the repository at `self.path`
|
"""Destroy the repository at `self.path`
|
||||||
"""
|
"""
|
||||||
|
if self.append_only:
|
||||||
|
raise ValueError(self.path + " is in append-only mode")
|
||||||
self.close()
|
self.close()
|
||||||
os.remove(os.path.join(self.path, 'config')) # kill config first
|
os.remove(os.path.join(self.path, 'config')) # kill config first
|
||||||
shutil.rmtree(self.path)
|
shutil.rmtree(self.path)
|
||||||
|
@ -148,6 +151,7 @@ class Repository:
|
||||||
raise self.InvalidRepository(path)
|
raise self.InvalidRepository(path)
|
||||||
self.max_segment_size = self.config.getint('repository', 'max_segment_size')
|
self.max_segment_size = self.config.getint('repository', 'max_segment_size')
|
||||||
self.segments_per_dir = self.config.getint('repository', 'segments_per_dir')
|
self.segments_per_dir = self.config.getint('repository', 'segments_per_dir')
|
||||||
|
self.append_only = self.config.getboolean('repository', 'append_only', fallback=False)
|
||||||
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)
|
||||||
|
|
||||||
|
@ -163,7 +167,8 @@ class Repository:
|
||||||
"""Commit transaction
|
"""Commit transaction
|
||||||
"""
|
"""
|
||||||
self.io.write_commit()
|
self.io.write_commit()
|
||||||
self.compact_segments(save_space=save_space)
|
if not self.append_only:
|
||||||
|
self.compact_segments(save_space=save_space)
|
||||||
self.write_index()
|
self.write_index()
|
||||||
self.rollback()
|
self.rollback()
|
||||||
|
|
||||||
|
@ -211,6 +216,11 @@ class Repository:
|
||||||
self.index.write(os.path.join(self.path, 'index.tmp'))
|
self.index.write(os.path.join(self.path, 'index.tmp'))
|
||||||
os.rename(os.path.join(self.path, 'index.tmp'),
|
os.rename(os.path.join(self.path, 'index.tmp'),
|
||||||
os.path.join(self.path, 'index.%d' % transaction_id))
|
os.path.join(self.path, 'index.%d' % transaction_id))
|
||||||
|
if self.append_only:
|
||||||
|
transaction_log = os.path.join(self.path, 'transactions')
|
||||||
|
if not os.path.exists(transaction_log):
|
||||||
|
os.mkdir(transaction_log)
|
||||||
|
open(os.path.join(transaction_log, str(transaction_id)), 'w').close()
|
||||||
# Remove old indices
|
# Remove old indices
|
||||||
current = '.%d' % transaction_id
|
current = '.%d' % transaction_id
|
||||||
for name in os.listdir(self.path):
|
for name in os.listdir(self.path):
|
||||||
|
|
|
@ -187,6 +187,34 @@ class RepositoryCommitTestCase(RepositoryTestCaseBase):
|
||||||
self.assert_equal(len(self.repository), 3)
|
self.assert_equal(len(self.repository), 3)
|
||||||
|
|
||||||
|
|
||||||
|
class RepositoryAppendOnlyTestCase(RepositoryTestCaseBase):
|
||||||
|
def test_destroy_append_only(self):
|
||||||
|
# Can't destroy append only repo (via the API)
|
||||||
|
self.repository.append_only = True
|
||||||
|
with self.assert_raises(ValueError):
|
||||||
|
self.repository.destroy()
|
||||||
|
|
||||||
|
def test_append_only(self):
|
||||||
|
def segments_in_repository():
|
||||||
|
return len(list(self.repository.io.segment_iterator()))
|
||||||
|
self.repository.put(b'00000000000000000000000000000000', b'foo')
|
||||||
|
self.repository.commit()
|
||||||
|
|
||||||
|
self.repository.append_only = False
|
||||||
|
assert segments_in_repository() == 1
|
||||||
|
self.repository.put(b'00000000000000000000000000000000', b'foo')
|
||||||
|
self.repository.commit()
|
||||||
|
# normal: compact squashes the data together, only one segment
|
||||||
|
assert segments_in_repository() == 1
|
||||||
|
|
||||||
|
self.repository.append_only = True
|
||||||
|
assert segments_in_repository() == 1
|
||||||
|
self.repository.put(b'00000000000000000000000000000000', b'foo')
|
||||||
|
self.repository.commit()
|
||||||
|
# append only: does not compact, only new segments written
|
||||||
|
assert segments_in_repository() == 2
|
||||||
|
|
||||||
|
|
||||||
class RepositoryCheckTestCase(RepositoryTestCaseBase):
|
class RepositoryCheckTestCase(RepositoryTestCaseBase):
|
||||||
|
|
||||||
def list_indices(self):
|
def list_indices(self):
|
||||||
|
|
Loading…
Reference in New Issue