Backup module: Implemented uploading files

Reviewed-by: James Valleroy <jvalleroy@mailbox.org>
This commit is contained in:
Michael Pimmer 2018-09-16 15:15:56 +00:00 committed by James Valleroy
parent 35446f2ca4
commit 7da361bbca
No known key found for this signature in database
GPG Key ID: 77C0C75E7B650808
6 changed files with 138 additions and 16 deletions

View File

@ -45,6 +45,8 @@ service = None
MANIFESTS_FOLDER = '/var/lib/plinth/backups-manifests/' MANIFESTS_FOLDER = '/var/lib/plinth/backups-manifests/'
BACKUP_FOLDER_NAME = 'FreedomBox-backups'
def init(): def init():
"""Intialize the module.""" """Intialize the module."""
@ -106,13 +108,10 @@ def delete_archive(name):
def export_archive(name, location): def export_archive(name, location):
if location[-1] != '/': filepath = get_archive_path(location, get_valid_filename(name) + '.tar.gz')
location += '/' # TODO: that's a full path, not a filename; rename argument
filename = location + 'FreedomBox-backups/' + get_valid_filename(
name) + '.tar.gz'
actions.superuser_run('backups', actions.superuser_run('backups',
['export', '--name', name, '--filename', filename]) ['export', '--name', name, '--filename', filepath])
def get_export_locations(): def get_export_locations():
@ -140,13 +139,17 @@ def get_export_files():
return export_files return export_files
def get_archive_path(location, archive_name):
return os.path.join(location, BACKUP_FOLDER_NAME, archive_name)
def find_exported_archive(disk_label, archive_name): def find_exported_archive(disk_label, archive_name):
"""Return the full path for the exported archive file.""" """Return the full path for the exported archive file."""
locations = get_export_locations() locations = get_export_locations()
for location in locations: for (location_path, location_name) in locations:
if location[1] == disk_label: if location_name == disk_label:
return os.path.join(location[0], 'FreedomBox-backups', return get_archive_path(location_path, archive_name)
archive_name)
raise FileNotFoundError(errno.ENOENT, os.strerror(errno.ENOENT), raise FileNotFoundError(errno.ENOENT, os.strerror(errno.ENOENT),
archive_name) archive_name)

View File

@ -18,12 +18,15 @@
Forms for backups module. Forms for backups module.
""" """
import os
from django import forms from django import forms
from django.core import validators from django.core import validators
from django.core.validators import FileExtensionValidator
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from . import backups as backups_api from . import backups as backups_api
from . import get_export_locations from . import get_export_locations, get_archive_path
class CreateArchiveForm(forms.Form): class CreateArchiveForm(forms.Form):
@ -72,3 +75,35 @@ class RestoreForm(forms.Form):
self.fields['selected_apps'].choices = [ self.fields['selected_apps'].choices = [
(app[0], app[1].name) for app in apps] (app[0], app[1].name) for app in apps]
self.fields['selected_apps'].initial = [app[0] for app in apps] self.fields['selected_apps'].initial = [app[0] for app in apps]
class UploadForm(forms.Form):
location = forms.ChoiceField(
choices=(),
label=_('Location'),
initial='',
widget=forms.Select(),
required=True,
help_text=_('Location to upload the archive to'))
file = forms.FileField(label=_('Upload File'), required=True,
validators=[FileExtensionValidator(['gz'],
'Backup files have to be in .tar.gz format')],
help_text=_('Select the backup file you want to upload'))
def __init__(self, *args, **kwargs):
"""Initialize the form with location choices."""
super().__init__(*args, **kwargs)
# TODO: write a test that assures the format of get_export_locations
self.fields['location'].choices = get_export_locations()
def clean(self):
"""Check that the uploaded file does not exist"""
cleaned_data = super().clean()
file = cleaned_data.get('file')
location = cleaned_data.get('location')
if (file and file.name):
filepath = get_archive_path(location, file.name)
if os.path.exists(filepath):
raise forms.ValidationError("File %s already exists" % file.name)
else:
self.cleaned_data.update({'filepath': filepath})

View File

@ -39,6 +39,8 @@
<p>{{ paragraph|safe }}</p> <p>{{ paragraph|safe }}</p>
{% endfor %} {% endfor %}
<h3>{% trans 'Backup archives' %}</h3>
{% if available_apps %} {% if available_apps %}
<p> <p>
<a title="{% trans 'New backup' %}" <a title="{% trans 'New backup' %}"
@ -63,7 +65,6 @@
</p> </p>
{% endif %} {% endif %}
<h3>{% trans 'Backup archives' %}</h3>
{% if not archives %} {% if not archives %}
<p>{% trans 'No archives currently exist.' %}</p> <p>{% trans 'No archives currently exist.' %}</p>
{% else %} {% else %}
@ -97,9 +98,9 @@
</table> </table>
{% endif %} {% endif %}
<h3>{% trans 'Exported backup archives' %}</h3> <h3>{% trans 'Existing backup files' %}</h3>
{% if not exports %} {% if not exports %}
<p>{% trans 'No exported backup archives were found.' %}</p> <p>{% trans 'No existing backup files were found.' %}</p>
{% else %} {% else %}
<table class="table table-bordered table-condensed table-striped" <table class="table table-bordered table-condensed table-striped"
id="exports-list"> id="exports-list">
@ -134,4 +135,12 @@
</table> </table>
{% endif %} {% endif %}
<p>
<a title="{% trans 'Upload a backup file' %}"
role="button" class="btn btn-primary"
href="{% url 'backups:upload' %}">
{% trans 'Upload backup file' %}
</a>
</p>
{% endblock %} {% endblock %}

View 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 page_head %}
{% endblock %}
{% block content %}
<h3>{{ title }}</h3>
<form class="form" enctype="multipart/form-data" method="post">
{% csrf_token %}
{{ form|bootstrap }}
<input type="submit" class="btn btn-primary"
value="{% trans "Upload file" %}"/>
</form>
{% endblock %}

View File

@ -21,7 +21,7 @@ URLs for the backups module.
from django.conf.urls import url from django.conf.urls import url
from .views import IndexView, CreateArchiveView, DownloadArchiveView, \ from .views import IndexView, CreateArchiveView, DownloadArchiveView, \
ExportArchiveView, DeleteArchiveView, RestoreView DeleteArchiveView, ExportArchiveView, RestoreView, UploadArchiveView
urlpatterns = [ urlpatterns = [
url(r'^sys/backups/$', IndexView.as_view(), name='index'), url(r'^sys/backups/$', IndexView.as_view(), name='index'),
@ -32,6 +32,7 @@ urlpatterns = [
DownloadArchiveView.as_view(), name='download'), DownloadArchiveView.as_view(), name='download'),
url(r'^sys/backups/delete/(?P<name>[^/]+)/$', url(r'^sys/backups/delete/(?P<name>[^/]+)/$',
DeleteArchiveView.as_view(), name='delete'), DeleteArchiveView.as_view(), name='delete'),
url(r'^sys/backups/upload/$', UploadArchiveView.as_view(), name='upload'),
url(r'^sys/backups/restore/(?P<label>[^/]+)/(?P<name>[^/]+)/$', url(r'^sys/backups/restore/(?P<label>[^/]+)/(?P<name>[^/]+)/$',
RestoreView.as_view(), name='restore'), RestoreView.as_view(), name='restore'),
] ]

View File

@ -29,12 +29,22 @@ from django.http import Http404, HttpResponse
from django.shortcuts import redirect from django.shortcuts import redirect
from django.urls import reverse_lazy from django.urls import reverse_lazy
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from django.utils.translation import ugettext_lazy
from django.views.generic import View, FormView, TemplateView from django.views.generic import View, FormView, TemplateView
from plinth.modules import backups from plinth.modules import backups
from . import backups as backups_api, find_exported_archive from . import backups as backups_api, find_exported_archive
from .forms import CreateArchiveForm, ExportArchiveForm, RestoreForm from .forms import CreateArchiveForm, ExportArchiveForm, RestoreForm, UploadForm
subsubmenu = [{
'url': reverse_lazy('backups:index'),
'text': ugettext_lazy('Backups')
}, {
'url': reverse_lazy('backups:upload'),
'text': ugettext_lazy('Upload backup')
}]
class IndexView(TemplateView): class IndexView(TemplateView):
@ -49,6 +59,7 @@ class IndexView(TemplateView):
context['info'] = backups.get_info() context['info'] = backups.get_info()
context['archives'] = backups.list_archives() context['archives'] = backups.list_archives()
context['exports'] = backups.get_export_files() context['exports'] = backups.get_export_files()
context['subsubmenu'] = subsubmenu
apps = backups_api.get_all_apps_for_backup() apps = backups_api.get_all_apps_for_backup()
context['available_apps'] = [app[0] for app in apps] context['available_apps'] = [app[0] for app in apps]
return context return context
@ -66,6 +77,7 @@ class CreateArchiveView(SuccessMessageMixin, FormView):
"""Return additional context for rendering the template.""" """Return additional context for rendering the template."""
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context['title'] = _('New Backup') context['title'] = _('New Backup')
context['subsubmenu'] = subsubmenu
return context return context
def get_initial(self): def get_initial(self):
@ -121,6 +133,28 @@ class DownloadArchiveView(View):
return response return response
class UploadArchiveView(SuccessMessageMixin, FormView):
form_class = UploadForm
prefix = 'backups'
template_name = 'backups_upload.html'
success_url = reverse_lazy('backups:index')
success_message = _('Archive uploaded.')
def get_context_data(self, **kwargs):
"""Return additional context for rendering the template."""
context = super().get_context_data(**kwargs)
context['title'] = _('Upload Backup File')
context['subsubmenu'] = subsubmenu
return context
def form_valid(self, form):
"""store uploaded file."""
with open(form.cleaned_data['filepath'], 'wb+') as destination:
for chunk in self.request.FILES['backups-file'].chunks():
destination.write(chunk)
return super().form_valid(form)
class ExportArchiveView(SuccessMessageMixin, FormView): class ExportArchiveView(SuccessMessageMixin, FormView):
"""View to export an archive.""" """View to export an archive."""
form_class = ExportArchiveForm form_class = ExportArchiveForm