diff --git a/plinth/modules/backups/forms.py b/plinth/modules/backups/forms.py index 245120711..6d61aa85e 100644 --- a/plinth/modules/backups/forms.py +++ b/plinth/modules/backups/forms.py @@ -23,6 +23,9 @@ from django.core import validators from django.core.validators import FileExtensionValidator from django.utils.translation import ugettext, ugettext_lazy as _ +from plinth.utils import format_lazy +from plinth import cfg + from . import api @@ -77,3 +80,43 @@ class UploadForm(forms.Form): validators=[FileExtensionValidator(['gz'], 'Backup files have to be in .tar.gz format')], help_text=_('Select the backup file you want to upload')) + + +class CreateRepositoryForm(forms.Form): + repository = forms.CharField( + label=_('Repository path'), strip=True, + help_text=_('Path of the new repository.')) + encryption = forms.ChoiceField( + label=_('Encryption'), + help_text=format_lazy(_('"Key in Repository" means that a ' + 'password-protected key is stored with the backup.
' + 'You need this password to restore a backup!')), + choices=[('repokey', 'Key in Repository'), ('none', 'None')] + ) + passphrase = forms.CharField( + label=_('Passphrase'), + help_text=_('Passphrase; Only needed when using encryption.'), + widget=forms.PasswordInput() + ) + confirm_passphrase = forms.CharField( + label=_('Confirm Passphrase'), + help_text=_('Repeat the passphrase.'), + widget=forms.PasswordInput() + ) + store_passphrase = forms.BooleanField( + label=_('Store passphrase on FreedomBox'), + help_text=format_lazy(_('Store the passphrase on your {box_name}.' + '
You need to store the passphrase if you want to run ' + 'recurrent backups.'), box_name=_(cfg.box_name)), + required=False + ) + + def clean(self): + cleaned_data = super(CreateRepositoryForm, self).clean() + passphrase = cleaned_data.get("passphrase") + confirm_passphrase = cleaned_data.get("confirm_passphrase") + + if passphrase != confirm_passphrase: + raise forms.ValidationError( + "passphrase and confirm_passphrase do not match" + ) diff --git a/plinth/modules/backups/templates/backups_repositories.html b/plinth/modules/backups/templates/backups_repositories.html new file mode 100644 index 000000000..b79d6d1dd --- /dev/null +++ b/plinth/modules/backups/templates/backups_repositories.html @@ -0,0 +1,39 @@ +{% 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 i18n %} + +{% block content %} + +

{% trans 'Backup repositories' %}

+ +

+ {% for repository in repositories %} + Repository: {{ repository.repository }}
+ {% endfor %} +

+ + + {% trans 'Add Repository' %} + + +{% endblock %} diff --git a/plinth/modules/backups/urls.py b/plinth/modules/backups/urls.py index de6ec073d..7e0090c03 100644 --- a/plinth/modules/backups/urls.py +++ b/plinth/modules/backups/urls.py @@ -20,9 +20,9 @@ URLs for the backups module. from django.conf.urls import url -from .views import IndexView, CreateArchiveView, DeleteArchiveView, \ - UploadArchiveView, ExportAndDownloadView, RestoreArchiveView, \ - RestoreFromUploadView +from .views import IndexView, CreateArchiveView, CreateRepositoryView, \ + DeleteArchiveView, UploadArchiveView, ExportAndDownloadView, \ + RepositoriesView, RestoreArchiveView, RestoreFromUploadView urlpatterns = [ url(r'^sys/backups/$', IndexView.as_view(), name='index'), @@ -36,4 +36,8 @@ urlpatterns = [ RestoreArchiveView.as_view(), name='restore-archive'), url(r'^sys/backups/restore-from-upload/$', RestoreFromUploadView.as_view(), name='restore-from-upload'), + url(r'^sys/backups/repositories/$', + RepositoriesView.as_view(), name='repositories'), + url(r'^sys/backups/repositories/create/$', + CreateRepositoryView.as_view(), name='create-repository'), ] diff --git a/plinth/modules/backups/views.py b/plinth/modules/backups/views.py index 29ddcd661..4ecfa2ff5 100644 --- a/plinth/modules/backups/views.py +++ b/plinth/modules/backups/views.py @@ -20,11 +20,13 @@ Views for the backups app. from datetime import datetime import gzip +import json from io import BytesIO import logging import mimetypes import os import tempfile +from uuid import uuid1 from urllib.parse import unquote from django.contrib import messages @@ -37,7 +39,7 @@ from django.utils.translation import ugettext as _ from django.utils.translation import ugettext_lazy from django.views.generic import View, FormView, TemplateView -from plinth import actions +from plinth import actions, kvstore from plinth.errors import PlinthError from plinth.modules import backups, storage @@ -51,10 +53,13 @@ subsubmenu = [{ 'text': ugettext_lazy('Backups') }, { 'url': reverse_lazy('backups:upload'), - 'text': ugettext_lazy('Upload backup') + 'text': ugettext_lazy('Upload') }, { 'url': reverse_lazy('backups:create'), - 'text': ugettext_lazy('Create backup') + 'text': ugettext_lazy('Create') +}, { + 'url': reverse_lazy('backups:repositories'), + 'text': ugettext_lazy('Repositories') }] @@ -284,3 +289,49 @@ class ExportAndDownloadView(View): response['Content-Disposition'] = 'attachment; filename="%s"' % \ filename return response + + +class RepositoriesView(TemplateView): + """View list of repositories.""" + template_name = 'backups_repositories.html' + + def get_context_data(self, **kwargs): + """Return additional context for rendering the template.""" + context = super().get_context_data(**kwargs) + context['title'] = 'Backup repositories' + repositories = kvstore.get_default('backups_repositories', []) + context['repositories'] = json.loads(repositories) + context['subsubmenu'] = subsubmenu + return context + + +class CreateRepositoryView(SuccessMessageMixin, FormView): + """View to create a new repository.""" + form_class = forms.CreateRepositoryForm + prefix = 'backups' + template_name = 'backups_form.html' + success_url = reverse_lazy('backups:repositories') + success_message = _('Created new repository.') + + def get_context_data(self, **kwargs): + """Return additional context for rendering the template.""" + context = super().get_context_data(**kwargs) + context['title'] = _('Create new repository') + context['subsubmenu'] = subsubmenu + return context + + def form_valid(self, form): + """Restore files from the archive on valid form submission.""" + repositories = kvstore.get_default('backups_repositories', []) + if repositories: + repositories = json.loads(repositories) + new_repo = { + 'uuid': str(uuid1()), + 'repository': form.cleaned_data['repository'], + 'encryption': form.cleaned_data['encryption'], + } + if form.cleaned_data['store_passphrase']: + new_repo['passphrase'] = form.cleaned_data['passphrase'] + repositories.append(new_repo) + kvstore.set('backups_repositories', json.dumps(repositories)) + return super().form_valid(form)