diff --git a/plinth/modules/backups/__init__.py b/plinth/modules/backups/__init__.py index e1cc6b4b1..ab8bb0023 100644 --- a/plinth/modules/backups/__init__.py +++ b/plinth/modules/backups/__init__.py @@ -40,6 +40,8 @@ managed_packages = ['borgbackup', 'sshfs'] name = _('Backups') +depends = ['storage'] + description = [ _('Backups allows creating and managing backup archives.'), ] @@ -48,8 +50,8 @@ manual_page = 'Backups' MANIFESTS_FOLDER = '/var/lib/plinth/backups-manifests/' ROOT_REPOSITORY = '/var/lib/freedombox/borgbackup' -ROOT_REPOSITORY_NAME = format_lazy( - _('{box_name} storage'), box_name=cfg.box_name) +ROOT_REPOSITORY_NAME = format_lazy(_('{box_name} storage'), + box_name=cfg.box_name) ROOT_REPOSITORY_UUID = 'root' # session variable name that stores when a backup file should be deleted SESSION_PATH_VARIABLE = 'fbx-backups-upload-path' @@ -110,9 +112,8 @@ def _backup_handler(packet, encryption_passphrase=None): arguments = ['create-archive', '--path', packet.path, '--paths'] + paths input_data = '' if encryption_passphrase: - input_data = json.dumps({ - 'encryption_passphrase': encryption_passphrase - }) + input_data = json.dumps( + {'encryption_passphrase': encryption_passphrase}) actions.superuser_run('backups', arguments, input=input_data.encode()) diff --git a/plinth/modules/backups/forms.py b/plinth/modules/backups/forms.py index 431082589..0fe391fc3 100644 --- a/plinth/modules/backups/forms.py +++ b/plinth/modules/backups/forms.py @@ -29,6 +29,7 @@ from django.core.validators import (FileExtensionValidator, from django.utils.translation import ugettext from django.utils.translation import ugettext_lazy as _ +from plinth.modules.storage import get_disks from plinth.utils import format_lazy from . import ROOT_REPOSITORY_NAME, api, network_storage, split_path @@ -124,18 +125,8 @@ def repository_validator(path): raise ValidationError(_(f'Invalid directory path: {dir_path}')) -class AddRepositoryForm(forms.Form): - """Form to add new SSH remote repository.""" - repository = forms.CharField( - label=_('SSH Repository Path'), strip=True, - help_text=_('Path of a new or existing repository. Example: ' - 'user@host:~/path/to/repo/'), - validators=[repository_validator]) - ssh_password = forms.CharField( - label=_('SSH server password'), strip=True, - help_text=_('Password of the SSH Server.
' - 'SSH key-based authentication is not yet possible.'), - widget=forms.PasswordInput(), required=False) +class EncryptedBackupsMixin(forms.Form): + """Form to add a new backup repository.""" encryption = forms.ChoiceField( label=_('Encryption'), help_text=format_lazy( _('"Key in Repository" means that a ' @@ -150,7 +141,7 @@ class AddRepositoryForm(forms.Form): widget=forms.PasswordInput(), required=False) def clean(self): - super(AddRepositoryForm, self).clean() + super().clean() passphrase = self.cleaned_data.get('encryption_passphrase') confirm_passphrase = self.cleaned_data.get( 'confirm_encryption_passphrase') @@ -165,6 +156,44 @@ class AddRepositoryForm(forms.Form): return self.cleaned_data + +encryption_fields = [ + 'encryption', 'encryption_passphrase', 'confirm_encryption_passphrase' +] + + +def get_disk_choices(): + """Returns a list of all available partitions except the root partition.""" + return [(device['mount_point'], + device['label'] if device['label'] else device['mount_point']) + for device in get_disks() if device['mount_point'] != '/'] + + +class AddRepositoryForm(EncryptedBackupsMixin, forms.Form): + """Form to create a new backups repository on a disk.""" + disk = forms.ChoiceField( + label=_('Select Disk or Partition'), help_text=format_lazy( + _('Backups will be stored in the directory FreedomBoxBackups')), + choices=get_disk_choices) + + field_order = ['disk'] + encryption_fields + + +class AddRemoteRepositoryForm(EncryptedBackupsMixin, forms.Form): + """Form to add new SSH remote repository.""" + repository = forms.CharField( + label=_('SSH Repository Path'), strip=True, + help_text=_('Path of a new or existing repository. Example: ' + 'user@host:~/path/to/repo/'), + validators=[repository_validator]) + ssh_password = forms.CharField( + label=_('SSH server password'), strip=True, + help_text=_('Password of the SSH Server.
' + 'SSH key-based authentication is not yet possible.'), + widget=forms.PasswordInput(), required=False) + + field_order = ['repository', 'ssh_password'] + encryption_fields + def clean_repository(self): """Validate repository form field.""" path = self.cleaned_data.get('repository') @@ -202,10 +231,8 @@ class VerifySshHostkeyForm(forms.Form): stderr=subprocess.DEVNULL) keys = keyscan.stdout.decode().splitlines() # Generate user-friendly fingerprints of public keys - keygen = subprocess.run( - ['ssh-keygen', '-l', '-f', '-'], - input=keyscan.stdout, - stdout=subprocess.PIPE) + keygen = subprocess.run(['ssh-keygen', '-l', '-f', '-'], + input=keyscan.stdout, stdout=subprocess.PIPE) fingerprints = keygen.stdout.decode().splitlines() return zip(keys, fingerprints) diff --git a/plinth/modules/backups/templates/backups.html b/plinth/modules/backups/templates/backups.html index e67fb0042..39c851792 100644 --- a/plinth/modules/backups/templates/backups.html +++ b/plinth/modules/backups/templates/backups.html @@ -66,13 +66,18 @@ {% include "backups_repository.inc" with editable=True %} {% endfor %} -
+ + + {% trans 'Add Backup Location' %} + + href="{% url 'backups:add-remote-repository' %}"> - {% trans 'Add Remote Location' %} + {% trans 'Add Remote Backup Location' %} {% endblock %} diff --git a/plinth/modules/backups/templates/backups_repository_add.html b/plinth/modules/backups/templates/backups_add_remote_repository.html similarity index 100% rename from plinth/modules/backups/templates/backups_repository_add.html rename to plinth/modules/backups/templates/backups_add_remote_repository.html diff --git a/plinth/modules/backups/templates/backups_add_repository.html b/plinth/modules/backups/templates/backups_add_repository.html new file mode 100644 index 000000000..2aab6fa3c --- /dev/null +++ b/plinth/modules/backups/templates/backups_add_repository.html @@ -0,0 +1,40 @@ +{% extends "base.html" %} +{% comment %} +# +# This file is part of FreedomBox. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +{% endcomment %} + +{% load bootstrap %} +{% load i18n %} + +{% block content %} + +

{{ title }}

+ +
+ {% csrf_token %} + + {{ form|bootstrap }} + + + + {% trans "Cancel" %} + +
+ +{% endblock %} diff --git a/plinth/modules/backups/tests/test_ssh_remotes.py b/plinth/modules/backups/tests/test_ssh_remotes.py index 417909f2d..14b5581e4 100644 --- a/plinth/modules/backups/tests/test_ssh_remotes.py +++ b/plinth/modules/backups/tests/test_ssh_remotes.py @@ -99,7 +99,7 @@ def test_add_repository_when_directory_is_missing(temp_user, temp_home, 'encryption': 'none' } # TODO test the view instead of the form - form = forms.AddRepositoryForm(data=data) + form = forms.AddRemoteRepositoryForm(data=data) form.is_valid() assert os.path.isdir(repo_path) # Directory gets created @@ -115,7 +115,7 @@ def test_add_repository_when_directory_exists_and_empty( 'encryption': 'none' } # TODO test the view instead of the form - form = forms.AddRepositoryForm(data=data) + form = forms.AddRemoteRepositoryForm(data=data) form.is_valid() @@ -131,6 +131,6 @@ def test_add_repository_when_directory_exists_and_not_empty( 'ssh_password': password, 'encryption': 'none' } - form = forms.AddRepositoryForm(data=data) + form = forms.AddRemoteRepositoryForm(data=data) with pytest.raises(ValidationError): form.is_valid() diff --git a/plinth/modules/backups/urls.py b/plinth/modules/backups/urls.py index 13f6351e7..757f808b4 100644 --- a/plinth/modules/backups/urls.py +++ b/plinth/modules/backups/urls.py @@ -20,11 +20,11 @@ URLs for the backups module. from django.conf.urls import url -from .views import (AddRepositoryView, CreateArchiveView, DeleteArchiveView, - DownloadArchiveView, IndexView, RemoveRepositoryView, - RestoreArchiveView, RestoreFromUploadView, - UploadArchiveView, VerifySshHostkeyView, mount_repository, - umount_repository) +from .views import (AddRemoteRepositoryView, AddRepositoryView, + CreateArchiveView, DeleteArchiveView, DownloadArchiveView, + IndexView, RemoveRepositoryView, RestoreArchiveView, + RestoreFromUploadView, UploadArchiveView, + VerifySshHostkeyView, mount_repository, umount_repository) urlpatterns = [ url(r'^sys/backups/$', IndexView.as_view(), name='index'), @@ -39,7 +39,9 @@ urlpatterns = [ url(r'^sys/backups/restore-from-upload/$', RestoreFromUploadView.as_view(), name='restore-from-upload'), url(r'^sys/backups/repositories/add$', AddRepositoryView.as_view(), - name='repository-add'), + name='add-repository'), + url(r'^sys/backups/repositories/add-remote$', + AddRemoteRepositoryView.as_view(), name='add-remote-repository'), url(r'^sys/backups/repositories/(?P[^/]+)/ssh-verify/$', VerifySshHostkeyView.as_view(), name='verify-ssh-hostkey'), url(r'^sys/backups/repositories/(?P[^/]+)/delete/$', diff --git a/plinth/modules/backups/views.py b/plinth/modules/backups/views.py index 5678316f0..a9b82b7f1 100644 --- a/plinth/modules/backups/views.py +++ b/plinth/modules/backups/views.py @@ -234,20 +234,40 @@ class DownloadArchiveView(View): def get(self, request, uuid, name): repository = get_repository(uuid) - filename = '%s.tar.gz' % name + filename = f'{name}.tar.gz' - response = StreamingHttpResponse( - repository.get_download_stream(name), - content_type='application/gzip') + response = StreamingHttpResponse(repository.get_download_stream(name), + content_type='application/gzip') response['Content-Disposition'] = 'attachment; filename="%s"' % \ filename return response class AddRepositoryView(SuccessMessageMixin, FormView): - """View to create a new remote backup repository.""" + """View to create a new backup repository.""" form_class = forms.AddRepositoryForm - template_name = 'backups_repository_add.html' + template_name = 'backups_add_repository.html' + success_url = reverse_lazy('backups:index') + + def get_context_data(self, **kwargs): + """Return additional context for rendering the template.""" + context = super().get_context_data(**kwargs) + context['title'] = _('Create backup repository') + return context + + def form_valid(self, form): + """Create and save a Borg repository.""" + path = pathlib.Path( + form.cleaned_data.get('disk')) / 'FreedomBoxBackups' + encryption_passphrase = form.cleaned_data.get('encryption_passphrase') + if form.cleaned_data.get('encryption') == 'none': + encryption_passphrase = None + + +class AddRemoteRepositoryView(SuccessMessageMixin, FormView): + """View to create a new remote backup repository.""" + form_class = forms.AddRemoteRepositoryForm + template_name = 'backups_add_remote_repository.html' def get_context_data(self, **kwargs): """Return additional context for rendering the template.""" @@ -378,7 +398,7 @@ class VerifySshHostkeyView(SuccessMessageMixin, FormView): # Delete the repository so that the user can have another go at # creating it. network_storage.delete(uuid) - return redirect(reverse_lazy('backups:repository-add')) + return redirect(reverse_lazy('backups:add-remote-repository')) def _list_remote_directory(path, credentials):