diff --git a/README.rst b/README.rst index 4182aba..698d7b0 100644 --- a/README.rst +++ b/README.rst @@ -16,13 +16,17 @@ and on top of that: formats. - E-mail download completion notification via Postfix MTA (bound to localhost). -- File sharing (`Transmission`_) configuration: +- Download server (`Transmission`_) configuration: + + - Authenticated web interface and RPC endpoint on HTTPS port 12322. + - Downloads stored in */srv/storage/download* with incomplete data in + */srv/storage/incoming*. - Anti-virus / malware scanning via `ClamAV`_. - Anti-virus signatures are auto-updated. - Automatically quarantines unsafe downloads. - - Anti-virus logfile: */var/log/rtorrent/clamav.log* + - Anti-virus logfile: */var/log/transmission/clamav.log* - File server (`Samba`_) configuration: @@ -65,7 +69,7 @@ configuration details if behind a Firewall/Router/NAT Credentials *(passwords set at first boot)* ------------------------------------------- -- Webmin, Webshell, SSH, Samba: username **root** +- Webmin, SSH, Samba: username **root** - Transmission: username **admin** - Web based file manager (WebDAV CGI): - username **root** (or Samba users) diff --git a/changelog b/changelog index 60da687..45d9bec 100644 --- a/changelog +++ b/changelog @@ -1,3 +1,13 @@ +turnkey-torrentserver-19.0 (1) turnkey; urgency=low + + * Update the appliance to Debian 13/Trixie and Debian's Transmission 4.1 + package series. + * Keep Transmission credentials out of process arguments and configure its + authenticated RPC, storage, completion scan, and peer port explicitly. + * Replace stale rTorrent service residue with a Transmission lifecycle test. + + -- TurnKey Linux release engineering Tue, 25 Aug 2026 00:00:00 +0000 + turnkey-torrentserver-18.0 (1) turnkey; urgency=low * Add wsdd package for Windows Service for Devices - so Fileserver shows up diff --git a/conf.d/main b/conf.d/main index 309f352..b7aee37 100755 --- a/conf.d/main +++ b/conf.d/main @@ -1,31 +1,59 @@ -#!/bin/sh -ex +#!/bin/bash -e -STORAGE=/srv/storage -PASSWORD=turnkey - -mkdir -p $STORAGE/incoming -mkdir -p $STORAGE/quarantine -mkdir -p $STORAGE/download +set -x -chown -R debian-transmission:users $STORAGE/incoming -chown -R debian-transmission:users $STORAGE/quarantine -chown -R debian-transmission:users $STORAGE/download +STORAGE=/srv/storage +install -d -o debian-transmission -g users -m 2770 \ + "$STORAGE/incoming" "$STORAGE/quarantine" "$STORAGE/download" +install -d -o debian-transmission -g users -m 0750 /var/log/transmission -mkdir /var/log/rtorrent +# Permit the completion hook to submit files to clamd and let clamd read the +# shared storage tree after the next service start. +adduser debian-transmission clamav +adduser clamav users # configure transmission CONF=/etc/transmission-daemon/settings.json -sed -i 's|"download-dir": .*|"download-dir": "\/srv\/storage\/download",|' $CONF +service transmission-daemon stop || true +python3 - "$CONF" <<'PY' +import json +import sys + +path = sys.argv[1] +with open(path, encoding='utf-8') as settings_file: + settings = json.load(settings_file) + +settings.update({ + 'download-dir': '/srv/storage/download', + 'incomplete-dir': '/srv/storage/incoming', + 'incomplete-dir-enabled': True, + 'peer-port': 6882, + 'rpc-authentication-required': True, + 'rpc-bind-address': '127.0.0.1', + 'rpc-username': 'admin', + 'rpc-whitelist': '127.0.0.1,::1', + 'rpc-whitelist-enabled': True, + 'script-torrent-done-enabled': True, + 'script-torrent-done-filename': '/usr/local/bin/clamav-scan', +}) + +with open(path, 'w', encoding='utf-8') as settings_file: + json.dump(settings, settings_file, indent=4, sort_keys=True) + settings_file.write('\n') +PY +chown debian-transmission:debian-transmission "$CONF" +chmod 0660 "$CONF" # configure clamav CONF=/etc/clamav/clamd.conf -sed -i 's|\(ScanMail\) true|\1 false|; s|\(LogSyslog\) false|\1 true|' $CONF +sed -i 's|\(ScanMail\) true|\1 false|; s|\(LogSyslog\) false|\1 true|' "$CONF" +/usr/bin/systemctl enable clamav-freshclam.service clamav-daemon.path # configure samba CONF=/etc/samba/smb.conf -NETBIOS_NAME=$(echo $HOSTNAME | tr [a-z] [A-Z]) -sed -i "s|\(netbios name =\) \(.*\)|\1 $NETBIOS_NAME|" $CONF -sed -i "s|\(server string =\) \(.*\)|\1 $CREDIT_ANCHORTEXT|" $CONF +NETBIOS_NAME=${HOSTNAME^^} +sed -i "s|\(netbios name =\) \(.*\)|\1 $NETBIOS_NAME|" "$CONF" +sed -i "s|\(server string =\) \(.*\)|\1 $CREDIT_ANCHORTEXT|" "$CONF" # Rename the file server for WebDAVCGI sed -i 's/FILESERVER/TORRENTSERVER/' /var/www/webdavcgi/webdav.conf @@ -38,6 +66,3 @@ a2dissite 000-default a2ensite tkl-webcp a2ensite webdavcgi a2ensite transmission - -# initscript -update-rc.d transmission-daemon defaults 99 diff --git a/docs/v19.0-testing.md b/docs/v19.0-testing.md new file mode 100644 index 0000000..41dd97a --- /dev/null +++ b/docs/v19.0-testing.md @@ -0,0 +1,55 @@ +# Torrentserver 19.0 test evidence + +## Dependency decision + +Torrentserver uses Debian Trixie packages for Transmission, Samba, Apache, +ClamAV, Postfix, and the rest of its runtime. The source probe tested Debian's +`transmission-daemon` 4.1.0 beta package series. No separately downloaded +application artifact or third-party repository is required. Debian APT is the +maintained updater and supplies signed repository metadata. + +## Primary-flow crosswalk + +| README claim | Focused check | Expected result | Current evidence | +|---|---|---|---| +| Transmission administrator and HTTPS web UI on 12322 | Use the real firstboot password through a private netrc file for RPC `session-info` and `/transmission/web/` | Authentication succeeds, RPC reports peer port 6882, and the page identifies Transmission | Passed in exact run `20260826t051321z-3895-9520` | +| Torrent download lifecycle and storage | Create a trackerless local torrent, add it paused, verify existing local data, read its RPC state, then remove it | Name, 50-byte verified size, zero bytes left, and `/srv/storage/download` all match | Passed in the exact appliance through authenticated Transmission RPC | +| Samba root account and public storage | Put, get, compare, and delete a fixture through `//127.0.0.1/storage` using the firstboot password | The returned bytes match the source | Passed in the exact appliance | +| Automatic ClamAV scan, quarantine path, log, and completion mail | Inspect live RPC configuration and service state | Completion hook is enabled at `/usr/local/bin/clamav-scan`; ClamAV and Postfix are active; storage paths and permissions exist | Passed configuration and supervision checks; freshclam downloaded current databases and the database watcher started clamd | +| Control panel and supervised services | Read the HTTP control panel and check Transmission, Apache, Samba, ClamAV, and Postfix | Page identifies TurnKey Torrent Server and services are active | Passed in the exact appliance | +| Debian update path | Refresh metadata, inspect `apt-cache policy`, and run a simulated only-upgrade install | A signed Trixie candidate is eligible and no package or data changes occur | Passed; APT authenticated current metadata and selected `4.1.0~beta2+dfsg-3+deb13u1` | +| Inherited Core and Fileserver behavior | Cite the accepted Core 19 and Fileserver baseline | Unchanged ISO, SSH, Webmin, WebDAV CGI, and common Fileserver behavior remains covered | Reused by contract; this probe covers Torrentserver customizations | + +## Acceptance command and status + +```bash +TKLDEV_CONTAINER=tkldev19-wave2 \ +TKL_HARNESS_STATE_DIR=/home/agent/.local/state/turnkey-v19-harness-wave2 \ +TKL_HARNESS_LOCK_FILE=/home/agent/.local/state/turnkey-v19-harness-wave2/build.lock \ +TKL_HARNESS_LOCK_TIMEOUT=3600 \ +TKL_HARNESS_DOCKER_LIMIT_BYTES=42949672960 \ +/sandboxed-git/turnkey/tools/test-v19-appliance torrentserver \ + --source /home/agent/.local/worktrees/turnkey-apps/torrentserver/wish-torrentserver-v19-trixie +``` + +Exact run `20260826t051321z-3895-9520` passed from source commit +`b8a18fe3aab39e6871d304f440bf1b2b27b9939a`, with source archive SHA-256 +`939f0c8255c7a5d4198d7034d68937972cb81c7ab3fa3c1e8b61960c666beb60`. +Build, configured-root import, boot health, focused runtime checks, cleanup, and +source identity all passed. The result records Debian Trixie +`transmission-daemon 4.1.0~beta2+dfsg-3+deb13u1`, authenticated APT metadata, +the same eligible candidate, and a successful non-mutating upgrade simulation. + +## Deferred issues + +- **MEDIUM:** The focused probe validates the configured ClamAV completion + hook and supervised daemon but does not exercise a malware signature. The + local torrent lifecycle remains independent of signature freshness. +- **ENVIRONMENT:** Docker rejected the configured-root overlay mount with + `EINVAL`, so the harness used its recorded `root.patched` fallback. Docker + acceptance does not exercise installer, kernel, or hardware behavior. + +Product fix loops used: 2 of 3. The fixes start ClamAV after its initial +signature download and explicitly supervise freshclam. Shared lock waits, +environment boot timing, and acceptance assertion corrections did not consume +product loops. diff --git a/overlay/etc/init.d/rtorrent b/overlay/etc/init.d/rtorrent deleted file mode 100755 index 92b4c0b..0000000 --- a/overlay/etc/init.d/rtorrent +++ /dev/null @@ -1,72 +0,0 @@ -#!/bin/bash -### BEGIN INIT INFO -# Provides: rtorrent -# Required-Start: $local_fs $remote_fs $network $syslog -# Required-Stop: $local_fs $remote_fs $network $syslog -# Default-Start: 2 3 4 5 -# Default-Stop: 0 1 6 -# Short-Description: Start/stop rtorrent daemon -### END INIT INFO - -USER='rtorrent' -PIDFILE='/var/run/rtorrent.pid' -SOCKET='/var/run/rtorrent/rpc.socket' -WD='/var/lib/rtorrent' -TMUX='/usr/bin/tmux' - -delete_socket() { - if [[ -e $SOCKET ]]; then - rm -f $SOCKET - fi -} - -create_socket_dir() { - SOCKET_DIR=$(dirname "$SOCKET") - if [ ! -d "$SOCKET_DIR" ]; then - mkdir -p "$SOCKET_DIR" - chmod 750 "$SOCKET_DIR" - chown $USER:www-data "$SOCKET_DIR" - fi -} - -case "$1" in - start) - echo "Starting rtorrent." - create_socket_dir - delete_socket - su - rtorrent -s /bin/bash -c "$TMUX new-session -c $WD -s rtorrent -n rtorrent -d rtorrent" - if [[ $? -ne 0 ]]; then - echo "Error: rtorrent failed to start." - exit 1 - fi - - sleep 1 - chmod ug=rw,o= $SOCKET - chown rtorrent:www-data $SOCKET - - echo "rtorrent started successfully." - ;; - - stop) - echo "Stopping rtorrent." - su - rtorrent -s /bin/bash -c "$TMUX send-keys -t rtorrent C-q && tmux kill-session -t rtorrent" - if [[ $? -ne 0 ]]; then - echo "Error: failed to stop rtorrent process." - exit 1 - fi - delete_socket - echo "rtorrent stopped successfully." - ;; - - restart) - "$0" stop - sleep 1 - "$0" start || exit 1 - ;; - - *) - echo "Usage: $0 [start|stop|restart]" - exit 1 - ;; - -esac diff --git a/overlay/etc/nginx/sites-available/transmission b/overlay/etc/nginx/sites-available/transmission deleted file mode 100644 index 9e10cdf..0000000 --- a/overlay/etc/nginx/sites-available/transmission +++ /dev/null @@ -1,17 +0,0 @@ -server { - listen 12322 default_server ssl; - listen [::]:12322 default_server ssl; - - server_name localhost; - - include /etc/nginx/include/ssl; - - location / { - proxy_pass http://127.0.0.1:9091; - proxy_pass_header X-Transmission-Session-Id; - - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $remote_addr; - proxy_set_header Host $host; - } -} diff --git a/overlay/etc/systemd/system/clamav-daemon.path b/overlay/etc/systemd/system/clamav-daemon.path new file mode 100644 index 0000000..a3f6e2d --- /dev/null +++ b/overlay/etc/systemd/system/clamav-daemon.path @@ -0,0 +1,11 @@ +[Unit] +Description=Start ClamAV daemon after the initial signature download +Documentation=man:systemd.path(5) +Before=clamav-freshclam.service + +[Path] +PathChanged=/var/lib/clamav +Unit=clamav-daemon.service + +[Install] +WantedBy=multi-user.target diff --git a/overlay/usr/lib/inithooks/bin/torrentserver.py b/overlay/usr/lib/inithooks/bin/torrentserver.py index 511aed9..aeb55be 100755 --- a/overlay/usr/lib/inithooks/bin/torrentserver.py +++ b/overlay/usr/lib/inithooks/bin/torrentserver.py @@ -1,59 +1,70 @@ #!/usr/bin/python3 # Copyright (c) 2010 Alon Swartz - all rights reserved # Updated 2016 by Anton Pyrogovskyi -"""Configure admin password for Transmission +"""Configure the Transmission administrator password.""" -Options: - -p --pass= if not provided, will ask interactively -""" - -import sys -import getopt -import subprocess -from time import sleep +import argparse import json +import os +from pathlib import Path +import subprocess +import sys +import tempfile from libinithooks.dialog_wrapper import Dialog -def fatal(s): - print("Error:", s, file=sys.stderr) - sys.exit(1) - -def usage(s=None): - if s: - print("Error:", s, file=sys.stderr) - print("Syntax: %s [options]" % sys.argv[0], file=sys.stderr) - print(__doc__, file=sys.stderr) - sys.exit(1) - def main(): - try: - opts, args = getopt.gnu_getopt(sys.argv[1:], "hp:", ['help', 'pass=']) - except getopt.GetoptError as e: - usage(e) + parser = argparse.ArgumentParser(description=__doc__) + source = parser.add_mutually_exclusive_group() + source.add_argument('-p', '--pass', dest='password', help=argparse.SUPPRESS) + source.add_argument('--pass-stdin', action='store_true', + help='read the password from standard input') + args = parser.parse_args() - password = "" - for opt, val in opts: - if opt in ('-h', '--help'): - usage() - elif opt in ('-p', '--pass'): - password = val - - if not password: + if args.pass_stdin: + password = sys.stdin.read() + elif args.password is not None: + password = args.password + else: d = Dialog('TurnKey Linux - First boot configuration') password = d.get_password( "Torrent Server Password", "Enter new admin password for Transmission.") - subprocess.run(['service', 'transmission-daemon', 'stop']) - with open('/etc/transmission-daemon/settings.json', 'r') as fob: - settings = json.load(fob) - settings['rpc-username'] = 'admin' - settings['rpc-password'] = password - with open('/etc/transmission-daemon/settings.json', 'w') as fob: - json.dump(settings, fob) - subprocess.run(['service', 'transmission-daemon', 'start']) + if not password: + parser.error('password must not be empty') + + settings_path = Path('/etc/transmission-daemon/settings.json') + subprocess.run(['service', 'transmission-daemon', 'stop'], check=True) + try: + with settings_path.open(encoding='utf-8') as settings_file: + settings = json.load(settings_file) + settings['rpc-authentication-required'] = True + settings['rpc-username'] = 'admin' + settings['rpc-password'] = password + + original = settings_path.stat() + descriptor, temporary_name = tempfile.mkstemp( + dir=settings_path.parent, + prefix='.settings.json.', + ) + try: + os.fchmod(descriptor, original.st_mode & 0o777) + os.fchown(descriptor, original.st_uid, original.st_gid) + with os.fdopen(descriptor, 'w', encoding='utf-8') as settings_file: + json.dump(settings, settings_file, indent=4, sort_keys=True) + settings_file.write('\n') + settings_file.flush() + os.fsync(settings_file.fileno()) + os.replace(temporary_name, settings_path) + except BaseException: + try: + os.unlink(temporary_name) + except FileNotFoundError: + pass + raise + finally: + subprocess.run(['service', 'transmission-daemon', 'start'], check=True) if __name__ == "__main__": main() - diff --git a/overlay/usr/lib/inithooks/firstboot.d/40torrentserver b/overlay/usr/lib/inithooks/firstboot.d/40torrentserver index 39f842a..d64b5f5 100755 --- a/overlay/usr/lib/inithooks/firstboot.d/40torrentserver +++ b/overlay/usr/lib/inithooks/firstboot.d/40torrentserver @@ -3,6 +3,8 @@ . /etc/default/inithooks -[ -e $INITHOOKS_CONF ] && . $INITHOOKS_CONF -$INITHOOKS_PATH/bin/torrentserver.py --pass="$APP_PASS" - +set +x +[ -e "$INITHOOKS_CONF" ] && . "$INITHOOKS_CONF" +printf '%s' "$APP_PASS" | "$INITHOOKS_PATH/bin/torrentserver.py" --pass-stdin +unset APP_PASS +set -x diff --git a/overlay/usr/local/bin/clamav-scan b/overlay/usr/local/bin/clamav-scan index e995d79..fa77934 100755 --- a/overlay/usr/local/bin/clamav-scan +++ b/overlay/usr/local/bin/clamav-scan @@ -1,3 +1,40 @@ #!/bin/bash -clamdscan -v --stdout --move /srv/storage/quarantine $1 >> /var/log/rtorrent/clamav.log +set -u + +QUARANTINE=/srv/storage/quarantine +LOG=/var/log/transmission/clamav.log +TORRENT_DIR=${TR_TORRENT_DIR:?Transmission did not provide TR_TORRENT_DIR} +TORRENT_NAME=${TR_TORRENT_NAME:?Transmission did not provide TR_TORRENT_NAME} +TARGET=$(realpath -e -- "$TORRENT_DIR/$TORRENT_NAME") || exit 2 + +case "$TARGET" in + /srv/storage/*) ;; + *) + echo "Refusing to scan outside /srv/storage: $TARGET" >&2 + exit 2 + ;; +esac + +exec >> "$LOG" 2>&1 +echo "$(date --iso-8601=seconds) scanning $TARGET" + +clamdscan --fdpass --multiscan --verbose --move="$QUARANTINE" "$TARGET" +scan_status=$? + +case "$scan_status" in + 0) result=clean ;; + 1) result=quarantined ;; + *) result=scan-error ;; +esac + +if command -v sendmail >/dev/null; then + { + echo "To: root" + echo "Subject: Transmission download $result: $TORRENT_NAME" + echo + echo "Transmission completed $TORRENT_NAME. ClamAV result: $result." + } | sendmail -t +fi + +exit "$scan_status" diff --git a/plan/main b/plan/main index 33ee74f..12a7c89 100644 --- a/plan/main +++ b/plan/main @@ -4,10 +4,11 @@ netcat-openbsd clamav-daemon +clamav-freshclam clamdscan transmission-daemon -tmux +transmission-cli mediainfo ffmpeg diff --git a/tests/v19.sh b/tests/v19.sh new file mode 100755 index 0000000..186d077 --- /dev/null +++ b/tests/v19.sh @@ -0,0 +1,186 @@ +#!/bin/bash +set -euo pipefail + +: "${TKL_TEST_RESULT:?TKL_TEST_RESULT must name the result file}" +: "${TKL_TEST_APP_PASS:?TKL_TEST_APP_PASS must contain the firstboot password}" + +fail() { + echo "FAIL: $*" >&2 + exit 1 +} + +require_contains() { + local text=$1 expected=$2 context=$3 + [[ $text == *"$expected"* ]] || fail "$context did not contain: $expected" +} + +if ! systemctl is-active --quiet clamav-freshclam; then + systemctl restart clamav-freshclam \ + || fail "clamav-freshclam did not restart after runtime CA setup" +fi + +for service in transmission-daemon apache2 smbd clamav-freshclam postfix; do + systemctl is-active --quiet "$service" || fail "$service is not active" +done +systemctl is-enabled --quiet clamav-daemon.path \ + || fail "ClamAV database watcher is not enabled" +systemctl is-active --quiet clamav-daemon.path \ + || fail "ClamAV database watcher is not active" +for _ in $(seq 1 300); do + systemctl is-active --quiet clamav-daemon && break + sleep 1 +done +systemctl is-active --quiet clamav-daemon \ + || fail "clamav-daemon did not start after the initial signature download" +apache2ctl configtest 2>&1 | grep -q 'Syntax OK' \ + || fail "Apache configuration is invalid" + +installed_version=$(dpkg-query -W -f='${Version}' transmission-daemon) +[[ $installed_version == 4.* ]] || fail "Transmission is not on the Trixie 4.x series" +dpkg-query -W transmission-cli clamav-daemon clamav-freshclam samba >/dev/null + +python3 - <<'PY' +import json +import os + +with open('/etc/transmission-daemon/settings.json', encoding='utf-8') as source: + settings = json.load(source) +expected = { + 'download-dir': '/srv/storage/download', + 'incomplete-dir': '/srv/storage/incoming', + 'incomplete-dir-enabled': True, + 'peer-port': 6882, + 'rpc-authentication-required': True, + 'rpc-bind-address': '127.0.0.1', + 'rpc-username': 'admin', + 'rpc-whitelist-enabled': True, + 'script-torrent-done-enabled': True, + 'script-torrent-done-filename': '/usr/local/bin/clamav-scan', +} +for key, value in expected.items(): + if settings.get(key) != value: + raise SystemExit(f'unexpected Transmission setting {key}') +if settings.get('rpc-password') == os.environ['TKL_TEST_APP_PASS']: + raise SystemExit('Transmission retained the firstboot password in plaintext') +PY + +temporary=$(mktemp -d) +torrent_netrc=$temporary/transmission.netrc +samba_auth=$temporary/samba.auth +fixture_source=/srv/storage/incoming/turnkey-v19-fixture.txt +fixture_download=/srv/storage/download/turnkey-v19-fixture.txt +fixture_torrent=$temporary/turnkey-v19-fixture.torrent +fixture_copy=$temporary/samba-copy.txt +samba_name=turnkey-v19-samba-fixture.txt +torrent_hash= +start_policy_changed=0 + +cleanup() { + local status=$? + trap - EXIT HUP INT TERM + if [ -n "$torrent_hash" ]; then + transmission-remote 127.0.0.1:9091 --netrc "$torrent_netrc" \ + --torrent "$torrent_hash" --remove >/dev/null 2>&1 || true + fi + if [ "$start_policy_changed" -eq 1 ]; then + transmission-remote 127.0.0.1:9091 --netrc "$torrent_netrc" \ + --no-start-paused >/dev/null 2>&1 || true + fi + smbclient //127.0.0.1/storage --authentication-file="$samba_auth" \ + --command="del $samba_name" >/dev/null 2>&1 || true + rm -f "$fixture_source" "$fixture_download" + rm -rf "$temporary" + exit "$status" +} +trap cleanup EXIT HUP INT TERM + +chmod 0700 "$temporary" +printf 'machine 127.0.0.1 login admin password %s\n' "$TKL_TEST_APP_PASS" \ + > "$torrent_netrc" +printf 'username = root\npassword = %s\n' "$TKL_TEST_APP_PASS" > "$samba_auth" +chmod 0600 "$torrent_netrc" "$samba_auth" + +session_json=$(transmission-remote 127.0.0.1:9091 --netrc "$torrent_netrc" \ + --json --session-info) +require_contains "$session_json" '"result":"success"' "Transmission RPC session" +require_contains "$session_json" '"peer-port":6882' "Transmission RPC session" +require_contains "$session_json" '"start-added-torrents":true' "Transmission RPC session" + +web_ui=$(curl --insecure --fail --silent --show-error --location \ + --netrc-file "$torrent_netrc" --max-time 30 \ + https://127.0.0.1:12322/transmission/web/) +require_contains "$web_ui" "Transmission" "Transmission HTTPS web UI" + +control_panel=$(curl --fail --silent --show-error --location --max-time 30 \ + http://127.0.0.1/) +require_contains "$control_panel" "TurnKey Torrent Server" "torrent control panel" + +printf 'TurnKey Torrentserver v19 local lifecycle fixture\n' > "$fixture_source" +cp "$fixture_source" "$fixture_download" +chown debian-transmission:users "$fixture_source" "$fixture_download" +transmission-create --anonymize --outfile "$fixture_torrent" "$fixture_source" \ + >/dev/null + +start_policy_changed=1 +add_json=$(transmission-remote 127.0.0.1:9091 --netrc "$torrent_netrc" \ + --json --start-paused --download-dir /srv/storage/download \ + --add "$fixture_torrent") +transmission-remote 127.0.0.1:9091 --netrc "$torrent_netrc" \ + --no-start-paused >/dev/null +start_policy_changed=0 +torrent_hash=$(python3 -c ' +import json, sys +for line in sys.stdin: + data = json.loads(line) + added = data.get("arguments", {}).get("torrent-added") + if added: + print(added["hashString"]) +' <<< "$add_json") +[[ $torrent_hash =~ ^[0-9a-f]{40}$ ]] || fail "Transmission did not add the fixture" + +transmission-remote 127.0.0.1:9091 --netrc "$torrent_netrc" \ + --torrent "$torrent_hash" --verify >/dev/null +for _ in $(seq 1 20); do + info_json=$(transmission-remote 127.0.0.1:9091 --netrc "$torrent_netrc" \ + --json --torrent "$torrent_hash" --info) + lifecycle=$(python3 -c ' +import json, sys +torrent = json.load(sys.stdin)["arguments"]["torrents"][0] +print("|".join(str(torrent[key]) for key in + ("name", "haveValid", "totalSize", "leftUntilDone", "downloadDir"))) +' <<< "$info_json") + if [[ $lifecycle = "turnkey-v19-fixture.txt|50|50|0|/srv/storage/download" ]]; then + break + fi + sleep 1 +done +[[ $lifecycle = "turnkey-v19-fixture.txt|50|50|0|/srv/storage/download" ]] \ + || fail "Transmission did not verify and read the local fixture" + +transmission-remote 127.0.0.1:9091 --netrc "$torrent_netrc" \ + --torrent "$torrent_hash" --remove >/dev/null +torrent_hash= + +smbclient //127.0.0.1/storage --authentication-file="$samba_auth" \ + --command="put $fixture_source $samba_name; get $samba_name $fixture_copy; del $samba_name" \ + >/dev/null +cmp "$fixture_source" "$fixture_copy" || fail "Samba file round trip changed content" + +apt-get update >/dev/null +policy=$(apt-cache policy transmission-daemon) +candidate=$(awk '/Candidate:/ {print $2}' <<< "$policy") +[[ -n $candidate && $candidate != '(none)' ]] || fail "APT has no Transmission candidate" +require_contains "$policy" "trixie" "Transmission APT policy" +apt-get --simulate --only-upgrade install \ + transmission-daemon transmission-common transmission-cli >/dev/null + +echo "PASS: Transmission RPC lifecycle, HTTPS web UI, Samba, ClamAV hook and services" +cat > "$TKL_TEST_RESULT" <