Commit 04d14e27 authored by James Valleroy's avatar James Valleroy Committed by Joseph Nuthalapati

backups: Restore from exported archive

Signed-off-by: James Valleroy's avatarJames Valleroy <jvalleroy@mailbox.org>
Reviewed-by: Joseph Nuthalapati's avatarJoseph Nuthalapati <njoseph@thoughtworks.com>
parent bd45de29
Pipeline #16758 passed with stages
in 12 minutes and 6 seconds
......@@ -21,6 +21,7 @@ Configuration helper for backups.
"""
import argparse
import glob
import json
import os
import subprocess
......@@ -57,8 +58,13 @@ def parse_arguments():
list_exports = subparsers.add_parser(
'list-exports', help='List exported backup archive files')
list_exports.add_argument('--locations', nargs='+',
help='list of locations to check')
list_exports.add_argument('--location', required=True,
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
return parser.parse_args()
......@@ -121,17 +127,27 @@ def subcommand_export(arguments):
def subcommand_list_exports(arguments):
"""List exported backup archive files."""
archive_files = []
for location in arguments.locations:
backup_path = location
if backup_path[-1] != '/':
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))
exports = []
path = arguments.location
if path[-1] != '/':
path += '/'
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():
......
......@@ -76,11 +76,6 @@ def delete_archive(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):
if location[-1] != '/':
location += '/'
......@@ -102,9 +97,27 @@ def get_export_locations():
return locations
def list_export_files():
"""Return a list of exported backup archives found in storage locations."""
locations = [x[0] for x in get_export_locations()]
command = ['list-exports', '--locations'] + locations
output = actions.superuser_run('backups', command)
return json.loads(output)
def get_export_files():
"""Return a dict of exported backup archives found in storage locations."""
locations = get_export_locations()
export_files = {}
for location in locations:
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
......@@ -37,12 +37,6 @@ class CreateArchiveForm(forms.Form):
'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):
disk = forms.ChoiceField(
label=_('Disk'), widget=forms.RadioSelect(),
......
......@@ -52,7 +52,7 @@
<p>{% trans 'No archives currently exist.' %}</p>
{% else %}
<table class="table table-bordered table-condensed table-striped"
id="archives-list">
id="archives-list">
<thead>
<tr>
<th>{% trans "Name" %}</th>
......@@ -67,18 +67,14 @@
<td class="archive-name">{{ archive.name }}</td>
<td class="archive-time">{{ archive.time }}</td>
<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"
href="{% url 'backups:export' archive.name %}">
{% trans "Export" %}
{% trans "Export" %}
</a>
<a class="archive-delete btn btn-sm btn-default"
href="{% url 'backups:delete' archive.name %}">
<span class="glyphicon glyphicon-trash" aria-hidden="true">
</span>
href="{% url 'backups:delete' archive.name %}">
<span class="glyphicon glyphicon-trash" aria-hidden="true">
</span>
</a>
</td>
</tr>
......@@ -92,18 +88,29 @@
<p>{% trans 'No exported backup archives were found.' %}</p>
{% else %}
<table class="table table-bordered table-condensed table-striped"
id="exports-list">
id="exports-list">
<thead>
<tr>
<th>{% trans "Location" %}</th>
<th>{% trans "Name" %}</th>
<th></th>
</tr>
</thead>
<tbody>
{% for export in exports %}
<tr id="export-{{ export }}" class="export">
<td class="export-name">{{ export }}</td>
</tr>
{% for label, file_list in exports.items %}
{% for name in file_list %}
<tr id="export-{{ label }}-{{ name }}" class="export">
<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 %}
</tbody>
</table>
......
{% 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 %}
......@@ -20,16 +20,16 @@ URLs for the backups module.
from django.conf.urls import url
from .views import IndexView, CreateArchiveView, DeleteArchiveView, \
ExtractArchiveView, ExportArchiveView
from .views import IndexView, CreateArchiveView, ExportArchiveView, \
DeleteArchiveView, RestoreView
urlpatterns = [
url(r'^sys/backups/$', IndexView.as_view(), name='index'),
url(r'^sys/backups/create/$', CreateArchiveView.as_view(), name='create'),
url(r'^sys/backups/(?P<name>[^/]+)/delete/$',
DeleteArchiveView.as_view(), name='delete'),
url(r'^sys/backups/(?P<name>[^/]+)/extract/$',
ExtractArchiveView.as_view(), name='extract'),
url(r'^sys/backups/(?P<name>[^/]+)/export/$',
url(r'^sys/backups/export/(?P<name>[^/]+)/$',
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'),
]
......@@ -25,10 +25,11 @@ from django.shortcuts import redirect
from django.urls import reverse_lazy
from django.utils.translation import ugettext as _
from django.views.generic import FormView, TemplateView
from urllib.parse import unquote
from plinth.modules import backups
from .forms import CreateArchiveForm, ExtractArchiveForm, ExportArchiveForm
from .forms import CreateArchiveForm, ExportArchiveForm
class IndexView(TemplateView):
......@@ -42,7 +43,7 @@ class IndexView(TemplateView):
context['description'] = backups.description
context['info'] = backups.get_info()
context['archives'] = backups.list_archives()
context['exports'] = backups.list_export_files()
context['exports'] = backups.get_export_files()
return context
......@@ -88,18 +89,18 @@ class DeleteArchiveView(SuccessMessageMixin, TemplateView):
return redirect(reverse_lazy('backups:index'))
class ExtractArchiveView(SuccessMessageMixin, FormView):
"""View to extract an archive."""
form_class = ExtractArchiveForm
class ExportArchiveView(SuccessMessageMixin, FormView):
"""View to export an archive."""
form_class = ExportArchiveForm
prefix = 'backups'
template_name = 'backups_form.html'
success_url = reverse_lazy('backups:index')
success_message = _('Archive extracted.')
success_message = _('Archive exported.')
def get_context_data(self, **kwargs):
"""Return additional context for rendering the template."""
context = super().get_context_data(**kwargs)
context['title'] = _('Extract Archive')
context['title'] = _('Export Archive')
context['archive'] = backups.get_archive(self.kwargs['name'])
if context['archive'] is None:
raise Http404
......@@ -108,30 +109,25 @@ class ExtractArchiveView(SuccessMessageMixin, FormView):
def form_valid(self, form):
"""Create the archive on valid form submission."""
backups.extract_archive(self.kwargs['name'], form.cleaned_data['path'])
backups.export_archive(self.kwargs['name'],
form.cleaned_data['disk'])
return super().form_valid(form)
class ExportArchiveView(SuccessMessageMixin, FormView):
"""View to export an archive."""
form_class = ExportArchiveForm
prefix = 'backups'
template_name = 'backups_form.html'
success_url = reverse_lazy('backups:index')
success_message = _('Archive exported.')
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'] = _('Export Archive')
context['archive'] = backups.get_archive(self.kwargs['name'])
if context['archive'] is None:
raise Http404
context['title'] = _('Restore from backup')
context['label'] = unquote(self.kwargs['label'])
context['name'] = self.kwargs['name']
return context
def form_valid(self, form):
"""Create the archive on valid form submission."""
backups.export_archive(self.kwargs['name'],
form.cleaned_data['disk'])
return super().form_valid(form)
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'))
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment