From 04d14e276f1699e05fbbfb70a2702a407a6719c6 Mon Sep 17 00:00:00 2001 From: James Valleroy Date: Thu, 16 Aug 2018 22:20:13 -0400 Subject: [PATCH] backups: Restore from exported archive Signed-off-by: James Valleroy Reviewed-by: Joseph Nuthalapati --- actions/backups | 40 +++++++++---- plinth/modules/backups/__init__.py | 35 ++++++++---- plinth/modules/backups/forms.py | 6 -- plinth/modules/backups/templates/backups.html | 35 +++++++----- .../backups/templates/backups_restore.html | 57 +++++++++++++++++++ plinth/modules/backups/urls.py | 14 ++--- plinth/modules/backups/views.py | 48 +++++++--------- 7 files changed, 159 insertions(+), 76 deletions(-) create mode 100644 plinth/modules/backups/templates/backups_restore.html diff --git a/actions/backups b/actions/backups index f0364abef..5a79bc01f 100755 --- a/actions/backups +++ b/actions/backups @@ -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(): diff --git a/plinth/modules/backups/__init__.py b/plinth/modules/backups/__init__.py index d38e37e9c..68ff63e62 100644 --- a/plinth/modules/backups/__init__.py +++ b/plinth/modules/backups/__init__.py @@ -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 diff --git a/plinth/modules/backups/forms.py b/plinth/modules/backups/forms.py index d8ec95b9c..79297c6c4 100644 --- a/plinth/modules/backups/forms.py +++ b/plinth/modules/backups/forms.py @@ -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(), diff --git a/plinth/modules/backups/templates/backups.html b/plinth/modules/backups/templates/backups.html index 4f40c9e21..f42e4b74b 100644 --- a/plinth/modules/backups/templates/backups.html +++ b/plinth/modules/backups/templates/backups.html @@ -52,7 +52,7 @@

{% trans 'No archives currently exist.' %}

{% else %} + id="archives-list"> @@ -67,18 +67,14 @@ @@ -92,18 +88,29 @@

{% trans 'No exported backup archives were found.' %}

{% else %}
{% trans "Name" %}{{ archive.name }} {{ archive.time }} - - {% trans "Extract" %} - - {% trans "Export" %} + {% trans "Export" %} - + href="{% url 'backups:delete' archive.name %}"> +
+ id="exports-list"> + + - {% for export in exports %} - - - + {% for label, file_list in exports.items %} + {% for name in file_list %} + + + + + + {% endfor %} {% endfor %}
{% trans "Location" %} {% trans "Name" %}
{{ export }}
{{ label }}{{ name }} + + {% trans "Restore" %} + +
diff --git a/plinth/modules/backups/templates/backups_restore.html b/plinth/modules/backups/templates/backups_restore.html new file mode 100644 index 000000000..60bd3b8f1 --- /dev/null +++ b/plinth/modules/backups/templates/backups_restore.html @@ -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 . +# +{% endcomment %} + +{% load bootstrap %} +{% load i18n %} + +{% block content %} +

{{ title }}

+ +

{% trans "Restore data from this archive?" %}

+ +
+
+ + + + + + + + + + + +
{% trans "Location" %}{% trans "Name" %}
{{ label }}{{ name }}
+
+
+ +

+

+ {% csrf_token %} + + +
+

+ +{% endblock %} diff --git a/plinth/modules/backups/urls.py b/plinth/modules/backups/urls.py index 12b0e578a..68617d855 100644 --- a/plinth/modules/backups/urls.py +++ b/plinth/modules/backups/urls.py @@ -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[^/]+)/delete/$', - DeleteArchiveView.as_view(), name='delete'), - url(r'^sys/backups/(?P[^/]+)/extract/$', - ExtractArchiveView.as_view(), name='extract'), - url(r'^sys/backups/(?P[^/]+)/export/$', + url(r'^sys/backups/export/(?P[^/]+)/$', ExportArchiveView.as_view(), name='export'), + url(r'^sys/backups/delete/(?P[^/]+)/$', + DeleteArchiveView.as_view(), name='delete'), + url(r'^sys/backups/restore/(?P