mirror of
https://github.com/freedombox/FreedomBox.git
synced 2026-01-21 07:55:00 +00:00
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:
parent
5b37d0df8d
commit
ff9d061e98
137
actions/coquelicot
Executable file
137
actions/coquelicot
Executable 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()
|
||||
@ -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>
|
||||
1
data/etc/plinth/modules-enabled/coquelicot
Normal file
1
data/etc/plinth/modules-enabled/coquelicot
Normal file
@ -0,0 +1 @@
|
||||
plinth.modules.coquelicot
|
||||
@ -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)
|
||||
|
||||
|
||||
|
||||
133
plinth/modules/coquelicot/__init__.py
Normal file
133
plinth/modules/coquelicot/__init__.py
Normal 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
|
||||
37
plinth/modules/coquelicot/forms.py
Normal file
37
plinth/modules/coquelicot/forms.py
Normal 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)
|
||||
28
plinth/modules/coquelicot/manifest.py
Normal file
28
plinth/modules/coquelicot/manifest.py
Normal 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'
|
||||
}]
|
||||
}])
|
||||
27
plinth/modules/coquelicot/urls.py
Normal file
27
plinth/modules/coquelicot/urls.py
Normal 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'),
|
||||
]
|
||||
72
plinth/modules/coquelicot/views.py
Normal file
72
plinth/modules/coquelicot/views.py
Normal 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)
|
||||
BIN
static/themes/default/icons/coquelicot.png
Normal file
BIN
static/themes/default/icons/coquelicot.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 27 KiB |
Loading…
x
Reference in New Issue
Block a user