diff --git a/actions/snapshot b/actions/snapshot index 538042258..f74513089 100755 --- a/actions/snapshot +++ b/actions/snapshot @@ -35,9 +35,6 @@ def parse_arguments(): help='Delete a snapshot by number') subparser.add_argument('number', help='Number of snapshot to delete') - subparser = subparsers.add_parser('delete-all', - help='Delete all the snapshots') - subparser = subparsers.add_parser('set-config', help='Configure automatic snapshots') subparser.add_argument('config') @@ -166,25 +163,30 @@ def _add_fstab_entry(mount_point): aug.save() -def _get_snapper_list(): - command = ['snapper', 'list'] - process = subprocess.run(command, stdout=subprocess.PIPE, check=True) - return process.stdout.decode().splitlines() +def _parse_number(number): + """Parse the char following the number and return status of snapshot.""" + is_default = number[-1] in ('+', '*') + is_active = number[-1] in ('-', '*') + return number.strip('-+*'), is_default, is_active def subcommand_list(_): """List snapshots.""" - lines = _get_snapper_list() - keys = ('number', 'type', 'pre_number', 'date', 'user', 'cleanup', - 'description') + process = subprocess.run(['snapper', 'list'], stdout=subprocess.PIPE, + check=True) + lines = process.stdout.decode().splitlines() + + keys = ('number', 'is_default', 'is_active', 'type', 'pre_number', 'date', + 'user', 'cleanup', 'description') snapshots = [] for line in lines[2:]: - parts = [part.strip('* ') for part in line.split('|')] - snapshots.append(dict(zip(keys, parts))) - - default = _get_default_snapshot() - for snapshot in snapshots: - snapshot['is_default'] = (snapshot['number'] == default) + parts = [part.strip() for part in line.split('|')] + parts = list(_parse_number(parts[0])) + parts[1:] + snapshot = dict(zip(keys, parts)) + # Snapshot 0 always represents the current system, it need not be + # listed and cannot be deleted. + if snapshot['number'] != '0': + snapshots.append(snapshot) snapshots.reverse() print(json.dumps(snapshots)) @@ -231,35 +233,6 @@ def subcommand_delete(arguments): subprocess.run(command, check=True) -def subcommand_delete_all(_): - """Delete all the snapshots (except the active one).""" - lines = _get_snapper_list() - snapshot_range = [line.split('|')[0].strip() for line in lines[3:]] - default_snapshot = _get_default_snapshot() - if snapshot_range: - if default_snapshot: - index = snapshot_range.index(default_snapshot) - range_before = snapshot_range[:index] - range_after = snapshot_range[index + 1:] - to_delete = [range_before, range_after] - else: - to_delete = [snapshot_range] - - delete_args = filter(None, map(_get_delete_arg, to_delete)) - for arg in delete_args: - subprocess.run(['snapper', 'delete', arg], check=True) - - -def _get_delete_arg(range_list): - """Return 'a-b' when given ['a', ..., 'b'].""" - if not range_list: - return None - elif len(range_list) == 1: - return range_list[0] - else: - return range_list[0] + '-' + range_list[-1] - - def subcommand_set_config(arguments): command = ['snapper', 'set-config'] + arguments.config.split() subprocess.run(command, check=True) diff --git a/plinth/modules/snapshot/templates/snapshot_delete_selected.html b/plinth/modules/snapshot/templates/snapshot_delete_selected.html index 2bfbab10d..929f6f4b5 100644 --- a/plinth/modules/snapshot/templates/snapshot_delete_selected.html +++ b/plinth/modules/snapshot/templates/snapshot_delete_selected.html @@ -19,13 +19,11 @@ {% for snapshot in snapshots %} - {% if not snapshot.is_default %} - - {{ snapshot.number }} - {{ snapshot.date }} - {{ snapshot.description }} - - {% endif %} + + {{ snapshot.number }} + {{ snapshot.date }} + {{ snapshot.description }} + {% endfor %} @@ -33,6 +31,11 @@

{% csrf_token %} + + {% for snapshot in snapshots %} + + {% endfor %} +
diff --git a/plinth/modules/snapshot/templates/snapshot_manage.html b/plinth/modules/snapshot/templates/snapshot_manage.html index 7244b2c39..e15842e21 100644 --- a/plinth/modules/snapshot/templates/snapshot_manage.html +++ b/plinth/modules/snapshot/templates/snapshot_manage.html @@ -19,7 +19,8 @@
+ value="{% trans 'Delete Snapshots' %}" + {{ has_deletable_snapshots|yesno:',disabled="disabled"' }}/>
@@ -33,35 +34,38 @@ {% for snapshot in snapshots %} - {% if snapshot.description != "current" %} - - - {{ snapshot.number }} - {% if snapshot.is_default %} - - {% trans "active" %} - - {% endif %} - - {{ snapshot.date }} - {{ snapshot.description }} - - - - - - - {% if not snapshot.is_default %} - - {% endif %} - - - {% endif %} + + + {{ snapshot.number }} + {% if snapshot.is_default %} + + {% trans "will be used at next boot" %} + + {% endif %} + {% if snapshot.is_active %} + + {% trans "in use" %} + + {% endif %} + + {{ snapshot.date }} + {{ snapshot.description }} + + + + + + + {% if not snapshot.is_default and not snapshot.is_active %} + + {% endif %} + + {% endfor %} diff --git a/plinth/modules/snapshot/views.py b/plinth/modules/snapshot/views.py index bfe741aa2..03ee06804 100644 --- a/plinth/modules/snapshot/views.py +++ b/plinth/modules/snapshot/views.py @@ -4,8 +4,10 @@ Views for snapshot module. """ import json +import urllib.parse from django.contrib import messages +from django.http import HttpResponseRedirect from django.shortcuts import redirect from django.template.response import TemplateResponse from django.urls import reverse, reverse_lazy @@ -78,15 +80,21 @@ def manage(request): actions.superuser_run('snapshot', ['create']) messages.success(request, _('Created snapshot.')) if 'delete_selected' in request.POST: - if request.POST.getlist('snapshot_list'): - snapshot_to_delete = request.POST.getlist('snapshot_list') - request.session['snapshots'] = snapshot_to_delete - return redirect(reverse('snapshot:delete-selected')) + to_delete = request.POST.getlist('snapshot_list') + if to_delete: + # Send values using GET params instead of session variables so + # that snapshots can be deleted even when disk is full. + params = [('snapshots', number) for number in to_delete] + params = urllib.parse.urlencode(params) + url = reverse('snapshot:delete-selected') + return HttpResponseRedirect(f'{url}?{params}') output = actions.superuser_run('snapshot', ['list']) snapshots = json.loads(output) - has_deletable_snapshots = any( - [snapshot for snapshot in snapshots[1:] if not snapshot['is_default']]) + has_deletable_snapshots = any([ + snapshot for snapshot in snapshots + if not snapshot['is_default'] and not snapshot['is_active'] + ]) return TemplateResponse( request, 'snapshot_manage.html', { @@ -100,6 +108,7 @@ def manage(request): def update_configuration(request, old_status, new_status): """Update configuration of snapshots.""" + def make_config(args): key, stamp = args[0], args[1] if old_status[key] != new_status[key]: @@ -142,39 +151,44 @@ def update_configuration(request, old_status, new_status): def delete_selected(request): + """View to delete selected snapshots.""" + if request.method == 'POST': + to_delete = set(request.POST.getlist('snapshots')) + else: + to_delete = set(request.GET.getlist('snapshots')) + + if not to_delete: + return redirect(reverse('snapshot:manage')) + output = actions.superuser_run('snapshot', ['list']) snapshots = json.loads(output) + snapshots_to_delete = [ + snapshot for snapshot in snapshots if snapshot['number'] in to_delete + and not snapshot['is_active'] and not snapshot['is_default'] + ] if request.method == 'POST': - if 'snapshots' in request.session: - to_delete = request.session['snapshots'] - try: - if to_delete == len(snapshots): - actions.superuser_run('snapshot', ['delete_all']) - messages.success(request, _('Deleted all snapshots')) - else: - for snapshot in to_delete: - actions.superuser_run('snapshot', ['delete', snapshot]) - messages.success(request, _('Deleted selected snapshots')) - except ActionError as exception: - if 'Config is in use.' in exception.args[2]: - messages.error( - request, - _('Snapshot is currently in use. ' - 'Please try again later.')) - else: - raise + try: + for snapshot in snapshots_to_delete: + actions.superuser_run('snapshot', + ['delete', snapshot['number']]) - return redirect(reverse('snapshot:manage')) + messages.success(request, _('Deleted selected snapshots')) + except ActionError as exception: + if 'Config is in use.' in exception.args[2]: + messages.error( + request, + _('Snapshot is currently in use. ' + 'Please try again later.')) + else: + raise - if 'snapshots' in request.session: - data = request.session['snapshots'] - to_delete = list(filter(lambda x: x['number'] in data, snapshots)) + return redirect(reverse('snapshot:manage')) - return TemplateResponse(request, 'snapshot_delete_selected.html', { - 'title': _('Delete Snapshots'), - 'snapshots': to_delete - }) + return TemplateResponse(request, 'snapshot_delete_selected.html', { + 'title': _('Delete Snapshots'), + 'snapshots': snapshots_to_delete + }) def rollback(request, number):