mirror of
https://github.com/freedombox/FreedomBox.git
synced 2026-01-28 08:03:36 +00:00
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:
parent
bd45de2915
commit
04d14e276f
@ -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))
|
||||
exports = []
|
||||
path = arguments.location
|
||||
if path[-1] != '/':
|
||||
path += '/'
|
||||
|
||||
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():
|
||||
|
||||
@ -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>
|
||||
|
||||
57
plinth/modules/backups/templates/backups_restore.html
Normal file
57
plinth/modules/backups/templates/backups_restore.html
Normal 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 %}
|
||||
@ -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,30 +89,6 @@ class DeleteArchiveView(SuccessMessageMixin, TemplateView):
|
||||
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):
|
||||
"""View to export an archive."""
|
||||
form_class = ExportArchiveForm
|
||||
@ -135,3 +112,22 @@ class ExportArchiveView(SuccessMessageMixin, FormView):
|
||||
backups.export_archive(self.kwargs['name'],
|
||||
form.cleaned_data['disk'])
|
||||
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'))
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user