diff --git a/actions/backups b/actions/backups index 439ff4252..a8d13118c 100755 --- a/actions/backups +++ b/actions/backups @@ -13,6 +13,7 @@ import sys import tarfile from plinth.modules.backups import MANIFESTS_FOLDER +from plinth.utils import Version TIMEOUT = 30 @@ -38,6 +39,9 @@ def parse_arguments(): help='Create archive') create_archive.add_argument('--paths', help='Paths to include in archive', nargs='+') + create_archive.add_argument('--comment', + help='Comment text to add to archive', + default='') delete_archive = subparsers.add_parser('delete-archive', help='Delete archive') @@ -112,13 +116,32 @@ def subcommand_info(arguments): def subcommand_list_repo(arguments): """List repository contents.""" - run(['borg', 'list', '--json', arguments.path], arguments) + run(['borg', 'list', '--json', '--format="{comment}"', arguments.path], + arguments) + + +def _get_borg_version(arugments): + """Return the version of borgbackup.""" + process = run(['borg', '--version'], arugments, stdout=subprocess.PIPE) + return process.stdout.decode().split()[1] # Example: "borg 1.1.9" def subcommand_create_archive(arguments): """Create archive.""" paths = filter(os.path.exists, arguments.paths) - run(['borg', 'create', '--json', arguments.path] + list(paths), arguments) + command = ['borg', 'create', '--json'] + if arguments.comment: + comment = arguments.comment + if Version(_get_borg_version(arguments)) < Version('1.1.10'): + # Undo any placeholder escape sequences in comments as this version + # of borg does not support placeholders. XXX: Drop this code when + # support for borg < 1.1.10 is dropped. + comment = comment.replace('{{', '{').replace('}}', '}') + + command += ['--comment', comment] + + command += [arguments.path] + list(paths) + run(command, arguments) def subcommand_delete_archive(arguments): diff --git a/plinth/modules/backups/__init__.py b/plinth/modules/backups/__init__.py index 8ec31183e..b0e3cd37c 100644 --- a/plinth/modules/backups/__init__.py +++ b/plinth/modules/backups/__init__.py @@ -85,7 +85,11 @@ def _backup_handler(packet, encryption_passphrase=None): paths = packet.directories + packet.files paths.append(manifest_path) - arguments = ['create-archive', '--path', packet.path, '--paths'] + paths + arguments = ['create-archive', '--path', packet.path] + if packet.archive_comment: + arguments += ['--comment', packet.archive_comment] + + arguments += ['--paths'] + paths input_data = '' if encryption_passphrase: input_data = json.dumps( diff --git a/plinth/modules/backups/api.py b/plinth/modules/backups/api.py index a82035f0c..37c3b6892 100644 --- a/plinth/modules/backups/api.py +++ b/plinth/modules/backups/api.py @@ -41,7 +41,8 @@ class BackupError: class Packet: """Information passed to a handlers for backup/restore operations.""" - def __init__(self, operation, scope, root, components=None, path=None): + def __init__(self, operation, scope, root, components=None, path=None, + archive_comment=None): """Initialize the packet. operation is either 'backup' or 'restore. @@ -62,6 +63,7 @@ class Packet: self.root = root self.components = components self.path = path + self.archive_comment = archive_comment self.errors = [] self.directories = [] @@ -105,8 +107,8 @@ def restore_full(restore_handler): _switch_to_subvolume(subvolume) -def backup_apps(backup_handler, path, app_ids=None, - encryption_passphrase=None): +def backup_apps(backup_handler, path, app_ids=None, encryption_passphrase=None, + archive_comment=None): """Backup data belonging to a set of applications.""" if not app_ids: components = get_all_components_for_backup() @@ -123,7 +125,8 @@ def backup_apps(backup_handler, path, app_ids=None, backup_root = '/' snapshotted = False - packet = Packet('backup', 'apps', backup_root, components, path) + packet = Packet('backup', 'apps', backup_root, components, path, + archive_comment) _run_operation(backup_handler, packet, encryption_passphrase=encryption_passphrase) diff --git a/plinth/modules/backups/repository.py b/plinth/modules/backups/repository.py index 7e9cc7ea2..5245c4274 100644 --- a/plinth/modules/backups/repository.py +++ b/plinth/modules/backups/repository.py @@ -166,12 +166,13 @@ class BaseBorgRepository(abc.ABC): return sorted(archives, key=lambda archive: archive['start'], reverse=True) - def create_archive(self, archive_name, app_ids): + def create_archive(self, archive_name, app_ids, archive_comment=None): """Create a new archive in this repository with given name.""" archive_path = self._get_archive_path(archive_name) passphrase = self.credentials.get('encryption_passphrase', None) api.backup_apps(_backup_handler, path=archive_path, app_ids=app_ids, - encryption_passphrase=passphrase) + encryption_passphrase=passphrase, + archive_comment=archive_comment) def delete_archive(self, archive_name): """Delete an archive with given name from this repository.""" diff --git a/plinth/modules/backups/tests/test_api.py b/plinth/modules/backups/tests/test_api.py index 8cf80b3ce..fe5019311 100644 --- a/plinth/modules/backups/tests/test_api.py +++ b/plinth/modules/backups/tests/test_api.py @@ -63,11 +63,21 @@ def _get_test_app(name): class TestBackupProcesses: """Test cases for backup processes""" + @staticmethod + def test_packet_init(): + """Test that packet is initialized properly.""" + packet = api.Packet('backup', 'apps', '/', []) + assert packet.archive_comment is None + packet = api.Packet('backup', 'apps', '/', [], + archive_comment='test comment') + assert packet.archive_comment == 'test comment' + @staticmethod def test_packet_collected_files_directories(): """Test that directories/files are collected from manifests.""" components = [_get_backup_component('a'), _get_backup_component('b')] - packet = api.Packet('backup', 'apps', '/', components) + packet = api.Packet('backup', 'apps', '/', components, + archive_comment='test comment') for component in components: for section in ['config', 'data', 'secrets']: for directory in getattr(component, section)['directories']: diff --git a/plinth/modules/backups/tests/test_backups.py b/plinth/modules/backups/tests/test_backups.py index 92fcdb63c..d8007cef9 100644 --- a/plinth/modules/backups/tests/test_backups.py +++ b/plinth/modules/backups/tests/test_backups.py @@ -88,18 +88,21 @@ def test_create_export_delete_archive(data_directory, backup_directory): """ repo_name = 'test_create_and_delete' archive_name = 'first_archive' + archive_comment = 'test_archive_comment' path = backup_directory / repo_name repository = BorgRepository(str(path)) repository.initialize() archive_path = "::".join([str(path), archive_name]) actions.superuser_run('backups', [ - 'create-archive', '--path', archive_path, '--paths', + 'create-archive', '--path', archive_path, '--comment', archive_comment, + '--paths', str(data_directory) ]) archive = repository.list_archives()[0] assert archive['name'] == archive_name + assert archive['comment'] == archive_comment repository.delete_archive(archive_name) content = repository.list_archives()