backups: Restore from exported archive

Signed-off-by: James Valleroy <jvalleroy@mailbox.org>
Reviewed-by: Joseph Nuthalapati <njoseph@thoughtworks.com>
This commit is contained in:
James Valleroy 2018-08-16 22:20:13 -04:00 committed by Joseph Nuthalapati
parent bd45de2915
commit 04d14e276f
No known key found for this signature in database
GPG Key ID: 5398F00A2FA43C35
7 changed files with 159 additions and 76 deletions

View File

@ -21,6 +21,7 @@ Configuration helper for backups.
""" """
import argparse import argparse
import glob
import json import json
import os import os
import subprocess import subprocess
@ -57,8 +58,13 @@ def parse_arguments():
list_exports = subparsers.add_parser( list_exports = subparsers.add_parser(
'list-exports', help='List exported backup archive files') 'list-exports', help='List exported backup archive files')
list_exports.add_argument('--locations', nargs='+', list_exports.add_argument('--location', required=True,
help='list of locations to check') help='location to check')
restore = subparsers.add_parser(
'restore', help='Restore files from an exported archive')
restore.add_argument('--filename', help='Tarball file name', required=True)
subparsers.required = True subparsers.required = True
return parser.parse_args() return parser.parse_args()
@ -121,17 +127,27 @@ def subcommand_export(arguments):
def subcommand_list_exports(arguments): def subcommand_list_exports(arguments):
"""List exported backup archive files.""" """List exported backup archive files."""
archive_files = [] exports = []
for location in arguments.locations: path = arguments.location
backup_path = location if path[-1] != '/':
if backup_path[-1] != '/': path += '/'
backup_path += '/'
backup_path += 'FreedomBox-backups/'
if os.path.exists(backup_path):
for filename in os.listdir(backup_path):
archive_files.append(os.path.join(backup_path, filename))
print(json.dumps(archive_files)) path += 'FreedomBox-backups/'
if os.path.exists(path):
for filename in glob.glob(path + '*.tar.gz'):
exports.append(os.path.basename(filename))
print(json.dumps(exports))
def subcommand_restore(arguments):
"""Restore files from an exported archive."""
prev_dir = os.getcwd()
try:
os.chdir('/')
subprocess.run(['tar', 'xf', arguments.filename], check=True)
finally:
os.chdir(prev_dir)
def main(): def main():

View File

@ -76,11 +76,6 @@ def delete_archive(name):
actions.superuser_run('backups', ['delete', '--name', name]) actions.superuser_run('backups', ['delete', '--name', name])
def extract_archive(name, destination):
actions.superuser_run(
'backups', ['extract', '--name', name, '--destination', destination])
def export_archive(name, location): def export_archive(name, location):
if location[-1] != '/': if location[-1] != '/':
location += '/' location += '/'
@ -102,9 +97,27 @@ def get_export_locations():
return locations return locations
def list_export_files(): def get_export_files():
"""Return a list of exported backup archives found in storage locations.""" """Return a dict of exported backup archives found in storage locations."""
locations = [x[0] for x in get_export_locations()] locations = get_export_locations()
command = ['list-exports', '--locations'] + locations export_files = {}
output = actions.superuser_run('backups', command) for location in locations:
return json.loads(output) output = actions.superuser_run(
'backups', ['list-exports', '--location', location[0]])
export_files[location[1]] = json.loads(output)
return export_files
def restore_exported(label, name):
"""Restore files from exported backup archive."""
locations = get_export_locations()
for location in locations:
if location[1] == label:
filename = location[0]
if filename[-1] != '/':
filename += '/'
filename += 'FreedomBox-backups/' + name
actions.superuser_run(
'backups', ['restore', '--filename', filename])
break

View File

@ -37,12 +37,6 @@ class CreateArchiveForm(forms.Form):
'backup repository.')) 'backup repository.'))
class ExtractArchiveForm(forms.Form):
path = forms.CharField(label=_('Path'), strip=True, help_text=_(
'Disk path to a folder on this server where the archive will be '
'extracted.'))
class ExportArchiveForm(forms.Form): class ExportArchiveForm(forms.Form):
disk = forms.ChoiceField( disk = forms.ChoiceField(
label=_('Disk'), widget=forms.RadioSelect(), label=_('Disk'), widget=forms.RadioSelect(),

View File

@ -52,7 +52,7 @@
<p>{% trans 'No archives currently exist.' %}</p> <p>{% trans 'No archives currently exist.' %}</p>
{% else %} {% else %}
<table class="table table-bordered table-condensed table-striped" <table class="table table-bordered table-condensed table-striped"
id="archives-list"> id="archives-list">
<thead> <thead>
<tr> <tr>
<th>{% trans "Name" %}</th> <th>{% trans "Name" %}</th>
@ -67,18 +67,14 @@
<td class="archive-name">{{ archive.name }}</td> <td class="archive-name">{{ archive.name }}</td>
<td class="archive-time">{{ archive.time }}</td> <td class="archive-time">{{ archive.time }}</td>
<td class="archive-operations"> <td class="archive-operations">
<a class="archive-extract btn btn-sm btn-default"
href="{% url 'backups:extract' archive.name %}">
{% trans "Extract" %}
</a>
<a class="archive-export btn btn-sm btn-default" <a class="archive-export btn btn-sm btn-default"
href="{% url 'backups:export' archive.name %}"> href="{% url 'backups:export' archive.name %}">
{% trans "Export" %} {% trans "Export" %}
</a> </a>
<a class="archive-delete btn btn-sm btn-default" <a class="archive-delete btn btn-sm btn-default"
href="{% url 'backups:delete' archive.name %}"> href="{% url 'backups:delete' archive.name %}">
<span class="glyphicon glyphicon-trash" aria-hidden="true"> <span class="glyphicon glyphicon-trash" aria-hidden="true">
</span> </span>
</a> </a>
</td> </td>
</tr> </tr>
@ -92,18 +88,29 @@
<p>{% trans 'No exported backup archives were found.' %}</p> <p>{% trans 'No exported backup archives 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">
<thead> <thead>
<tr> <tr>
<th>{% trans "Location" %}</th>
<th>{% trans "Name" %}</th> <th>{% trans "Name" %}</th>
<th></th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for export in exports %} {% for label, file_list in exports.items %}
<tr id="export-{{ export }}" class="export"> {% for name in file_list %}
<td class="export-name">{{ export }}</td> <tr id="export-{{ label }}-{{ name }}" class="export">
</tr> <td class="export-label">{{ label }}</td>
<td class="export-name">{{ name }}</td>
<td class="export-operations">
<a class="restore btn btn-sm btn-default"
href="{% url 'backups:restore' label|urlencode name|urlencode %}">
{% trans "Restore" %}
</a>
</td>
</tr>
{% endfor %}
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>

View File

@ -0,0 +1,57 @@
{% 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 %}
<h2>{{ title }}</h2>
<p>{% trans "Restore data from this archive?" %}</p>
<div class="row">
<div class="col-lg-12">
<table class="table table-bordered table-condensed table-striped">
<thead>
<th>{% trans "Location" %}</th>
<th>{% trans "Name" %}</th>
</thead>
<tbody>
<tr>
<td>{{ label }}</td>
<td>{{ name }}</td>
</tr>
</tbody>
</table>
</div>
</div>
<p>
<form class="form" method="post">
{% csrf_token %}
<input type="submit" class="btn btn-danger"
value="{% blocktrans trimmed %}
Restore data from {{ name }}
{% endblocktrans %}"/>
</form>
</p>
{% endblock %}

View File

@ -20,16 +20,16 @@ URLs for the backups module.
from django.conf.urls import url from django.conf.urls import url
from .views import IndexView, CreateArchiveView, DeleteArchiveView, \ from .views import IndexView, CreateArchiveView, ExportArchiveView, \
ExtractArchiveView, ExportArchiveView DeleteArchiveView, RestoreView
urlpatterns = [ urlpatterns = [
url(r'^sys/backups/$', IndexView.as_view(), name='index'), url(r'^sys/backups/$', IndexView.as_view(), name='index'),
url(r'^sys/backups/create/$', CreateArchiveView.as_view(), name='create'), url(r'^sys/backups/create/$', CreateArchiveView.as_view(), name='create'),
url(r'^sys/backups/(?P<name>[^/]+)/delete/$', url(r'^sys/backups/export/(?P<name>[^/]+)/$',
DeleteArchiveView.as_view(), name='delete'),
url(r'^sys/backups/(?P<name>[^/]+)/extract/$',
ExtractArchiveView.as_view(), name='extract'),
url(r'^sys/backups/(?P<name>[^/]+)/export/$',
ExportArchiveView.as_view(), name='export'), ExportArchiveView.as_view(), name='export'),
url(r'^sys/backups/delete/(?P<name>[^/]+)/$',
DeleteArchiveView.as_view(), name='delete'),
url(r'^sys/backups/restore/(?P<label>[^/]+)/(?P<name>[^/]+)/$',
RestoreView.as_view(), name='restore'),
] ]

View File

@ -25,10 +25,11 @@ 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.views.generic import FormView, TemplateView from django.views.generic import FormView, TemplateView
from urllib.parse import unquote
from plinth.modules import backups from plinth.modules import backups
from .forms import CreateArchiveForm, ExtractArchiveForm, ExportArchiveForm from .forms import CreateArchiveForm, ExportArchiveForm
class IndexView(TemplateView): class IndexView(TemplateView):
@ -42,7 +43,7 @@ class IndexView(TemplateView):
context['description'] = backups.description context['description'] = backups.description
context['info'] = backups.get_info() context['info'] = backups.get_info()
context['archives'] = backups.list_archives() context['archives'] = backups.list_archives()
context['exports'] = backups.list_export_files() context['exports'] = backups.get_export_files()
return context return context
@ -88,30 +89,6 @@ class DeleteArchiveView(SuccessMessageMixin, TemplateView):
return redirect(reverse_lazy('backups:index')) return redirect(reverse_lazy('backups:index'))
class ExtractArchiveView(SuccessMessageMixin, FormView):
"""View to extract an archive."""
form_class = ExtractArchiveForm
prefix = 'backups'
template_name = 'backups_form.html'
success_url = reverse_lazy('backups:index')
success_message = _('Archive extracted.')
def get_context_data(self, **kwargs):
"""Return additional context for rendering the template."""
context = super().get_context_data(**kwargs)
context['title'] = _('Extract Archive')
context['archive'] = backups.get_archive(self.kwargs['name'])
if context['archive'] is None:
raise Http404
return context
def form_valid(self, form):
"""Create the archive on valid form submission."""
backups.extract_archive(self.kwargs['name'], form.cleaned_data['path'])
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
@ -135,3 +112,22 @@ class ExportArchiveView(SuccessMessageMixin, FormView):
backups.export_archive(self.kwargs['name'], backups.export_archive(self.kwargs['name'],
form.cleaned_data['disk']) form.cleaned_data['disk'])
return super().form_valid(form) return super().form_valid(form)
class RestoreView(SuccessMessageMixin, TemplateView):
"""View to restore files from an exported archive."""
template_name = 'backups_restore.html'
def get_context_data(self, **kwargs):
"""Return additional context for rendering the template."""
context = super().get_context_data(**kwargs)
context['title'] = _('Restore from backup')
context['label'] = unquote(self.kwargs['label'])
context['name'] = self.kwargs['name']
return context
def post(self, request, label, name):
"""Restore files from the archive on valid form submission."""
backups.restore_exported(label, name)
messages.success(request, _('Restored data from backup.'))
return redirect(reverse_lazy('backups:index'))