From f94d0d5414faf6f76ee420f460a041e14bd6cf88 Mon Sep 17 00:00:00 2001 From: Sunil Mohan Adapa Date: Sun, 12 Apr 2015 18:32:14 +0530 Subject: [PATCH] transmission: New module for BitTorrent downloads --- actions/transmission | 150 ++++++++++++++++++ data/etc/plinth/modules-enabled/transmission | 1 + .../services/transmission-rpcplinth.xml | 6 + plinth/modules/transmission/__init__.py | 46 ++++++ plinth/modules/transmission/forms.py | 62 ++++++++ .../transmission/templates/transmission.html | 55 +++++++ plinth/modules/transmission/urls.py | 28 ++++ plinth/modules/transmission/views.py | 111 +++++++++++++ 8 files changed, 459 insertions(+) create mode 100755 actions/transmission create mode 100644 data/etc/plinth/modules-enabled/transmission create mode 100644 data/usr/lib/firewalld/services/transmission-rpcplinth.xml create mode 100644 plinth/modules/transmission/__init__.py create mode 100644 plinth/modules/transmission/forms.py create mode 100644 plinth/modules/transmission/templates/transmission.html create mode 100644 plinth/modules/transmission/urls.py create mode 100644 plinth/modules/transmission/views.py diff --git a/actions/transmission b/actions/transmission new file mode 100755 index 000000000..1290b3d62 --- /dev/null +++ b/actions/transmission @@ -0,0 +1,150 @@ +#!/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 . +# + +""" +Configuration helper for Transmission daemon. +""" + +import argparse +import json +import subprocess + + +SERVICE_CONFIG = '/etc/default/transmission-daemon' +TRANSMISSION_CONFIG = '/etc/transmission-daemon/settings.json' + + +def parse_arguments(): + """Return parsed command line arguments as dictionary.""" + parser = argparse.ArgumentParser() + subparsers = parser.add_subparsers(dest='subcommand', help='Sub command') + + # Get whether service is enabled + subparsers.add_parser('get-enabled', + help='Get whether Transmission service is enabled') + + # Enable service + subparsers.add_parser('enable', help='Enable Transmission service') + + # Disable service + subparsers.add_parser('disable', help='Disable Transmission service') + + # Get whether daemon is running + subparsers.add_parser('is-running', + help='Get whether Transmission daemon is running') + + # Get currently configured Tor hidden service information + merge_configuration = subparsers.add_parser( + 'merge-configuration', + help='Merge given JSON configration with existing') + merge_configuration.add_argument( + 'configuration', + help='JSON encoded configuration to merge') + + return parser.parse_args() + + +def subcommand_get_enabled(_): + """Get whether Transmission service is enabled.""" + try: + with open(SERVICE_CONFIG, 'r') as file_handle: + for line in file_handle: + if line.startswith('ENABLE_DAEMON'): + value = line.split('=')[1].strip() + print('yes' if int(value) else 'no') + return + except IOError: + pass + + print('no') + + +def subcommand_enable(_): + """Start Transmission service.""" + set_service_enable(enable=True) + subprocess.call(['service', 'transmission-daemon', 'start']) + + +def subcommand_disable(_): + """Stop Transmission service.""" + subprocess.call(['service', 'transmission-daemon', 'stop']) + set_service_enable(enable=False) + + +def set_service_enable(enable): + """Enable/disable Transmission daemon; enable: boolean.""" + newline = 'ENABLE_DAEMON=1\n' if enable else 'ENABLE_DAEMON=0\n' + + with open(SERVICE_CONFIG, 'r') as file_handle: + lines = file_handle.readlines() + for index, line in enumerate(lines): + if line.startswith('ENABLE_DAEMON'): + lines[index] = newline + break + + with open(SERVICE_CONFIG, 'w') as file_handle: + file_handle.writelines(lines) + + +def subcommand_is_running(_): + """Get whether Transmission is running.""" + try: + output = subprocess.check_output(['service', 'transmission-daemon', + 'status']) + except subprocess.CalledProcessError: + # If daemon is not running we get a status code != 0 and a + # CalledProcessError + print('no') + else: + running = False + for line in output.decode().split('\n'): + if 'Active' in line and 'running' in line: + running = True + break + + print('yes' if running else 'no') + + +def subcommand_merge_configuration(arguments): + """Merge given JSON configuration with existing configuration.""" + configuration = arguments.configuration + configuration = json.loads(configuration) + + current_configuration = open(TRANSMISSION_CONFIG, 'r').read() + current_configuration = json.loads(current_configuration) + + new_configuration = current_configuration + new_configuration.update(configuration) + new_configuration = json.dumps(new_configuration, indent=4, sort_keys=True) + + open(TRANSMISSION_CONFIG, 'w').write(new_configuration) + subprocess.call(['service', 'transmission-daemon', 'reload']) + + +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/transmission b/data/etc/plinth/modules-enabled/transmission new file mode 100644 index 000000000..4b3f272d7 --- /dev/null +++ b/data/etc/plinth/modules-enabled/transmission @@ -0,0 +1 @@ +plinth.modules.transmission diff --git a/data/usr/lib/firewalld/services/transmission-rpcplinth.xml b/data/usr/lib/firewalld/services/transmission-rpcplinth.xml new file mode 100644 index 000000000..ecfbcb235 --- /dev/null +++ b/data/usr/lib/firewalld/services/transmission-rpcplinth.xml @@ -0,0 +1,6 @@ + + + Transmission Web Interface + Transmission is a client for BitTorrent, the peer-to-peer file sharing protocol. Transmission can run as a GUI client or as a daemon process running in the background. Both provide a web interface allowing themselves to be controlled using a web browser. Also, both can be controlled using a command line interface. Enable this if you wish to control Transmission BitTorrent GUI or daemon using its web interface or the command line utility. + + diff --git a/plinth/modules/transmission/__init__.py b/plinth/modules/transmission/__init__.py new file mode 100644 index 000000000..30928c633 --- /dev/null +++ b/plinth/modules/transmission/__init__.py @@ -0,0 +1,46 @@ +# +# 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 . +# + +""" +Plinth module to configure Transmission server +""" + +from gettext import gettext as _ + +from plinth import actions +from plinth import cfg +from plinth import service as service_module + + +depends = ['plinth.modules.apps'] + +service = None + + +def init(): + """Intialize the Transmission module.""" + menu = cfg.main_menu.get('apps:index') + menu.add_urlname(_('BitTorrent (Transmission)'), 'glyphicon-save', + 'transmission:index', 100) + + output = actions.run('transmission', ['get-enabled']) + enabled = (output.strip() == 'yes') + + global service + service = service_module.Service( + 'transmission-rpcplinth', _('Transmission BitTorrent'), + is_external=True, enabled=enabled) diff --git a/plinth/modules/transmission/forms.py b/plinth/modules/transmission/forms.py new file mode 100644 index 000000000..dc51f69d8 --- /dev/null +++ b/plinth/modules/transmission/forms.py @@ -0,0 +1,62 @@ +# +# 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 . +# + +""" +Plinth module for configuring Transmission. +""" + +from django import forms +from django.core.validators import RegexValidator +from gettext import gettext as _ +import re + + +ipv4_wildcard_re = r'(25[0-5]|2[0-4]\d|[0-1]?\d?\d)' \ + r'(\.(25[0-5]|2[0-4]\d|[0-1]?\d?\d|\*)){3}' + +multiple_ips_re = re.compile(r'^({ipv4})(\s*,\s*{ipv4})*$'.format(ipv4=ipv4_wildcard_re)) +ip_validator = RegexValidator(multiple_ips_re) + + +class TransmissionForm(forms.Form): # pylint: disable=W0232 + """Tor configuration form""" + enabled = forms.BooleanField( + label=_('Enable Transmission daemon'), + required=False) + + download_dir = forms.CharField( + label=_('Download directory'), + help_text=_('Directory where downloads are saved. If you change the \ +default directory, ensure that the new directory exists and is writable by \ +"debian-tramission" user')) + + rpc_username = forms.CharField( + label=_('Username'), + help_text=_('Username to login to the web interface')) + + rpc_password = forms.CharField( + label=_('Password'), + help_text=_('Password to login to the web interface. Current \ +password is shown in a hashed format. To set a new password, enter the \ +password in plain text.')) + + rpc_whitelist = forms.CharField( + label=_('IP addresses to allow'), + validators=[ip_validator], + help_text=_('A comma separated list of IP addresses that will be \ +allowed to connect to the web interface. IP addresses may use wild cards, \ +such as 192.168.*.* .')) diff --git a/plinth/modules/transmission/templates/transmission.html b/plinth/modules/transmission/templates/transmission.html new file mode 100644 index 000000000..927cd71c4 --- /dev/null +++ b/plinth/modules/transmission/templates/transmission.html @@ -0,0 +1,55 @@ +{% extends "base.html" %} +{% comment %} +# +# 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 . +# +{% endcomment %} + +{% load bootstrap %} + +{% block content %} + +

Bittorrent (Transmission)

+ +

BitTorrent is a peer-to-peer file sharing protocol. Transmission + daemon handles Bitorrent file sharing. Note that BitTorrent is not + anonymous.

+ +

Access the web interface at + + http://{{ status.hostname }}:{{ status.rpc_port }} + +

Status

+ +

+ {% if status.is_running %} + Transmission daemon is running + {% else %} + Transmission daemon is not running + {% endif %} +

+ +

Configuration

+ +
+ {% csrf_token %} + + {{ form|bootstrap }} + + +
+ +{% endblock %} diff --git a/plinth/modules/transmission/urls.py b/plinth/modules/transmission/urls.py new file mode 100644 index 000000000..a599ee83a --- /dev/null +++ b/plinth/modules/transmission/urls.py @@ -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 . +# + +""" +URLs for the Transmission module. +""" + +from django.conf.urls import patterns, url + + +urlpatterns = patterns( # pylint: disable-msg=C0103 + 'plinth.modules.transmission.views', + url(r'^apps/transmission/$', 'index', name='index'), + ) diff --git a/plinth/modules/transmission/views.py b/plinth/modules/transmission/views.py new file mode 100644 index 000000000..71c5b85af --- /dev/null +++ b/plinth/modules/transmission/views.py @@ -0,0 +1,111 @@ +# +# 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 . +# + +""" +Plinth module for configuring Transmission Server +""" + +from django.contrib import messages +from django.contrib.auth.decorators import login_required +from django.template.response import TemplateResponse +from gettext import gettext as _ +import json +import logging +import socket + +from .forms import TransmissionForm +from plinth import actions +from plinth import package +from plinth.modules import transmission + +logger = logging.getLogger(__name__) + +TRANSMISSION_CONFIG = '/etc/transmission-daemon/settings.json' + + +@login_required +@package.required(['transmission-daemon']) +def index(request): + """Serve configuration page.""" + status = get_status() + + form = None + + if request.method == 'POST': + form = TransmissionForm(request.POST, prefix='transmission') + # pylint: disable=E1101 + if form.is_valid(): + _apply_changes(request, status, form.cleaned_data) + status = get_status() + form = TransmissionForm(initial=status, prefix='transmission') + else: + form = TransmissionForm(initial=status, prefix='transmission') + + return TemplateResponse(request, 'transmission.html', + {'title': _('BitTorrent (Transmission)'), + 'status': status, + 'form': form}) + + +def get_status(): + """Get the current settings from Transmission server.""" + output = actions.run('transmission', ['get-enabled']) + enabled = (output.strip() == 'yes') + + output = actions.superuser_run('transmission', ['is-running']) + is_running = (output.strip() == 'yes') + + configuration = open(TRANSMISSION_CONFIG, 'r').read() + status = json.loads(configuration) + status = {key.translate(str.maketrans({'-': '_'})): value + for key, value in status.items()} + status['enabled'] = enabled + status['is_running'] = is_running + status['hostname'] = socket.gethostname() + + return status + + +def _apply_changes(request, old_status, new_status): + """Apply the changes""" + modified = False + + if old_status['enabled'] != new_status['enabled']: + sub_command = 'enable' if new_status['enabled'] else 'disable' + actions.superuser_run('transmission', [sub_command]) + transmission.service.notify_enabled(None, new_status['enabled']) + modified = True + + if old_status['download_dir'] != new_status['download_dir'] or \ + old_status['rpc_username'] != new_status['rpc_username'] or \ + old_status['rpc_password'] != new_status['rpc_password'] or \ + old_status['rpc_whitelist'] != new_status['rpc_whitelist']: + new_configuration = { + 'download-dir': new_status['download_dir'], + 'rpc-username': new_status['rpc_username'], + 'rpc-password': new_status['rpc_password'], + 'rpc-whitelist': new_status['rpc_whitelist'] + } + + actions.superuser_run('transmission', ['merge-configuration', + json.dumps(new_configuration)]) + modified = True + + if modified: + messages.success(request, _('Configuration updated')) + else: + messages.info(request, _('Setting unchanged'))