Add file-sharing application Coquelicot to FreedomBox

- Add settings in Service View
- Fixes for maximum file setting
- Don't allow negative values for max. file size in UI
- Minor text changes to django messages
- Minor correction to maximum file size calculation
- Rename apache conf file to coquelicot-freedombox.conf
- Remove all hacks to adjust file size.
- Fix permissions issues for settings file
- Show status block in UI
- try-restart on settings change instead of restart

Signed-off-by: Joseph Nuthalapati <njoseph@thoughtworks.com>
Reviewed-by: James Valleroy <jvalleroy@mailbox.org>
This commit is contained in:
Joseph Nuthalapati 2018-01-28 18:26:12 +05:30 committed by James Valleroy
parent 5b37d0df8d
commit ff9d061e98
No known key found for this signature in database
GPG Key ID: 77C0C75E7B650808
10 changed files with 455 additions and 20 deletions

137
actions/coquelicot Executable file
View File

@ -0,0 +1,137 @@
#!/usr/bin/python3
# -*- mode: python -*-
#
# This file is part of Plinth.
#
# 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 <http://www.gnu.org/licenses/>.
#
"""
Configuration helper for coquelicot.
"""
import argparse
import hashlib
import os
import sys
import yaml
from plinth import action_utils
SETTINGS_FILE = '/etc/coquelicot/settings.yml'
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('setup',
help='Post-installation operations for coquelicot')
subparsers.add_parser('enable', help='Enable coquelicot')
subparsers.add_parser('disable', help='Disable coquelicot')
subparsers.add_parser(
'set-upload-password',
help='Set a new global, pre-shared password for uploading files')
max_file_size = subparsers.add_parser(
'set-max-file-size',
help='Change the maximum size of the files that can be uploaded to '
'Coquelicot')
max_file_size.add_argument('size', type=int, help='upload file size in MB')
subparsers.add_parser(
'get-max-file-size',
help='Print the maximum size of the files that can be uploaded to '
'Coquelicot')
subparsers.required = True
return parser.parse_args()
def subcommand_setup(_):
"""Perform post-installation operations for coquelicot."""
settings = read_settings()
settings['path'] = "/coquelicot"
settings['max_file_size'] = mebibytes(1024)
write_settings(settings)
action_utils.service_restart('coquelicot')
def subcommand_enable(_):
"""Enable web configuration and reload."""
action_utils.service_enable('coquelicot')
action_utils.webserver_enable('coquelicot-freedombox')
def subcommand_disable(_):
"""Disable web configuration and reload."""
action_utils.webserver_disable('coquelicot-freedombox')
action_utils.service_disable('coquelicot')
def subcommand_set_upload_password(arguments):
"""Set a new upload password for Coquelicot."""
upload_password = ''.join(sys.stdin)
settings = read_settings()
hashed_pw = hashlib.sha1(upload_password.encode()).hexdigest()
settings['authentication_method']['upload_password'] = hashed_pw
write_settings(settings)
action_utils.service_try_restart('coquelicot')
def subcommand_set_max_file_size(arguments):
"""Set a new maximum file size for Coquelicot."""
size_in_bytes = mebibytes(arguments.size)
settings = read_settings()
settings['max_file_size'] = size_in_bytes
write_settings(settings)
action_utils.service_try_restart('coquelicot')
def subcommand_get_max_file_size(_):
"""Print the maximum file size to stdout."""
if os.path.exists(SETTINGS_FILE):
settings = read_settings()
print(int(settings['max_file_size'] / (1024 * 1024)))
else:
print(-1)
def read_settings():
with open(SETTINGS_FILE, 'rb') as settings_file:
return yaml.load(settings_file)
def write_settings(settings):
with open(SETTINGS_FILE, 'w') as settings_file:
yaml.dump(settings, settings_file)
def main():
"""Parse arguments and perform all duties."""
arguments = parse_arguments()
subcommand = arguments.subcommand.replace('-', '_')
subcommand_method = globals()['subcommand_' + subcommand]
subcommand_method(arguments)
def mebibytes(size):
"""Return the given size of mebibytes in bytes."""
return size * 1024 * 1024
if __name__ == '__main__':
main()

View File

@ -0,0 +1,5 @@
<Location /coquelicot>
ProxyPass http://127.0.0.1:51161/coquelicot
SetEnv proxy-sendchunks 1
RequestHeader set X-Forwarded-SSL "on"
</Location>

View File

@ -0,0 +1 @@
plinth.modules.coquelicot

View File

@ -99,41 +99,36 @@ def service_unmask(service_name):
def service_start(service_name):
"""Start a service with systemd or sysvinit."""
if is_systemd_running():
subprocess.run(['systemctl', 'start', service_name],
stdout=subprocess.DEVNULL)
else:
subprocess.run(['service', service_name, 'start'],
stdout=subprocess.DEVNULL)
service_action(service_name, 'start')
def service_stop(service_name):
"""Stop a service with systemd or sysvinit."""
if is_systemd_running():
subprocess.run(['systemctl', 'stop', service_name],
stdout=subprocess.DEVNULL)
else:
subprocess.run(['service', service_name, 'stop'],
stdout=subprocess.DEVNULL)
service_action(service_name, 'stop')
def service_restart(service_name):
"""Restart a service with systemd or sysvinit."""
if is_systemd_running():
subprocess.run(['systemctl', 'restart', service_name],
stdout=subprocess.DEVNULL)
else:
subprocess.run(['service', service_name, 'restart'],
stdout=subprocess.DEVNULL)
service_action(service_name, 'restart')
def service_try_restart(service_name):
"""Try to restart a service with systemd or sysvinit."""
service_action(service_name, 'try-restart')
def service_reload(service_name):
"""Reload a service with systemd or sysvinit."""
service_action(service_name, 'reload')
def service_action(service_name, action):
"""Preform the given action on the service_name."""
if is_systemd_running():
subprocess.run(['systemctl', 'reload', service_name],
subprocess.run(['systemctl', action, service_name],
stdout=subprocess.DEVNULL)
else:
subprocess.run(['service', service_name, 'reload'],
subprocess.run(['service', service_name, action],
stdout=subprocess.DEVNULL)

View File

@ -0,0 +1,133 @@
#
# This file is part of Plinth.
#
# 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 <http://www.gnu.org/licenses/>.
#
"""
Plinth module to configure coquelicot.
"""
from django.utils.translation import ugettext_lazy as _
from plinth import service as service_module
from plinth import action_utils, actions, frontpage
from plinth.menu import main_menu
from .manifest import clients
clients = clients
version = 1
managed_services = ['coquelicot']
managed_packages = ['coquelicot']
name = _('Coquelicot')
short_description = _('File Sharing')
description = [
_('Coquelicot is a “one-click” file sharing web application with a focus '
'on protecting users privacy. It is best used for quickly sharing a '
'single file. '),
_('This Coquelicot instance is exposed to the public but requires an '
'upload password to prevent unauthorized access. You can set a new '
'upload password in the form that will appear below after installation. '
'The default upload password is "test".')
]
service = None
def init():
"""Intialize the module."""
menu = main_menu.get('apps')
menu.add_urlname(name, 'glyphicon-open-file', 'coquelicot: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=[
'http', 'https'
], is_external=True, is_enabled=is_enabled, enable=enable,
disable=disable,
is_running=is_running)
if is_enabled():
add_shortcut()
def setup(helper, old_version=None):
"""Install and configure the module."""
helper.install(managed_packages)
helper.call('post', actions.superuser_run, 'coquelicot', ['setup'])
helper.call('post', actions.superuser_run, 'coquelicot', ['enable'])
global service
if service is None:
service = service_module.Service(managed_services[0], name, ports=[
'http', 'https'
], is_external=True, is_enabled=is_enabled, enable=enable,
disable=disable,
is_running=is_running)
helper.call('post', service.notify_enabled, None, True)
helper.call('post', add_shortcut)
def add_shortcut():
"""Helper method to add a shortcut to the frontpage."""
frontpage.add_shortcut('coquelicot', name,
short_description=short_description,
url='/coquelicot', login_required=True)
def is_running():
"""Return whether the service is running."""
return action_utils.service_is_running('coquelicot')
def is_enabled():
"""Return whether the module is enabled."""
return (action_utils.service_is_enabled('coquelicot')
and action_utils.webserver_is_enabled('coquelicot-freedombox'))
def enable():
"""Enable the module."""
actions.superuser_run('coquelicot', ['enable'])
add_shortcut()
def disable():
"""Disable the module."""
actions.superuser_run('coquelicot', ['disable'])
frontpage.remove_shortcut('coquelicot')
def get_current_max_file_size():
"""Get the current value of maximum file size."""
size = actions.superuser_run('coquelicot', ['get-max-file-size'])
return int(size.strip())
def diagnose():
"""Run diagnostics and return the results."""
results = []
results.extend(
action_utils.diagnose_url_on_all('https://{host}/coquelicot',
check_certificate=False))
return results

View File

@ -0,0 +1,37 @@
#
# This file is part of Plinth.
#
# 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 <http://www.gnu.org/licenses/>.
#
"""
Plinth form for configuring Coquelicot.
"""
from django import forms
from django.utils.translation import ugettext_lazy as _
from plinth.forms import ServiceForm
class CoquelicotForm(ServiceForm): # pylint: disable=W0232
"""Coquelicot configuration form."""
upload_password = forms.CharField(
label=_('Upload Password'),
help_text=_('Set a new upload password for Coquelicot. '
'Leave this field blank to keep the current password.'),
required=False, widget=forms.PasswordInput)
max_file_size = forms.IntegerField(
label=_("Maximum File Size (in MiB)"), help_text=_(
'Set the maximum size of the files that can be uploaded to '
'Coquelicot.'), required=False, min_value=0)

View File

@ -0,0 +1,28 @@
#
# This file is part of Plinth.
#
# 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 <http://www.gnu.org/licenses/>.
#
from django.utils.translation import ugettext_lazy as _
from plinth.clients import validate
clients = validate([{
'name': _('coquelicot'),
'platforms': [{
'type': 'web',
'url': '/coquelicot'
}]
}])

View File

@ -0,0 +1,27 @@
#
# This file is part of Plinth.
#
# 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 <http://www.gnu.org/licenses/>.
#
"""
URLs for the coquelicot module.
"""
from django.conf.urls import url
from .views import CoquelicotServiceView
urlpatterns = [
url(r'^apps/coquelicot/$', CoquelicotServiceView.as_view(), name='index'),
]

View File

@ -0,0 +1,72 @@
#
# This file is part of Plinth.
#
# 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 <http://www.gnu.org/licenses/>.
#
"""
Plinth views for Coquelicot.
"""
from django.contrib import messages
from django.utils.translation import ugettext as _
from plinth import actions, views
from plinth.errors import ActionError
from plinth.modules.coquelicot import (clients, description,
get_current_max_file_size)
from .forms import CoquelicotForm
class CoquelicotServiceView(views.ServiceView):
"""Serve configuration page."""
clients = clients
description = description
diagnostics_module_name = 'coquelicot'
service_id = 'coquelicot'
form_class = CoquelicotForm
show_status_block = True
def get_initial(self):
"""Return the status of the service to fill in the form."""
initial = super().get_initial()
initial['max_file_size'] = get_current_max_file_size()
return initial
def form_valid(self, form):
"""Apply the changes submitted in the form."""
form_data = form.cleaned_data
if form_data['upload_password']:
try:
actions.superuser_run(
'coquelicot', ['set-upload-password'],
input=form_data['upload_password'].encode())
messages.success(self.request, _('Upload password updated'))
except ActionError as e:
messages.error(self.request,
_('Failed to update upload password'))
max_file_size = form_data['max_file_size']
if max_file_size and max_file_size != get_current_max_file_size():
try:
actions.superuser_run(
'coquelicot', ['set-max-file-size',
str(max_file_size)])
messages.success(self.request, _('Maximum file size updated'))
except ActionError as e:
messages.error(self.request,
_('Failed to update maximum file size'))
return super().form_valid(form)

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB