mirror of
https://github.com/freedombox/FreedomBox.git
synced 2026-05-20 10:34:30 +00:00
deluge: Allow to set a download directory
- add directory selection form to the app configuration page - add debian-deluged user to the freedombox-share group - storage: new validator parameter check-creatable (because deluged is able to create subdirectories) Signed-off-by: Veiko Aasa <veiko17@disroot.org> Reviewed-by: James Valleroy <jvalleroy@mailbox.org>
This commit is contained in:
parent
e217a4c87e
commit
8e698987de
@ -20,6 +20,7 @@ Configuration helper for BitTorrent web client.
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
|
import json
|
||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
@ -80,6 +81,16 @@ def parse_arguments():
|
|||||||
|
|
||||||
subparsers.add_parser('setup', help='Setup deluge')
|
subparsers.add_parser('setup', help='Setup deluge')
|
||||||
|
|
||||||
|
subparsers.add_parser('get-configuration',
|
||||||
|
help='Return the current configuration')
|
||||||
|
|
||||||
|
subparser = subparsers.add_parser('set-configuration',
|
||||||
|
help='Set the configuration parameter')
|
||||||
|
subparser.add_argument('parameter',
|
||||||
|
help='Name of the configuration parameter')
|
||||||
|
subparser.add_argument('value',
|
||||||
|
help='Value of the configuration parameter')
|
||||||
|
|
||||||
subparsers.required = True
|
subparsers.required = True
|
||||||
return parser.parse_args()
|
return parser.parse_args()
|
||||||
|
|
||||||
@ -141,6 +152,29 @@ def _set_deluged_daemon_options():
|
|||||||
aug.save()
|
aug.save()
|
||||||
|
|
||||||
|
|
||||||
|
def subcommand_get_configuration(_):
|
||||||
|
"""Return the current deluged configuration in JSON format."""
|
||||||
|
deluged_conf_file = os.path.join(DELUGE_CONF_DIR, 'core.conf')
|
||||||
|
if config is None:
|
||||||
|
script = 'from deluge import config;\
|
||||||
|
conf = config.Config(filename="{0}");\
|
||||||
|
print(conf["download_location"])'.format(deluged_conf_file)
|
||||||
|
output = subprocess.check_output(['python2', '-c', script]).decode()
|
||||||
|
download_location = output.strip()
|
||||||
|
else:
|
||||||
|
conf = config.Config(filename=deluged_conf_file)
|
||||||
|
download_location = conf["download_location"]
|
||||||
|
|
||||||
|
print(json.dumps({'download_location': download_location}))
|
||||||
|
|
||||||
|
|
||||||
|
def subcommand_set_configuration(arguments):
|
||||||
|
"""Set the deluged configuration."""
|
||||||
|
if arguments.parameter != 'download_location':
|
||||||
|
return
|
||||||
|
_set_configuration('core.conf', arguments.parameter, arguments.value)
|
||||||
|
|
||||||
|
|
||||||
def subcommand_setup(_):
|
def subcommand_setup(_):
|
||||||
"""Perform initial setup for deluge."""
|
"""Perform initial setup for deluge."""
|
||||||
|
|
||||||
|
|||||||
@ -56,6 +56,9 @@ def parse_arguments():
|
|||||||
help='Validate a directory')
|
help='Validate a directory')
|
||||||
subparser.add_argument('--path', help='Path of the directory',
|
subparser.add_argument('--path', help='Path of the directory',
|
||||||
required=True)
|
required=True)
|
||||||
|
subparser.add_argument('--check-creatable', required=False, default=False,
|
||||||
|
action='store_true',
|
||||||
|
help='Check that the directory is creatable')
|
||||||
subparser.add_argument('--check-writable', required=False, default=False,
|
subparser.add_argument('--check-writable', required=False, default=False,
|
||||||
action='store_true',
|
action='store_true',
|
||||||
help='Check that the directory is writable')
|
help='Check that the directory is writable')
|
||||||
@ -329,14 +332,32 @@ def subcommand_usage_info(_):
|
|||||||
def subcommand_validate_directory(arguments):
|
def subcommand_validate_directory(arguments):
|
||||||
"""Validate a directory"""
|
"""Validate a directory"""
|
||||||
directory = arguments.path
|
directory = arguments.path
|
||||||
if not os.path.exists(directory):
|
|
||||||
print('ValidationError: 1')
|
def part_exists(path):
|
||||||
|
"""Returns part of the path that exists."""
|
||||||
|
if not path or os.path.exists(path):
|
||||||
|
return path
|
||||||
|
return part_exists(os.path.dirname(path))
|
||||||
|
|
||||||
|
if arguments.check_creatable:
|
||||||
|
directory = part_exists(directory)
|
||||||
|
if not directory:
|
||||||
|
directory = '.'
|
||||||
|
else:
|
||||||
|
if not os.path.exists(directory):
|
||||||
|
# doesn't exist
|
||||||
|
print('ValidationError: 1')
|
||||||
|
|
||||||
if not os.path.isdir(directory):
|
if not os.path.isdir(directory):
|
||||||
|
# is not a directory
|
||||||
print('ValidationError: 2')
|
print('ValidationError: 2')
|
||||||
if not os.access(directory, os.R_OK):
|
if not os.access(directory, os.R_OK):
|
||||||
|
# is not readable
|
||||||
print('ValidationError: 3')
|
print('ValidationError: 3')
|
||||||
if arguments.check_writable and not os.access(directory, os.W_OK):
|
if arguments.check_writable or arguments.check_creatable:
|
||||||
print('ValidationError: 4')
|
if not os.access(directory, os.W_OK):
|
||||||
|
# is not writable
|
||||||
|
print('ValidationError: 4')
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
|
|||||||
@ -26,11 +26,11 @@ from plinth import frontpage, menu
|
|||||||
from plinth.daemon import Daemon
|
from plinth.daemon import Daemon
|
||||||
from plinth.modules.apache.components import Webserver
|
from plinth.modules.apache.components import Webserver
|
||||||
from plinth.modules.firewall.components import Firewall
|
from plinth.modules.firewall.components import Firewall
|
||||||
from plinth.modules.users import register_group
|
from plinth.modules.users import register_group, add_user_to_share_group
|
||||||
|
|
||||||
from .manifest import backup, clients # noqa, pylint: disable=unused-import
|
from .manifest import backup, clients # noqa, pylint: disable=unused-import
|
||||||
|
|
||||||
version = 5
|
version = 6
|
||||||
|
|
||||||
managed_services = ['deluged', 'deluge-web']
|
managed_services = ['deluged', 'deluge-web']
|
||||||
|
|
||||||
@ -109,4 +109,5 @@ def setup(helper, old_version=None):
|
|||||||
"""Install and configure the module."""
|
"""Install and configure the module."""
|
||||||
helper.install(managed_packages)
|
helper.install(managed_packages)
|
||||||
helper.call('post', actions.superuser_run, 'deluge', ['setup'])
|
helper.call('post', actions.superuser_run, 'deluge', ['setup'])
|
||||||
|
add_user_to_share_group(reserved_usernames[0])
|
||||||
helper.call('post', app.enable)
|
helper.call('post', app.enable)
|
||||||
|
|||||||
37
plinth/modules/deluge/forms.py
Normal file
37
plinth/modules/deluge/forms.py
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
#
|
||||||
|
# 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 <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
"""
|
||||||
|
Forms for Deluge app.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
|
from plinth.modules.deluge import reserved_usernames
|
||||||
|
from plinth.modules.storage.forms import (DirectorySelectForm,
|
||||||
|
DirectoryValidator)
|
||||||
|
|
||||||
|
|
||||||
|
class DelugeForm(DirectorySelectForm):
|
||||||
|
"""Deluge configuration form"""
|
||||||
|
|
||||||
|
def __init__(self, *args, **kw):
|
||||||
|
validator = DirectoryValidator(username=reserved_usernames[0],
|
||||||
|
check_creatable=True)
|
||||||
|
super(DelugeForm, self).__init__(
|
||||||
|
title=_('Download directory'),
|
||||||
|
default='/var/lib/deluged/Downloads/', validator=validator, *args,
|
||||||
|
**kw)
|
||||||
@ -20,14 +20,8 @@ URLs for the Deluge module.
|
|||||||
|
|
||||||
from django.conf.urls import url
|
from django.conf.urls import url
|
||||||
|
|
||||||
from plinth.modules import deluge
|
from .views import DelugeAppView
|
||||||
from plinth.views import AppView
|
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
url(
|
url(r'^apps/deluge/$', DelugeAppView.as_view(), name='index')
|
||||||
r'^apps/deluge/$',
|
|
||||||
AppView.as_view(name=deluge.name, description=deluge.description,
|
|
||||||
clients=deluge.clients, app_id='deluge',
|
|
||||||
manual_page=deluge.manual_page,
|
|
||||||
icon_filename=deluge.icon_filename), name='index'),
|
|
||||||
]
|
]
|
||||||
|
|||||||
69
plinth/modules/deluge/views.py
Normal file
69
plinth/modules/deluge/views.py
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
#
|
||||||
|
# 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 <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
"""
|
||||||
|
Django views for Deluge.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
|
||||||
|
from django.contrib import messages
|
||||||
|
from django.utils.translation import ugettext as _
|
||||||
|
|
||||||
|
from plinth import actions, views
|
||||||
|
from plinth.modules import deluge
|
||||||
|
|
||||||
|
from .forms import DelugeForm
|
||||||
|
|
||||||
|
|
||||||
|
class DelugeAppView(views.AppView):
|
||||||
|
"""Serve configuration page."""
|
||||||
|
clients = deluge.clients
|
||||||
|
name = deluge.name
|
||||||
|
description = deluge.description
|
||||||
|
diagnostics_module_name = 'deluge'
|
||||||
|
form_class = DelugeForm
|
||||||
|
app_id = 'deluge'
|
||||||
|
manual_page = deluge.manual_page
|
||||||
|
icon_filename = deluge.icon_filename
|
||||||
|
|
||||||
|
def get_initial(self):
|
||||||
|
"""Get current Deluge server settings."""
|
||||||
|
status = super().get_initial()
|
||||||
|
configuration = json.loads(
|
||||||
|
actions.superuser_run('deluge', ['get-configuration']))
|
||||||
|
status['storage_path'] = os.path.normpath(
|
||||||
|
configuration['download_location'])
|
||||||
|
return status
|
||||||
|
|
||||||
|
def form_valid(self, form):
|
||||||
|
"""Apply the changes submitted in the form."""
|
||||||
|
old_status = form.initial
|
||||||
|
new_status = form.cleaned_data
|
||||||
|
|
||||||
|
# don't change the configuration if the application was disabled
|
||||||
|
if new_status['is_enabled'] or not old_status['is_enabled']:
|
||||||
|
if old_status['storage_path'] != new_status['storage_path']:
|
||||||
|
new_configuration = [
|
||||||
|
'download_location', new_status['storage_path']
|
||||||
|
]
|
||||||
|
|
||||||
|
actions.superuser_run(
|
||||||
|
'deluge', ['set-configuration'] + new_configuration)
|
||||||
|
messages.success(self.request, _('Configuration updated'))
|
||||||
|
|
||||||
|
return super().form_valid(form)
|
||||||
@ -60,14 +60,18 @@ def is_module_enabled(name):
|
|||||||
class DirectoryValidator:
|
class DirectoryValidator:
|
||||||
username = None
|
username = None
|
||||||
check_writable = False
|
check_writable = False
|
||||||
|
check_creatable = False
|
||||||
add_user_to_share_group = False
|
add_user_to_share_group = False
|
||||||
service_to_restart = None
|
service_to_restart = None
|
||||||
|
|
||||||
def __init__(self, username=None, check_writable=None):
|
def __init__(self, username=None, check_writable=None,
|
||||||
|
check_creatable=None):
|
||||||
if username is not None:
|
if username is not None:
|
||||||
self.username = username
|
self.username = username
|
||||||
if check_writable is not None:
|
if check_writable is not None:
|
||||||
self.check_writable = check_writable
|
self.check_writable = check_writable
|
||||||
|
if check_creatable is not None:
|
||||||
|
self.check_creatable = check_creatable
|
||||||
|
|
||||||
def __call__(self, value):
|
def __call__(self, value):
|
||||||
"""Validate a directory."""
|
"""Validate a directory."""
|
||||||
@ -75,7 +79,9 @@ class DirectoryValidator:
|
|||||||
raise ValidationError(_('Invalid directory name.'), 'invalid')
|
raise ValidationError(_('Invalid directory name.'), 'invalid')
|
||||||
|
|
||||||
command = ['validate-directory', '--path', value]
|
command = ['validate-directory', '--path', value]
|
||||||
if self.check_writable:
|
if self.check_creatable:
|
||||||
|
command.append('--check-creatable')
|
||||||
|
elif self.check_writable:
|
||||||
command.append('--check-writable')
|
command.append('--check-writable')
|
||||||
|
|
||||||
if self.username:
|
if self.username:
|
||||||
|
|||||||
@ -247,11 +247,14 @@ class TestActions:
|
|||||||
subprocess.run(command, stdout=subprocess.DEVNULL,
|
subprocess.run(command, stdout=subprocess.DEVNULL,
|
||||||
stderr=subprocess.DEVNULL, check=True)
|
stderr=subprocess.DEVNULL, check=True)
|
||||||
|
|
||||||
def assert_validate_directory(self, path, error, check_writable=False):
|
def assert_validate_directory(self, path, error, check_writable=False,
|
||||||
|
check_creatable=False):
|
||||||
"""Perform directory validation checks."""
|
"""Perform directory validation checks."""
|
||||||
action_command = ['storage', 'validate-directory', '--path', path]
|
action_command = ['storage', 'validate-directory', '--path', path]
|
||||||
if check_writable:
|
if check_writable:
|
||||||
action_command += ['--check-writable']
|
action_command += ['--check-writable']
|
||||||
|
if check_creatable:
|
||||||
|
action_command += ['--check-creatable']
|
||||||
proc = self.call_action(action_command, stderr=subprocess.PIPE,
|
proc = self.call_action(action_command, stderr=subprocess.PIPE,
|
||||||
stdout=subprocess.PIPE)
|
stdout=subprocess.PIPE)
|
||||||
output = proc.stdout.decode()
|
output = proc.stdout.decode()
|
||||||
@ -291,3 +294,16 @@ class TestActions:
|
|||||||
"""Test that directory writable validation returns expected output."""
|
"""Test that directory writable validation returns expected output."""
|
||||||
self.assert_validate_directory(directory['path'], directory['error'],
|
self.assert_validate_directory(directory['path'], directory['error'],
|
||||||
check_writable=True)
|
check_writable=True)
|
||||||
|
|
||||||
|
@pytest.mark.usefixtures('needs_not_root')
|
||||||
|
@pytest.mark.parametrize('directory', [{
|
||||||
|
'path': '/var/lib/plinth_storage_test_not_exists',
|
||||||
|
'error': '4'
|
||||||
|
}, {
|
||||||
|
'path': '/tmp/plint_storage_test_not_exists',
|
||||||
|
'error': ''
|
||||||
|
}])
|
||||||
|
def test_validate_directory_creatable(self, directory):
|
||||||
|
"""Test that directory creatable validation returns expected output."""
|
||||||
|
self.assert_validate_directory(directory['path'], directory['error'],
|
||||||
|
check_creatable=True)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user