diff --git a/actions/udiskie b/actions/udiskie
new file mode 100755
index 000000000..2b249a163
--- /dev/null
+++ b/actions/udiskie
@@ -0,0 +1,60 @@
+#!/usr/bin/python3
+# -*- mode: python -*-
+#
+# 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 .
+#
+"""
+Configuration helper for udiskie.
+"""
+
+import argparse
+
+from plinth import action_utils
+
+
+def parse_arguments():
+ """Return parsed command line arguments as dictionary."""
+ parser = argparse.ArgumentParser()
+ subparsers = parser.add_subparsers(dest='subcommand', help='Sub command')
+
+ subparsers.add_parser('enable', help='Enable udiskie')
+ subparsers.add_parser('disable', help='Disable udiskie')
+
+ subparsers.required = True
+ return parser.parse_args()
+
+
+def subcommand_enable(_):
+ """Enable web configuration and reload."""
+ action_utils.service_enable('freedombox-udiskie')
+
+
+def subcommand_disable(_):
+ """Disable web configuration and reload."""
+ action_utils.service_disable('freedombox-udiskie')
+
+
+def main():
+ """Parse arguments and perform all duties."""
+ arguments = parse_arguments()
+
+ subcommand = arguments.subcommand.replace('-', '_')
+ subcommand_method = globals()['subcommand_' + subcommand]
+ subcommand_method(arguments)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/data/etc/plinth/modules-enabled/udiskie b/data/etc/plinth/modules-enabled/udiskie
new file mode 100644
index 000000000..bb489cbd2
--- /dev/null
+++ b/data/etc/plinth/modules-enabled/udiskie
@@ -0,0 +1 @@
+plinth.modules.udiskie
diff --git a/data/lib/systemd/system/freedombox-udiskie.service b/data/lib/systemd/system/freedombox-udiskie.service
new file mode 100644
index 000000000..a905d999c
--- /dev/null
+++ b/data/lib/systemd/system/freedombox-udiskie.service
@@ -0,0 +1,26 @@
+#
+# 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 .
+#
+
+[Unit]
+Description=handle automounting
+Documentation=man:udiskie(1)
+
+[Service]
+ExecStart=/usr/bin/udiskie
+
+[Install]
+WantedBy=multi-user.target
diff --git a/debian/control b/debian/control
index ec78660dd..c6128f01c 100644
--- a/debian/control
+++ b/debian/control
@@ -25,6 +25,7 @@ Build-Depends: debhelper (>= 11~)
, python3-cherrypy3
, python3-configobj
, python3-coverage
+ , python3-dbus
, python3-django (>= 1.11)
, python3-django-axes (>= 3.0.3)
, python3-django-captcha
@@ -69,6 +70,7 @@ Depends: ${python3:Depends}
, python3-bootstrapform
, python3-cherrypy3
, python3-configobj
+ , python3-dbus
, python3-django (>= 1.11)
, python3-django-axes (>= 3.0.3)
, python3-django-captcha
diff --git a/plinth/modules/udiskie/__init__.py b/plinth/modules/udiskie/__init__.py
new file mode 100644
index 000000000..70e41ddf5
--- /dev/null
+++ b/plinth/modules/udiskie/__init__.py
@@ -0,0 +1,147 @@
+#
+# 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 .
+#
+"""
+FreedomBox app for udiskie.
+"""
+
+import dbus
+from django.utils.translation import ugettext_lazy as _
+
+from plinth import service as service_module
+from plinth import action_utils, actions
+from plinth.menu import main_menu
+from plinth.modules.storage import format_bytes
+
+version = 1
+
+managed_services = ['freedombox-udiskie']
+
+managed_packages = ['udiskie']
+
+name = _('udiskie')
+
+short_description = _('Removable Media')
+
+description = [
+ _('udiskie allows automatic mounting of removable media, such as flash '
+ 'drives.'),
+]
+
+service = None
+
+
+def init():
+ """Intialize the module."""
+ menu = main_menu.get('system')
+ menu.add_urlname(name, 'glyphicon-floppy-disk', 'udiskie:index',
+ short_description)
+
+ global service
+ setup_helper = globals()['setup_helper']
+ if setup_helper.get_state() != 'needs-setup':
+ service = service_module.Service(
+ managed_services[0], name, ports=[], is_external=False,
+ is_enabled=is_enabled, enable=enable, disable=disable,
+ is_running=is_running)
+
+
+def setup(helper, old_version=None):
+ """Install and configure the module."""
+ helper.install(managed_packages, skip_recommends=True)
+ helper.call('post', actions.superuser_run, 'udiskie', ['enable'])
+ global service
+ if service is None:
+ service = service_module.Service(
+ managed_services[0], name, ports=[], is_external=True,
+ is_enabled=is_enabled, enable=enable, disable=disable,
+ is_running=is_running)
+ helper.call('post', service.notify_enabled, None, True)
+
+
+def is_running():
+ """Return whether the service is running."""
+ return action_utils.service_is_running('freedombox-udiskie')
+
+
+def is_enabled():
+ """Return whether the module is enabled."""
+ return action_utils.service_is_enabled('freedombox-udiskie')
+
+
+def enable():
+ """Enable the module."""
+ actions.superuser_run('udiskie', ['enable'])
+
+
+def disable():
+ """Disable the module."""
+ actions.superuser_run('udiskie', ['disable'])
+
+
+def list_devices():
+ UDISKS2 = 'org.freedesktop.UDisks2'
+ UDISKS2_PATH = '/org/freedesktop/UDisks2'
+ BLOCK = UDISKS2 + '.Block'
+ PROPERTIES = 'org.freedesktop.DBus.Properties'
+
+ devices = []
+ bus = dbus.SystemBus()
+ udisks_obj = bus.get_object(UDISKS2, UDISKS2_PATH)
+ manager = dbus.Interface(udisks_obj, 'org.freedesktop.DBus.ObjectManager')
+ for k, v in manager.GetManagedObjects().items():
+ drive_info = v.get(BLOCK, {})
+ if drive_info.get('IdUsage') == "filesystem" \
+ and not drive_info.get('HintSystem') \
+ and not drive_info.get('ReadOnly'):
+ device_name = drive_info.get('Device')
+ if device_name:
+ device_name = bytearray(device_name).replace(
+ b'\x00', b'').decode('utf-8')
+ short_name = device_name.replace('/dev', '', 1)
+ bd = bus.get_object(
+ UDISKS2, UDISKS2_PATH + '/block_devices%s' % short_name)
+ drive_name = bd.Get(BLOCK, 'Drive', dbus_interface=PROPERTIES)
+ drive = bus.get_object(UDISKS2, drive_name)
+ ejectable = drive.Get(UDISKS2 + '.Drive', 'Ejectable',
+ dbus_interface=PROPERTIES)
+ if ejectable:
+ label = bd.Get(BLOCK, 'IdLabel', dbus_interface=PROPERTIES)
+ size = bd.Get(BLOCK, 'Size', dbus_interface=PROPERTIES)
+ file_system = bd.Get(BLOCK, 'IdType',
+ dbus_interface=PROPERTIES)
+ try:
+ mount_points = bd.Get(UDISKS2 + '.Filesystem',
+ 'MountPoints',
+ dbus_interface=PROPERTIES)
+ mount_point = mount_points[0]
+ except:
+ mount_point = None
+
+ devices.append({
+ 'device':
+ device_name,
+ 'label':
+ str(label),
+ 'size':
+ format_bytes(size),
+ 'file_system':
+ str(file_system),
+ 'mount_point':
+ ''.join([chr(ch) for ch in mount_point]),
+ })
+
+ return devices
diff --git a/plinth/modules/udiskie/templates/udiskie.html b/plinth/modules/udiskie/templates/udiskie.html
new file mode 100644
index 000000000..fd3270bc8
--- /dev/null
+++ b/plinth/modules/udiskie/templates/udiskie.html
@@ -0,0 +1,98 @@
+{% extends "base.html" %}
+{% comment %}
+#
+# 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 .
+#
+{% endcomment %}
+
+{% load bootstrap %}
+{% load i18n %}
+{% load static %}
+
+{% block content %}
+
+
{{ service.name }}
+
+ {% for paragraph in description %}
+ {{ paragraph|safe }}
+ {% endfor %}
+
+ {% if manual_page %}
+
+
+ {% trans 'Learn more...' %}
+
+
+ {% endif %}
+
+ {% trans "Devices" %}
+
+
+
+
+
+ | {% trans "Device" %} |
+ {% trans "Label" %} |
+ {% trans "Size" %} |
+ {% trans "File System" %} |
+ {% trans "Mount Point" %} |
+
+
+
+ {% for device in devices %}
+
+ | {{ device.device }} |
+ {{ device.label }} |
+ {{ device.size }} |
+ {{ device.file_system }} |
+ {{ device.mount_point }} |
+
+ {% endfor %}
+
+
+
+
+
+ {% if show_status_block %}
+ {% trans "Status" %}
+
+ {% with service_name=service.name %}
+ {% if service.is_running %}
+
+ {% blocktrans trimmed %}
+ Service {{ service_name }} is running.
+ {% endblocktrans %}
+ {% else %}
+
+ {% blocktrans trimmed %}
+ Service {{ service_name }} is not running.
+ {% endblocktrans %}
+ {% endif %}
+ {% endwith %}
+
+ {% endif %}
+
+ {% trans "Configuration" %}
+
+
+{% endblock %}
diff --git a/plinth/modules/udiskie/urls.py b/plinth/modules/udiskie/urls.py
new file mode 100644
index 000000000..c1570fbe4
--- /dev/null
+++ b/plinth/modules/udiskie/urls.py
@@ -0,0 +1,31 @@
+#
+# 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 .
+#
+"""
+URLs for the udiskie module.
+"""
+
+from django.conf.urls import url
+
+from .views import UdiskieView
+from plinth.modules import udiskie
+
+urlpatterns = [
+ url(r'^sys/udiskie/$',
+ UdiskieView.as_view(service_id=udiskie.managed_services[0],
+ description=udiskie.description,
+ show_status_block=True), name='index'),
+]
diff --git a/plinth/modules/udiskie/views.py b/plinth/modules/udiskie/views.py
new file mode 100644
index 000000000..5d146a6e3
--- /dev/null
+++ b/plinth/modules/udiskie/views.py
@@ -0,0 +1,32 @@
+#
+# 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 .
+#
+"""
+Views for udiskie module.
+"""
+
+from plinth.modules import udiskie
+from plinth.views import ServiceView
+
+
+class UdiskieView(ServiceView):
+ template_name = 'udiskie.html'
+
+ def get_context_data(self, **kwargs):
+ """Return the context data rendering the template."""
+ context = super().get_context_data(**kwargs)
+ context['devices'] = udiskie.list_devices()
+ return context
diff --git a/setup.py b/setup.py
index 525fb1e37..e30d4c1e4 100755
--- a/setup.py
+++ b/setup.py
@@ -241,7 +241,7 @@ setuptools.setup(
['data/etc/NetworkManager/dispatcher.d/10-freedombox-batman']),
('/etc/sudoers.d', ['data/etc/sudoers.d/plinth']),
('/lib/systemd/system',
- ['data/lib/systemd/system/plinth.service']),
+ glob.glob('data/lib/systemd/system/*.service')),
('/usr/share/plinth/actions',
glob.glob(os.path.join('actions', '*'))),
('/usr/share/polkit-1/rules.d',