mirror of
https://github.com/freedombox/FreedomBox.git
synced 2026-05-20 10:34:30 +00:00
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:
parent
89f31db71f
commit
cdf07a92b4
@ -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>
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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'),
|
||||||
]
|
]
|
||||||
|
|||||||
@ -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'))
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user