From 2319b40d875f72cd4ac051cb23f499eadd3b766c Mon Sep 17 00:00:00 2001 From: Michael Pimmer Date: Mon, 29 Oct 2018 00:56:51 +0000 Subject: [PATCH] Backups: do not hardcode uploaded backup file path And use a decorator instead of a middleware to delete backup files. Reviewed-by: James Valleroy --- plinth/modules/backups/__init__.py | 11 ++----- plinth/modules/backups/decorators.py | 40 +++++++++++++++++++++++ plinth/modules/backups/views.py | 49 ++++++++++------------------ 3 files changed, 60 insertions(+), 40 deletions(-) create mode 100644 plinth/modules/backups/decorators.py diff --git a/plinth/modules/backups/__init__.py b/plinth/modules/backups/__init__.py index 4da8b624a..7f589057f 100644 --- a/plinth/modules/backups/__init__.py +++ b/plinth/modules/backups/__init__.py @@ -44,7 +44,7 @@ service = None MANIFESTS_FOLDER = '/var/lib/plinth/backups-manifests/' REPOSITORY = '/var/lib/freedombox/borgbackup' # session variable name that stores when a backup file should be deleted -SESSION_BACKUP_VARIABLE = 'fbx-backup-filestamp' +SESSION_BACKUP_PATH = 'fbx-backup-path' def init(): @@ -106,11 +106,6 @@ def delete_archive(name): actions.superuser_run('backups', ['delete', '--name', name]) -def delete_upload_backup_file(path): - if os.path.isfile(path): - os.remove(path) - - def export_archive(name, filepath): """Export an archive as .tar.gz file @@ -156,10 +151,10 @@ def _restore_archive_handler(packet): packet.label, '--destination', '/'], input=locations_data.encode()) -def restore_from_upload(apps=None): +def restore_from_upload(path, apps=None): """Restore files from (uploaded) eported backup file""" api.restore_apps(_restore_exported_archive_handler, app_names=apps, - create_subvolume=False, backup_file=UPLOAD_BACKUP_PATH) + create_subvolume=False, backup_file=path) def restore(archive_path, apps=None): diff --git a/plinth/modules/backups/decorators.py b/plinth/modules/backups/decorators.py new file mode 100644 index 000000000..960583a32 --- /dev/null +++ b/plinth/modules/backups/decorators.py @@ -0,0 +1,40 @@ +# +# 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 . +# +""" +Decorators for the backup views. +""" + +import os + +from . import SESSION_BACKUP_PATH + + +def delete_tmp_backup_file(function): + """Decorator to delete uploaded backup files""" + + def wrap(request, *args, **kwargs): + path = request.session.get(SESSION_BACKUP_PATH, None) + if path: + if os.path.isfile(path): + os.remove(path) + del request.session[SESSION_BACKUP_PATH] + return function(request, *args, **kwargs) + + wrap.__doc__ = function.__doc__ + wrap.__name__ = function.__name__ + + return wrap diff --git a/plinth/modules/backups/views.py b/plinth/modules/backups/views.py index 976039c8c..7cdcf38ec 100644 --- a/plinth/modules/backups/views.py +++ b/plinth/modules/backups/views.py @@ -18,12 +18,12 @@ Views for the backups app. """ -import gzip -import mimetypes from datetime import datetime -import os +import gzip from io import BytesIO -import time +import mimetypes +import os +import tempfile from urllib.parse import unquote from django.contrib import messages @@ -31,6 +31,7 @@ from django.contrib.messages.views import SuccessMessageMixin from django.http import Http404, FileResponse, StreamingHttpResponse from django.shortcuts import redirect from django.urls import reverse_lazy +from django.utils.decorators import method_decorator from django.utils.translation import ugettext as _ from django.utils.translation import ugettext_lazy from django.views.generic import View, FormView, TemplateView @@ -38,10 +39,8 @@ from django.views.generic import View, FormView, TemplateView from plinth.modules import backups from plinth import actions -from . import api, forms, SESSION_BACKUP_VARIABLE, delete_upload_backup_file - -# number of seconds an uploaded backup file should be kept/stored -KEEP_UPLOADED_BACKUP_FOR = 60*10 +from . import api, forms, SESSION_BACKUP_PATH +from .decorators import delete_tmp_backup_file subsubmenu = [{ 'url': reverse_lazy('backups:index'), @@ -55,6 +54,7 @@ subsubmenu = [{ }] +@method_decorator(delete_tmp_backup_file, name='dispatch') class IndexView(TemplateView): """View to show list of archives.""" template_name = 'backups.html' @@ -131,23 +131,6 @@ def _get_file_response(path, filename): return response -class create_temporary_backup_file: - """Create a temporary backup file that gets deleted after using it""" - # TODO: try using export-tar with FILE parameter '-' and reading stdout: - # https://borgbackup.readthedocs.io/en/stable/usage/tar.html - - def __init__(self, name): - self.name = name - self.path = UPLOAD_BACKUP_PATH - - def __enter__(self): - backups.export_archive(self.name, self.path) - return self.path - - def __exit__(self, type, value, traceback): - delete_upload_backup_file(self.path) - - class UploadArchiveView(SuccessMessageMixin, FormView): form_class = forms.UploadForm prefix = 'backups' @@ -163,11 +146,10 @@ class UploadArchiveView(SuccessMessageMixin, FormView): def form_valid(self, form): """store uploaded file.""" - with open(UPLOAD_BACKUP_PATH, 'wb+') as destination: + with tempfile.NamedTemporaryFile(delete=False) as tmp_file: + self.request.session[SESSION_BACKUP_PATH] = tmp_file.name for chunk in self.request.FILES['backups-file'].chunks(): - destination.write(chunk) - self.request.session[SESSION_BACKUP_VARIABLE] = time.time() + \ - KEEP_UPLOADED_BACKUP_FOR + tmp_file.write(chunk) return super().form_valid(form) @@ -201,7 +183,8 @@ class RestoreFromUploadView(BaseRestoreView): """View to restore files from an (uploaded) exported archive.""" def get(self, *args, **kwargs): - if not os.path.isfile(UPLOAD_BACKUP_PATH): + path = self.request.session.get(SESSION_BACKUP_PATH) + if not os.path.isfile(path): messages.error(self.request, _('No backup file found.')) return redirect(reverse_lazy('backups:index')) else: @@ -215,11 +198,13 @@ class RestoreFromUploadView(BaseRestoreView): def _get_included_apps(self): """Save some data used to instantiate the form.""" - return backups.get_exported_archive_apps(UPLOAD_BACKUP_PATH) + path = self.request.session.get(SESSION_BACKUP_PATH) + return backups.get_exported_archive_apps(path) def form_valid(self, form): """Restore files from the archive on valid form submission.""" - backups.restore_from_upload(form.cleaned_data['selected_apps']) + path = self.request.session.get(SESSION_BACKUP_PATH) + backups.restore_from_upload(path, form.cleaned_data['selected_apps']) return super().form_valid(form)