Full Code¶
Transmission app is already included in FreedomBox. Here is the full source for the module for reference.
plinth/modules/transmission/__init__.py¶
# SPDX-License-Identifier: AGPL-3.0-or-later
"""
FreedomBox app to configure Transmission server.
"""
from django.urls import reverse_lazy
from django.utils.translation import gettext_lazy as _
from plinth import app as app_module
from plinth import cfg, frontpage, menu
from plinth.config import DropinConfigs
from plinth.daemon import Daemon
from plinth.modules.apache.components import Webserver
from plinth.modules.backups.components import BackupRestore
from plinth.modules.firewall.components import (Firewall,
FirewallLocalProtection)
from plinth.modules.users import add_user_to_share_group
from plinth.modules.users.components import UsersAndGroups
from plinth.package import Packages
from plinth.utils import format_lazy
from . import manifest, privileged
_description = [
_('Transmission is a BitTorrent client with a web interface.'),
_('BitTorrent is a peer-to-peer file sharing protocol. '
'Note that BitTorrent is not anonymous.'),
_('Please do not change the default port of the Transmission daemon.'),
format_lazy(
_('Compared to <a href="{deluge_url}">'
'Deluge</a>, Transmission is simpler and lightweight but is less '
'customizable.'), deluge_url=reverse_lazy('deluge:index')),
format_lazy(
_('It can be accessed by <a href="{users_url}">any user</a> on '
'{box_name} belonging to the bit-torrent group.'),
box_name=_(cfg.box_name), users_url=reverse_lazy('users:index')),
format_lazy(
_('In addition to the web interface, mobile and desktop apps can also '
'be used to remotely control Transmission on {box_name}. To '
'configure remote control apps, use the URL '
'<a href="/transmission-remote/rpc">/transmission-remote/rpc</a>.'),
box_name=_(cfg.box_name)),
format_lazy(
_('<a href="{samba_url}">Samba</a> shares can be set as the '
'default download directory from the dropdown menu below.'),
samba_url=reverse_lazy('samba:index')),
format_lazy(
_('After a download has completed, you can also access your files '
'using the <a href="{sharing_url}">Sharing</a> app.'),
sharing_url=reverse_lazy('sharing:index'))
]
SYSTEM_USER = 'debian-transmission'
class TransmissionApp(app_module.App):
"""FreedomBox app for Transmission."""
app_id = 'transmission'
_version = 7
DAEMON = 'transmission-daemon'
def __init__(self) -> None:
"""Create components for the app."""
super().__init__()
groups = {
'bit-torrent': _('Download files using BitTorrent applications')
}
info = app_module.Info(
app_id=self.app_id, version=self._version, name=_('Transmission'),
icon_filename='transmission',
short_description=_('BitTorrent Web Client'),
description=_description, manual_page='Transmission',
clients=manifest.clients,
donation_url='https://transmissionbt.com/donate/')
self.add(info)
menu_item = menu.Menu('menu-transmission', info.name,
info.short_description, info.icon_filename,
'transmission:index', parent_url_name='apps')
self.add(menu_item)
shortcut = frontpage.Shortcut(
'shortcut-transmission', info.name,
short_description=info.short_description, icon=info.icon_filename,
url='/transmission', clients=info.clients, login_required=True,
allowed_groups=list(groups))
self.add(shortcut)
packages = Packages('packages-transmission', ['transmission-daemon'])
self.add(packages)
dropin_configs = DropinConfigs('dropin-configs-transmission', [
'/etc/apache2/conf-available/transmission-plinth.conf',
])
self.add(dropin_configs)
firewall = Firewall('firewall-transmission', info.name,
ports=['http', 'https',
'transmission-client'], is_external=True)
self.add(firewall)
firewall_local_protection = FirewallLocalProtection(
'firewall-local-protection-transmission', ['9091'])
self.add(firewall_local_protection)
webserver = Webserver('webserver-transmission', 'transmission-plinth',
urls=['https://{host}/transmission'],
last_updated_version=6)
self.add(webserver)
daemon = Daemon(
'daemon-transmission', self.DAEMON, listen_ports=[
(9091, 'tcp4'),
(51413, 'tcp4'),
(51413, 'tcp6'),
(51413, 'udp4'),
])
self.add(daemon)
users_and_groups = UsersAndGroups('users-and-groups-transmission',
reserved_usernames=[SYSTEM_USER],
groups=groups)
self.add(users_and_groups)
backup_restore = BackupRestore('backup-restore-transmission',
**manifest.backup)
self.add(backup_restore)
def setup(self, old_version):
"""Install and configure the app."""
super().setup(old_version)
if old_version and old_version <= 3 and self.is_enabled():
self.get_component('firewall-transmission').enable()
new_configuration = {
'rpc-whitelist-enabled': False,
'rpc-authentication-required': False
}
privileged.merge_configuration(new_configuration)
add_user_to_share_group(SYSTEM_USER, TransmissionApp.DAEMON)
if not old_version:
self.enable()
plinth/modules/transmission/forms.py¶
# SPDX-License-Identifier: AGPL-3.0-or-later
"""
FreedomBox app for configuring Transmission.
"""
from django.utils.translation import gettext_lazy as _
from plinth.modules.storage.forms import (DirectorySelectForm,
DirectoryValidator)
from . import SYSTEM_USER
class TransmissionForm(DirectorySelectForm):
"""Transmission configuration form"""
def __init__(self, *args, **kw):
validator = DirectoryValidator(username=SYSTEM_USER,
check_creatable=True)
super().__init__(title=_('Download directory'),
default='/var/lib/transmission-daemon/downloads',
validator=validator, *args, **kw)
plinth/modules/transmission/manifest.py¶
# SPDX-License-Identifier: AGPL-3.0-or-later
from django.utils.translation import gettext_lazy as _
from plinth.clients import store_url
clients = [{
'name': _('Transmission'),
'platforms': [{
'type': 'web',
'url': '/transmission'
}]
}, {
'name':
_('Tremotesf'),
'platforms': [{
'type': 'store',
'os': 'android',
'store_name': 'f-droid',
'url': store_url('f-droid', 'org.equeim.tremotesf')
}, {
'type': 'store',
'os': 'android',
'store_name': 'google-play',
'url': store_url('google-play', 'org.equeim.tremotesf')
}]
}]
backup = {
'data': {
'directories': ['/var/lib/transmission-daemon/.config']
},
'secrets': {
'files': ['/etc/transmission-daemon/settings.json']
},
'services': ['transmission-daemon']
}
plinth/modules/transmission/privileged.py¶
# SPDX-License-Identifier: AGPL-3.0-or-later
"""
Configuration helper for Transmission daemon.
"""
import json
import pathlib
from plinth import action_utils
from plinth.actions import privileged
_transmission_config = pathlib.Path('/etc/transmission-daemon/settings.json')
@privileged
def get_configuration() -> dict[str, str]:
"""Return the current configuration in JSON format."""
return json.loads(_transmission_config.read_text(encoding='utf-8'))
@privileged
def merge_configuration(configuration: dict[str, str | bool]):
"""Merge given JSON configuration with existing configuration."""
current_configuration_bytes = _transmission_config.read_bytes()
current_configuration = json.loads(current_configuration_bytes)
new_configuration = current_configuration
new_configuration.update(configuration)
new_configuration_bytes = json.dumps(new_configuration, indent=4,
sort_keys=True)
_transmission_config.write_text(new_configuration_bytes, encoding='utf-8')
action_utils.service_reload('transmission-daemon')
plinth/modules/transmission/urls.py¶
# SPDX-License-Identifier: AGPL-3.0-or-later
"""
URLs for the Transmission module.
"""
from django.urls import re_path
from .views import TransmissionAppView
urlpatterns = [
re_path(r'^apps/transmission/$', TransmissionAppView.as_view(),
name='index'),
]
plinth/modules/transmission/views.py¶
# SPDX-License-Identifier: AGPL-3.0-or-later
"""
FreedomBox app for configuring Transmission Server.
"""
import logging
import socket
from django.contrib import messages
from django.utils.translation import gettext as _
from plinth import views
from . import privileged
from .forms import TransmissionForm
logger = logging.getLogger(__name__)
class TransmissionAppView(views.AppView):
"""Serve configuration page."""
form_class = TransmissionForm
app_id = 'transmission'
def get_initial(self):
"""Get the current settings from Transmission server."""
status = super().get_initial()
configuration = privileged.get_configuration()
status['storage_path'] = configuration['download-dir']
status['hostname'] = socket.gethostname()
return status
def form_valid(self, form):
"""Apply the changes submitted in the form."""
old_status = form.initial
new_status = form.cleaned_data
if old_status['storage_path'] != new_status['storage_path']:
new_configuration = {
'download-dir': new_status['storage_path'],
}
privileged.merge_configuration(new_configuration)
messages.success(self.request, _('Configuration updated'))
return super().form_valid(form)
plinth/modules/transmission/tests/__init__.py¶