mirror of
https://github.com/freedombox/FreedomBox.git
synced 2026-04-22 10:01:45 +00:00
backups: Allow adding backup repositories on multiple disks
Signed-off-by: Joseph Nuthalapati <njoseph@riseup.net> [sunil@medhas.org Fix showing form choices, undo blank line removals] [sunil@medhas.org Fix typo in tooltip for add repo button] Signed-off-by: Sunil Mohan Adapa <sunil@medhas.org> Reviewed-by: Sunil Mohan Adapa <sunil@medhas.org> Reviewed-by: James Valleroy <jvalleroy@mailbox.org>
This commit is contained in:
parent
5fbc3fc31f
commit
8a93b5b90c
@ -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())
|
||||
|
||||
|
||||
@ -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: '
|
||||
'<i>user@host:~/path/to/repo/</i>'),
|
||||
validators=[repository_validator])
|
||||
ssh_password = forms.CharField(
|
||||
label=_('SSH server password'), strip=True,
|
||||
help_text=_('Password of the SSH Server.<br />'
|
||||
'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: '
|
||||
'<i>user@host:~/path/to/repo/</i>'),
|
||||
validators=[repository_validator])
|
||||
ssh_password = forms.CharField(
|
||||
label=_('SSH server password'), strip=True,
|
||||
help_text=_('Password of the SSH Server.<br />'
|
||||
'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)
|
||||
|
||||
@ -66,13 +66,18 @@
|
||||
{% include "backups_repository.inc" with editable=True %}
|
||||
{% endfor %}
|
||||
|
||||
<br/>
|
||||
<a title="{% trans 'Add a backup location' %}"
|
||||
role="button" class="btn btn-default"
|
||||
href="{% url 'backups:add-repository' %}">
|
||||
<span class="fa fa-plus" aria-hidden="true"></span>
|
||||
{% trans 'Add Backup Location' %}
|
||||
</a>
|
||||
|
||||
<a title="{% trans 'Add a remote backup location' %}"
|
||||
role="button" class="btn btn-default"
|
||||
href="{% url 'backups:repository-add' %}">
|
||||
href="{% url 'backups:add-remote-repository' %}">
|
||||
<span class="fa fa-plus" aria-hidden="true"></span>
|
||||
{% trans 'Add Remote Location' %}
|
||||
{% trans 'Add Remote Backup Location' %}
|
||||
</a>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
40
plinth/modules/backups/templates/backups_add_repository.html
Normal file
40
plinth/modules/backups/templates/backups_add_repository.html
Normal file
@ -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 <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
{% endcomment %}
|
||||
|
||||
{% load bootstrap %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<h3>{{ title }}</h3>
|
||||
|
||||
<form class="form" method="post">
|
||||
{% csrf_token %}
|
||||
|
||||
{{ form|bootstrap }}
|
||||
|
||||
<input type="submit" class="btn btn-primary"
|
||||
value="{% trans "Create Repository" %}"/>
|
||||
<a class="btn btn-default" role="button" href="{% url 'backups:index' %}">
|
||||
{% trans "Cancel" %}
|
||||
</a>
|
||||
</form>
|
||||
|
||||
{% endblock %}
|
||||
@ -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()
|
||||
|
||||
@ -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<uuid>[^/]+)/ssh-verify/$',
|
||||
VerifySshHostkeyView.as_view(), name='verify-ssh-hostkey'),
|
||||
url(r'^sys/backups/repositories/(?P<uuid>[^/]+)/delete/$',
|
||||
|
||||
@ -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):
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user