From cdf07a92b4ec6d143bb3268c9e0d033fea29aa34 Mon Sep 17 00:00:00 2001 From: Sunil Mohan Adapa Date: Mon, 23 Jul 2018 17:05:06 -0700 Subject: [PATCH] udiskie: Add eject functionality for a drive When a block device with a filesystem is selected for ejecting, all the filesystems on the drive to which the selected filesystem belongs will first be unmounted. Finally the drive itself is ejected. Reviewed-by: James Valleroy --- plinth/modules/udiskie/templates/udiskie.html | 13 ++ plinth/modules/udiskie/udisks2.py | 116 +++++++++++++++++- plinth/modules/udiskie/urls.py | 13 +- plinth/modules/udiskie/views.py | 49 +++++++- 4 files changed, 182 insertions(+), 9 deletions(-) diff --git a/plinth/modules/udiskie/templates/udiskie.html b/plinth/modules/udiskie/templates/udiskie.html index ba2f6efdb..abccae023 100644 --- a/plinth/modules/udiskie/templates/udiskie.html +++ b/plinth/modules/udiskie/templates/udiskie.html @@ -36,6 +36,7 @@ {% trans "Size" %} {% trans "Filesystem" %} {% trans "Mount Point" %} + {% trans "Actions" %} @@ -46,6 +47,18 @@ {{ device.size }} {{ device.filesystem_type }} {{ device.mount_points|join:', ' }} + + {% if device.mount_points %} +
+ {% csrf_token %} + + +
+ {% endif %} + {% endfor %} diff --git a/plinth/modules/udiskie/udisks2.py b/plinth/modules/udiskie/udisks2.py index 4c30c188a..377a631f1 100644 --- a/plinth/modules/udiskie/udisks2.py +++ b/plinth/modules/udiskie/udisks2.py @@ -18,14 +18,24 @@ Library for interacting with udisks2 D-Bus service. """ +from django.utils.translation import ugettext as _ + from plinth import utils from plinth.modules.storage import format_bytes +glib = utils.import_from_gi('GLib', '2.0') +udisks = utils.import_from_gi('UDisks', '2.0') + + +def _get_options(): + """Return the common options used for udisks2 operations.""" + options = glib.Variant( + 'a{sv}', {'auth.no_user_interaction': glib.Variant('b', True)}) + return options + def list_devices(): """List devices that can be ejected.""" - udisks = utils.import_from_gi('UDisks', '2.0') - client = udisks.Client.new_sync() object_manager = client.get_object_manager() @@ -60,3 +70,105 @@ def list_devices(): devices.append(device) return devices + + +def eject_drive_of_device(device_path): + """Eject a device after unmounting all of its partitions. + + Return the details (model, vendor) of drives ejected. + """ + client = udisks.Client.new_sync() + object_manager = client.get_object_manager() + + found_objects = [ + obj for obj in object_manager.get_objects() + if obj.get_block() and obj.get_block().props.device == device_path + ] + + if not found_objects: + raise ValueError( + _('No such device - {device_path}').format( + device_path=device_path)) + + obj = found_objects[0] + + # Unmount filesystems + block_device = obj.get_block() + drive_object_path = block_device.props.drive + if drive_object_path != '/': + umount_all_filesystems_of_drive(drive_object_path) + else: + # Block device has not associated drive + umount_filesystem(obj.get_filesystem()) + + # Eject the drive + drive = client.get_drive_for_block(block_device) + if drive: + drive.call_eject_sync(_get_options(), None) + return { + 'vendor': drive.props.vendor, + 'model': drive.props.model, + } + + return None + + +def umount_filesystem(filesystem): + """Unmount a filesystem """ + if filesystem and filesystem.props.mount_points: + filesystem.call_unmount_sync(_get_options()) + + +def umount_all_filesystems_of_drive(drive_object_path): + """Unmount all filesystems on block devices of a drive.""" + client = udisks.Client.new_sync() + object_manager = client.get_object_manager() + + for obj in object_manager.get_objects(): + block_device = obj.get_block() + if not block_device or block_device.props.drive != drive_object_path: + continue + + umount_filesystem(obj.get_filesystem()) + + +def get_error_message(error): + """Return an error message given an exception.""" + if error.matches(udisks.Error.quark(), udisks.Error.FAILED): + message = _('The operation failed.') + elif error.matches(udisks.Error.quark(), udisks.Error.CANCELLED): + message = _('The operation was cancelled.') + elif error.matches(udisks.Error.quark(), udisks.Error.ALREADY_UNMOUNTING): + message = _('The device is already unmounting.') + elif error.matches(udisks.Error.quark(), udisks.Error.NOT_SUPPORTED): + message = _('The operation is not supported due to ' + 'missing driver/tool support.') + elif error.matches(udisks.Error.quark(), udisks.Error.TIMED_OUT): + message = _('The operation timed out.') + elif error.matches(udisks.Error.quark(), udisks.Error.WOULD_WAKEUP): + message = _('The operation would wake up a disk that is ' + 'in a deep-sleep state.') + elif error.matches(udisks.Error.quark(), udisks.Error.DEVICE_BUSY): + message = _('Attempting to unmount a device that is busy.') + elif error.matches(udisks.Error.quark(), udisks.Error.ALREADY_CANCELLED): + message = _('The operation has already been cancelled.') + elif error.matches(udisks.Error.quark(), udisks.Error.NOT_AUTHORIZED) or \ + error.matches(udisks.Error.quark(), + udisks.Error.NOT_AUTHORIZED_CAN_OBTAIN) or \ + error.matches(udisks.Error.quark(), + udisks.Error.NOT_AUTHORIZED_DISMISSED): + message = _('Not authorized to perform the requested operation.') + elif error.matches(udisks.Error.quark(), udisks.Error.ALREADY_MOUNTED): + message = _('The device is already mounted.') + elif error.matches(udisks.Error.quark(), udisks.Error.NOT_MOUNTED): + message = _('The device is not mounted.') + elif error.matches(udisks.Error.quark(), + udisks.Error.OPTION_NOT_PERMITTED): + message = _('Not permitted to use the requested option.') + elif error.matches(udisks.Error.quark(), + udisks.Error.MOUNTED_BY_OTHER_USER): + message = _('The device is mounted by another user.') + else: + message = error.message + + return message diff --git a/plinth/modules/udiskie/urls.py b/plinth/modules/udiskie/urls.py index c1570fbe4..519acb26b 100644 --- a/plinth/modules/udiskie/urls.py +++ b/plinth/modules/udiskie/urls.py @@ -20,12 +20,15 @@ URLs for the udiskie module. from django.conf.urls import url -from .views import UdiskieView from plinth.modules import udiskie +from .views import Index, eject + urlpatterns = [ - url(r'^sys/udiskie/$', - UdiskieView.as_view(service_id=udiskie.managed_services[0], - description=udiskie.description, - show_status_block=True), name='index'), + url( + r'^sys/udiskie/$', + Index.as_view(service_id=udiskie.managed_services[0], + description=udiskie.description, show_status_block=True), + name='index'), + url(r'^sys/udiskie/eject/(?P[\w%]+)/$', eject, name='eject'), ] diff --git a/plinth/modules/udiskie/views.py b/plinth/modules/udiskie/views.py index 42bf11474..c00cbb689 100644 --- a/plinth/modules/udiskie/views.py +++ b/plinth/modules/udiskie/views.py @@ -18,17 +18,62 @@ Views for udiskie module. """ +import urllib.parse +from logging import Logger + +from django.contrib import messages +from django.shortcuts import redirect +from django.urls import reverse +from django.utils.translation import ugettext as _ +from django.views.decorators.http import require_POST + from plinth.views import ServiceView from . import udisks2 +logger = Logger(__name__) -class UdiskieView(ServiceView): + +class Index(ServiceView): """View to show devices.""" template_name = 'udiskie.html' - def get_context_data(self, **kwargs): + def get_context_data(self, *args, **kwargs): """Return the context data rendering the template.""" context = super().get_context_data(**kwargs) context['devices'] = udisks2.list_devices() return context + + +@require_POST +def eject(request, device_path): + """Eject a device, given its path. + + Device path is quoted with slashes written as %2F. + + """ + device_path = urllib.parse.unquote(device_path) + + try: + drive = udisks2.eject_drive_of_device(device_path) + if drive: + messages.success( + request, + _('{drive_vendor} {drive_model} can be safely unplugged.') + .format(drive_vendor=drive['vendor'], + drive_model=drive['model'])) + else: + messages.success(request, _('Device can be safely unplugged.')) + except Exception as exception: + try: + message = udisks2.get_error_message(exception) + except AttributeError: + message = str(exception) + + logger.exception('Error ejecting device - %s', message) + messages.error( + request, + _('Error ejecting device: {error_message}').format( + error_message=message)) + + return redirect(reverse('udiskie:index'))