mirror of
https://github.com/freedombox/FreedomBox.git
synced 2026-05-13 10:30:16 +00:00
backups: Add ability to cleanup files before restoring a backup
- Many times, merging old and new data folders is not ideal and could lead to unexpected outcomes. Perhaps removing all the backup folders and files before restore is ideal. However, this patch tries to introduce that approach slowly on an experimental basis. Tests: - Unit tests work. Signed-off-by: Sunil Mohan Adapa <sunil@medhas.org> Reviewed-by: James Valleroy <jvalleroy@mailbox.org>
This commit is contained in:
parent
c3655d9b7f
commit
ac8dbcfc1c
@ -57,11 +57,21 @@ def _validate_settings(settings):
|
|||||||
assert isinstance(setting, str)
|
assert isinstance(setting, str)
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_paths(paths):
|
||||||
|
"""Validate a list of files or directories."""
|
||||||
|
if not paths:
|
||||||
|
return
|
||||||
|
|
||||||
|
assert isinstance(paths, list)
|
||||||
|
for path in paths:
|
||||||
|
assert isinstance(path, str)
|
||||||
|
|
||||||
|
|
||||||
class BackupRestore(app.FollowerComponent):
|
class BackupRestore(app.FollowerComponent):
|
||||||
"""Component to backup/restore an app."""
|
"""Component to backup/restore an app."""
|
||||||
|
|
||||||
def __init__(self, component_id, config=None, data=None, secrets=None,
|
def __init__(self, component_id, config=None, data=None, secrets=None,
|
||||||
services=None, settings=None):
|
services=None, settings=None, delete_before_restore=None):
|
||||||
"""Initialize the backup/restore component."""
|
"""Initialize the backup/restore component."""
|
||||||
super().__init__(component_id)
|
super().__init__(component_id)
|
||||||
|
|
||||||
@ -75,6 +85,8 @@ class BackupRestore(app.FollowerComponent):
|
|||||||
self.services = services or []
|
self.services = services or []
|
||||||
_validate_settings(settings)
|
_validate_settings(settings)
|
||||||
self.settings = settings or []
|
self.settings = settings or []
|
||||||
|
_validate_paths(delete_before_restore)
|
||||||
|
self.delete_before_restore = delete_before_restore or []
|
||||||
|
|
||||||
self.has_data = (bool(config) or bool(data) or bool(secrets)
|
self.has_data = (bool(config) or bool(data) or bool(secrets)
|
||||||
or bool(settings))
|
or bool(settings))
|
||||||
@ -112,6 +124,9 @@ class BackupRestore(app.FollowerComponent):
|
|||||||
if self.settings:
|
if self.settings:
|
||||||
manifest['settings'] = self.settings
|
manifest['settings'] = self.settings
|
||||||
|
|
||||||
|
if self.delete_before_restore:
|
||||||
|
manifest['delete_before_restore'] = self.delete_before_restore
|
||||||
|
|
||||||
return manifest
|
return manifest
|
||||||
|
|
||||||
def backup_pre(self, packet):
|
def backup_pre(self, packet):
|
||||||
@ -123,6 +138,7 @@ class BackupRestore(app.FollowerComponent):
|
|||||||
|
|
||||||
def restore_pre(self, packet):
|
def restore_pre(self, packet):
|
||||||
"""Perform any special operations before restore."""
|
"""Perform any special operations before restore."""
|
||||||
|
self._files_restore_pre()
|
||||||
|
|
||||||
def restore_post(self, packet):
|
def restore_post(self, packet):
|
||||||
"""Perform any special operations after restore."""
|
"""Perform any special operations after restore."""
|
||||||
@ -151,6 +167,13 @@ class BackupRestore(app.FollowerComponent):
|
|||||||
|
|
||||||
privileged.dump_settings(self.app_id, data)
|
privileged.dump_settings(self.app_id, data)
|
||||||
|
|
||||||
|
def _files_restore_pre(self):
|
||||||
|
"""Delete some files and directories before restoring."""
|
||||||
|
if not self.delete_before_restore:
|
||||||
|
return
|
||||||
|
|
||||||
|
privileged.delete_before_restore(self.app_id)
|
||||||
|
|
||||||
def _settings_restore_post(self):
|
def _settings_restore_post(self):
|
||||||
"""Read from a file and restore keys to kvstore."""
|
"""Read from a file and restore keys to kvstore."""
|
||||||
if not self.settings:
|
if not self.settings:
|
||||||
|
|||||||
@ -6,12 +6,15 @@ import json
|
|||||||
import os
|
import os
|
||||||
import pathlib
|
import pathlib
|
||||||
import re
|
import re
|
||||||
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
import tarfile
|
import tarfile
|
||||||
|
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from plinth import action_utils
|
from plinth import action_utils
|
||||||
|
from plinth import app as app_module
|
||||||
|
from plinth import module_loader
|
||||||
from plinth.actions import privileged, secret_str
|
from plinth.actions import privileged, secret_str
|
||||||
from plinth.utils import Version
|
from plinth.utils import Version
|
||||||
|
|
||||||
@ -481,6 +484,20 @@ def load_settings(app_id: str) -> dict[str, int | float | bool | str]:
|
|||||||
return {}
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
@privileged
|
||||||
|
def delete_before_restore(app_id: str):
|
||||||
|
"""Delete some paths before restoring an app."""
|
||||||
|
module_loader.load_modules()
|
||||||
|
app_module.apps_init()
|
||||||
|
app = app_module.App.get(app_id)
|
||||||
|
|
||||||
|
from plinth.modules.backups.components import BackupRestore
|
||||||
|
components = app.get_components_of_type(BackupRestore)
|
||||||
|
for component in components:
|
||||||
|
for path in component.delete_before_restore:
|
||||||
|
shutil.rmtree(path, ignore_errors=True)
|
||||||
|
|
||||||
|
|
||||||
def _get_env(encryption_passphrase: str | None = None):
|
def _get_env(encryption_passphrase: str | None = None):
|
||||||
"""Create encryption and ssh kwargs out of given arguments."""
|
"""Create encryption and ssh kwargs out of given arguments."""
|
||||||
env = dict(os.environ, BORG_RELOCATED_REPO_ACCESS_IS_OK='yes',
|
env = dict(os.environ, BORG_RELOCATED_REPO_ACCESS_IS_OK='yes',
|
||||||
|
|||||||
@ -21,8 +21,10 @@ def fixture_backup_restore():
|
|||||||
value = {'files': ['a', 'b'], 'directories': ['a', 'b']}
|
value = {'files': ['a', 'b'], 'directories': ['a', 'b']}
|
||||||
services = ['service-1', {'type': 'system', 'name': 'service-2'}]
|
services = ['service-1', {'type': 'system', 'name': 'service-2'}]
|
||||||
settings = ['setting-1', 'setting-2']
|
settings = ['setting-1', 'setting-2']
|
||||||
|
delete_before_restore = ['path1', 'path2']
|
||||||
return BackupRestore('test-backup-restore', config=value, data=value,
|
return BackupRestore('test-backup-restore', config=value, data=value,
|
||||||
secrets=value, services=services, settings=settings)
|
secrets=value, services=services, settings=settings,
|
||||||
|
delete_before_restore=delete_before_restore)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('section', [
|
@pytest.mark.parametrize('section', [
|
||||||
@ -156,6 +158,20 @@ def test_invalid_services(services):
|
|||||||
components._validate_services(services)
|
components._validate_services(services)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('paths', [
|
||||||
|
10,
|
||||||
|
'invalid',
|
||||||
|
[None],
|
||||||
|
[10],
|
||||||
|
[[]],
|
||||||
|
[{}],
|
||||||
|
])
|
||||||
|
def test_invalid_paths(paths):
|
||||||
|
"""Test invalid values for paths."""
|
||||||
|
with pytest.raises(AssertionError):
|
||||||
|
components._validate_paths(paths)
|
||||||
|
|
||||||
|
|
||||||
def test_backup_restore_init_default_arguments():
|
def test_backup_restore_init_default_arguments():
|
||||||
"""Test initialization of the backup restore object."""
|
"""Test initialization of the backup restore object."""
|
||||||
component = BackupRestore('test-backup-restore')
|
component = BackupRestore('test-backup-restore')
|
||||||
@ -230,7 +246,6 @@ def test_backup_restore_hooks(backup_restore):
|
|||||||
"""Test running hooks on backup restore object."""
|
"""Test running hooks on backup restore object."""
|
||||||
packet = None
|
packet = None
|
||||||
backup_restore.backup_post(packet)
|
backup_restore.backup_post(packet)
|
||||||
backup_restore.restore_pre(packet)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
@ -249,6 +264,20 @@ def test_backup_restore_backup_pre(dump_settings, backup_restore):
|
|||||||
dump_settings.assert_has_calls([call('testapp', {'setting-1': 'value-1'})])
|
dump_settings.assert_has_calls([call('testapp', {'setting-1': 'value-1'})])
|
||||||
|
|
||||||
|
|
||||||
|
@patch('plinth.modules.backups.privileged.delete_before_restore')
|
||||||
|
def test_backup_restore_restore_pre(delete_before_restore, backup_restore):
|
||||||
|
"""Test running restore-pre hook."""
|
||||||
|
packet = None
|
||||||
|
backup_restore.app_id = 'testapp'
|
||||||
|
|
||||||
|
component = BackupRestore('test-backup-restore')
|
||||||
|
component.restore_pre(packet)
|
||||||
|
delete_before_restore.assert_has_calls([])
|
||||||
|
|
||||||
|
backup_restore.restore_pre(packet)
|
||||||
|
delete_before_restore.assert_has_calls([call('testapp')])
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
@patch('plinth.modules.backups.privileged.load_settings')
|
@patch('plinth.modules.backups.privileged.load_settings')
|
||||||
def test_backup_restore_restore_post(load_settings, backup_restore):
|
def test_backup_restore_restore_post(load_settings, backup_restore):
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user