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 <jvalleroy@mailbox.org>
This commit is contained in:
Sunil Mohan Adapa 2018-07-23 17:05:06 -07:00 committed by James Valleroy
parent 89f31db71f
commit cdf07a92b4
No known key found for this signature in database
GPG Key ID: 77C0C75E7B650808
4 changed files with 182 additions and 9 deletions

View File

@ -36,6 +36,7 @@
<th>{% trans "Size" %}</th> <th>{% trans "Size" %}</th>
<th>{% trans "Filesystem" %}</th> <th>{% trans "Filesystem" %}</th>
<th>{% trans "Mount Point" %}</th> <th>{% trans "Mount Point" %}</th>
<th>{% trans "Actions" %}</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@ -46,6 +47,18 @@
<td>{{ device.size }}</td> <td>{{ device.size }}</td>
<td>{{ device.filesystem_type }}</td> <td>{{ device.filesystem_type }}</td>
<td>{{ device.mount_points|join:', ' }}</td> <td>{{ device.mount_points|join:', ' }}</td>
<td>
{% if device.mount_points %}
<form class="form" method="post"
action="{% url 'udiskie:eject' device.device|urlencode:"" %}">
{% csrf_token %}
<button type="submit"
class="btn btn-sm btn-default glyphicon glyphicon-eject">
</button>
</form>
{% endif %}
</td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>

View File

@ -18,14 +18,24 @@
Library for interacting with udisks2 D-Bus service. Library for interacting with udisks2 D-Bus service.
""" """
from django.utils.translation import ugettext as _
from plinth import utils from plinth import utils
from plinth.modules.storage import format_bytes 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(): def list_devices():
"""List devices that can be ejected.""" """List devices that can be ejected."""
udisks = utils.import_from_gi('UDisks', '2.0')
client = udisks.Client.new_sync() client = udisks.Client.new_sync()
object_manager = client.get_object_manager() object_manager = client.get_object_manager()
@ -60,3 +70,105 @@ def list_devices():
devices.append(device) devices.append(device)
return devices 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

View File

@ -20,12 +20,15 @@ URLs for the udiskie module.
from django.conf.urls import url from django.conf.urls import url
from .views import UdiskieView
from plinth.modules import udiskie from plinth.modules import udiskie
from .views import Index, eject
urlpatterns = [ urlpatterns = [
url(r'^sys/udiskie/$', url(
UdiskieView.as_view(service_id=udiskie.managed_services[0], r'^sys/udiskie/$',
description=udiskie.description, Index.as_view(service_id=udiskie.managed_services[0],
show_status_block=True), name='index'), description=udiskie.description, show_status_block=True),
name='index'),
url(r'^sys/udiskie/eject/(?P<device_path>[\w%]+)/$', eject, name='eject'),
] ]

View File

@ -18,17 +18,62 @@
Views for udiskie module. 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 plinth.views import ServiceView
from . import udisks2 from . import udisks2
logger = Logger(__name__)
class UdiskieView(ServiceView):
class Index(ServiceView):
"""View to show devices.""" """View to show devices."""
template_name = 'udiskie.html' template_name = 'udiskie.html'
def get_context_data(self, **kwargs): def get_context_data(self, *args, **kwargs):
"""Return the context data rendering the template.""" """Return the context data rendering the template."""
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context['devices'] = udisks2.list_devices() context['devices'] = udisks2.list_devices()
return context 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'))