Skip to content

Commit

Permalink
Append-only mode for repositories
Browse files Browse the repository at this point in the history
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 borgbackup#809

TODO:
- tests
- docs
  • Loading branch information
enkore committed Mar 31, 2016
1 parent 5a0f75d commit 205e901
Show file tree
Hide file tree
Showing 2 changed files with 30 additions and 1 deletion.
12 changes: 11 additions & 1 deletion borg/repository.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ def create(self, path):
config.set('repository', 'version', '1')
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', 'append_only', '0')
config.set('repository', 'id', hexlify(os.urandom(32)).decode('ascii'))
self.save_config(path, config)

Expand All @@ -120,6 +121,8 @@ def load_key(self):
def destroy(self):
"""Destroy the repository at `self.path`
"""
if self.append_only:
raise ValueError(self.path + " is in append-only mode")
self.close()
os.remove(os.path.join(self.path, 'config')) # kill config first
shutil.rmtree(self.path)
Expand Down Expand Up @@ -163,6 +166,7 @@ def open(self, path, exclusive, lock_wait=None, lock=True):
raise self.InvalidRepository(path)
self.max_segment_size = self.config.getint('repository', 'max_segment_size')
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.io = LoggedIO(self.path, self.max_segment_size, self.segments_per_dir)

Expand All @@ -178,7 +182,8 @@ def commit(self, save_space=False):
"""Commit transaction
"""
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.rollback()

Expand Down Expand Up @@ -226,6 +231,11 @@ def write_index(self):
self.index.write(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))
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
current = '.%d' % transaction_id
for name in os.listdir(self.path):
Expand Down
19 changes: 19 additions & 0 deletions borg/testsuite/repository.py
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,25 @@ def test_crash_before_deleting_compacted_segments(self):
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):
self.repository.put(b'00000000000000000000000000000000', b'foo')
self.repository.commit()
self.repository.append_only = True
original_segment = self.repository.io.get_latest_segment()
self.repository.put(b'00000000000000000000000000000000', b'foo')
self.repository.commit()
# append only: does not compact, only new segments written
latest_segment = self.repository.io.get_latest_segment()
assert latest_segment == original_segment + 1


class RepositoryCheckTestCase(RepositoryTestCaseBase):

def list_indices(self):
Expand Down

0 comments on commit 205e901

Please sign in to comment.