616 lines
25 KiB
Bash
Executable File
616 lines
25 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
# Install pyTorrent only, for hosts where rTorrent is already configured.
|
|
|
|
APP_USER="${PYTORRENT_USER:-pytorrent}"
|
|
APP_DIR="${PYTORRENT_APP_DIR:-/opt/pytorrent}"
|
|
SERVICE_NAME="${PYTORRENT_SERVICE_NAME:-pytorrent}"
|
|
PYTHON_BIN="${PYTHON_BIN:-python3}"
|
|
APP_HOST="${PYTORRENT_HOST:-0.0.0.0}"
|
|
APP_PORT="${PYTORRENT_PORT:-8090}"
|
|
PROFILE_NAME="${PYTORRENT_PROFILE_NAME:-Local rTorrent}"
|
|
SCGI_URL="${PYTORRENT_RTORRENT_SCGI_URL:-scgi://127.0.0.1:5000}"
|
|
LOG_ENABLE="${PYTORRENT_LOG_ENABLE:-true}"
|
|
LOG_DIR="${PYTORRENT_LOG_DIR:-data/logs}"
|
|
LOG_RETENTION_HOURS="${PYTORRENT_LOG_RETENTION_HOURS:-24}"
|
|
LIBS_MODE="${PYTORRENT_LIBS_MODE:-offline}"
|
|
AUTH_MODE="${PYTORRENT_AUTH_MODE:-ask}"
|
|
AUTH_PROVIDER="${PYTORRENT_AUTH_PROVIDER:-local}"
|
|
AUTH_USER="${PYTORRENT_AUTH_USER:-pytorrent}"
|
|
AUTH_PASSWORD="${PYTORRENT_AUTH_PASSWORD:-pytorrent}"
|
|
ADMIN_PASSWORD="${PYTORRENT_ADMIN_PASSWORD:-}"
|
|
REVERSE_PROXY="${PYTORRENT_REVERSE_PROXY:-ask}"
|
|
PROXY_DOMAINS="${PYTORRENT_PROXY_DOMAINS:-}"
|
|
CORS_ORIGINS="${PYTORRENT_SOCKETIO_CORS_ALLOWED_ORIGINS:-}"
|
|
LOCAL_ORIGINS="${PYTORRENT_LOCAL_ORIGINS:-}"
|
|
RTORRENT_SOCKET="${RTORRENT_SOCKET:-}"
|
|
INSTALL_SCGI_PROXY="${PYTORRENT_INSTALL_SCGI_PROXY:-ask}"
|
|
RT_PROXY_USER="${RTORRENT_SCGI_PROXY_USER:-rtproxy}"
|
|
RT_PROXY_LISTEN="${RTORRENT_SCGI_PROXY_LISTEN:-127.0.0.1:5050}"
|
|
RT_PROXY_TOKEN="${RTORRENT_SCGI_PROXY_TOKEN:-}"
|
|
RT_PROXY_ALLOW_NET="${RTORRENT_SCGI_PROXY_ALLOW_NET:-127.0.0.1}"
|
|
RT_PROXY_TARGET_NETWORK="${RTORRENT_SCGI_PROXY_TARGET_NETWORK:-tcp}"
|
|
RT_PROXY_TARGET_ADDRESS="${RTORRENT_SCGI_PROXY_TARGET_ADDRESS:-127.0.0.1:5000}"
|
|
RT_PROXY_BINARY_URL="${RTORRENT_SCGI_PROXY_BINARY_URL:-https://git.linuxiarz.pl/gru/rtorrent-scgi-proxy/raw/branch/master/dist/rtorrent-scgi-proxy-linux-amd64}"
|
|
RT_PROXY_TARGET_URI="${RTORRENT_SCGI_PROXY_TARGET_URI:-/RPC2}"
|
|
ASSUME_YES=0
|
|
INTERACTIVE=1
|
|
SKIP_PROFILE=0
|
|
|
|
log() { printf '[pyTorrent only] %s\n' "$*"; }
|
|
fail() { printf '[pyTorrent only] ERROR: %s\n' "$*" >&2; exit 1; }
|
|
|
|
usage() {
|
|
cat <<'USAGE'
|
|
Usage: sudo bash scripts/install_pytorrent_only.sh [options]
|
|
|
|
Options:
|
|
--yes Accept defaults and skip prompts.
|
|
--non-interactive Do not prompt; use flags/env/defaults.
|
|
--app-dir PATH Installation directory. Default: /opt/pytorrent.
|
|
--user NAME System user. Default: pytorrent.
|
|
--service-name NAME systemd service name. Default: pytorrent.
|
|
--host HOST Bind host. Default: 0.0.0.0.
|
|
--port PORT Application port. Default: 8090.
|
|
--profile-name NAME pyTorrent profile name. Default: Local rTorrent.
|
|
--scgi-url URL rTorrent SCGI URL. Default: scgi://127.0.0.1:5000.
|
|
--rtorrent-socket PATH rTorrent Unix socket; can enable SCGI proxy setup.
|
|
--auth enable|disable Enable pyTorrent authentication.
|
|
--auth-provider local|proxy|tinyauth
|
|
--auth-user USER Local auth user to create/update. Default: pytorrent.
|
|
--auth-password PASSWORD Local auth user password. Default: pytorrent.
|
|
--admin-password PASSWORD Optional admin password reset.
|
|
--logs enable|disable File logging. Default: enable.
|
|
--log-dir PATH Log directory. Default: data/logs.
|
|
--libs offline|online Frontend library mode. Default: offline.
|
|
--reverse-proxy yes|no Configure reverse-proxy-safe env values.
|
|
--proxy-domains CSV Domains for reverse proxy, e.g. torrent.example.com,https://p.example.com.
|
|
--cors-origins CSV Extra allowed origins.
|
|
--local-origins CSV Extra local origins added to CORS.
|
|
--install-scgi-proxy yes|no Install rtorrent-scgi-proxy.
|
|
--proxy-listen HOST:PORT SCGI proxy listen address. Default: 127.0.0.1:5050.
|
|
--proxy-token TOKEN SCGI proxy path token.
|
|
--proxy-allow-net VALUE SCGI proxy ALLOW_NET. Default: 127.0.0.1.
|
|
--proxy-target-network tcp|unix
|
|
--proxy-target-address VALUE
|
|
--skip-profile Do not create/update pyTorrent rTorrent profile.
|
|
-h, --help Show this help.
|
|
|
|
Environment variables with the same PYTORRENT_* names are also supported.
|
|
USAGE
|
|
}
|
|
|
|
require_root() {
|
|
# Note: The installer writes systemd units, creates users and installs files under /opt.
|
|
[[ "${EUID}" -eq 0 ]] || fail "Run as root, for example: sudo bash scripts/install_pytorrent_only.sh"
|
|
}
|
|
|
|
bool_value() {
|
|
case "$(printf '%s' "$1" | tr '[:upper:]' '[:lower:]')" in
|
|
1|true|yes|y|on|enable|enabled) echo "true" ;;
|
|
0|false|no|n|off|disable|disabled) echo "false" ;;
|
|
*) echo "$1" ;;
|
|
esac
|
|
}
|
|
|
|
prompt() {
|
|
local var_name="$1" question="$2" default_value="$3" current_value input
|
|
current_value="${!var_name:-$default_value}"
|
|
if [[ "${INTERACTIVE}" != "1" || "${ASSUME_YES}" == "1" ]]; then
|
|
printf -v "${var_name}" '%s' "${current_value}"
|
|
return
|
|
fi
|
|
read -r -p "${question} [${current_value}]: " input
|
|
printf -v "${var_name}" '%s' "${input:-$current_value}"
|
|
}
|
|
|
|
prompt_secret() {
|
|
local var_name="$1" question="$2" default_value="$3" current_value input
|
|
current_value="${!var_name:-$default_value}"
|
|
if [[ "${INTERACTIVE}" != "1" || "${ASSUME_YES}" == "1" ]]; then
|
|
printf -v "${var_name}" '%s' "${current_value}"
|
|
return
|
|
fi
|
|
read -r -s -p "${question} [default is set]: " input
|
|
printf '\n'
|
|
printf -v "${var_name}" '%s' "${input:-$current_value}"
|
|
}
|
|
|
|
normalize_yes_no() {
|
|
local value
|
|
value="$(bool_value "$1")"
|
|
case "${value}" in
|
|
true) echo "yes" ;;
|
|
false) echo "no" ;;
|
|
ask) echo "ask" ;;
|
|
*) fail "Invalid yes/no value: $1" ;;
|
|
esac
|
|
}
|
|
|
|
parse_args() {
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--yes) ASSUME_YES=1; INTERACTIVE=0; shift ;;
|
|
--non-interactive) INTERACTIVE=0; shift ;;
|
|
--app-dir) APP_DIR="$2"; shift 2 ;;
|
|
--user) APP_USER="$2"; shift 2 ;;
|
|
--service-name) SERVICE_NAME="$2"; shift 2 ;;
|
|
--host) APP_HOST="$2"; shift 2 ;;
|
|
--port) APP_PORT="$2"; shift 2 ;;
|
|
--profile-name) PROFILE_NAME="$2"; shift 2 ;;
|
|
--scgi-url) SCGI_URL="$2"; shift 2 ;;
|
|
--rtorrent-socket) RTORRENT_SOCKET="$2"; shift 2 ;;
|
|
--auth) AUTH_MODE="$(bool_value "$2")"; shift 2 ;;
|
|
--auth-provider) AUTH_PROVIDER="$2"; shift 2 ;;
|
|
--auth-user) AUTH_USER="$2"; shift 2 ;;
|
|
--auth-password) AUTH_PASSWORD="$2"; shift 2 ;;
|
|
--admin-password) ADMIN_PASSWORD="$2"; shift 2 ;;
|
|
--logs) LOG_ENABLE="$(bool_value "$2")"; shift 2 ;;
|
|
--log-dir) LOG_DIR="$2"; shift 2 ;;
|
|
--libs) LIBS_MODE="$2"; shift 2 ;;
|
|
--reverse-proxy) REVERSE_PROXY="$(normalize_yes_no "$2")"; shift 2 ;;
|
|
--proxy-domains) PROXY_DOMAINS="$2"; shift 2 ;;
|
|
--cors-origins) CORS_ORIGINS="$2"; shift 2 ;;
|
|
--local-origins) LOCAL_ORIGINS="$2"; shift 2 ;;
|
|
--install-scgi-proxy) INSTALL_SCGI_PROXY="$(normalize_yes_no "$2")"; shift 2 ;;
|
|
--proxy-listen) RT_PROXY_LISTEN="$2"; shift 2 ;;
|
|
--proxy-token) RT_PROXY_TOKEN="$2"; shift 2 ;;
|
|
--proxy-allow-net) RT_PROXY_ALLOW_NET="$2"; shift 2 ;;
|
|
--proxy-target-network) RT_PROXY_TARGET_NETWORK="$2"; shift 2 ;;
|
|
--proxy-target-address) RT_PROXY_TARGET_ADDRESS="$2"; shift 2 ;;
|
|
--skip-profile) SKIP_PROFILE=1; shift ;;
|
|
-h|--help) usage; exit 0 ;;
|
|
*) fail "Unknown option: $1" ;;
|
|
esac
|
|
done
|
|
}
|
|
|
|
detect_os_family() {
|
|
# Note: Use the same Debian/RHEL split as the full stack installer.
|
|
[[ -f /etc/os-release ]] || fail "Cannot detect OS: /etc/os-release is missing."
|
|
# shellcheck disable=SC1091
|
|
. /etc/os-release
|
|
case "${ID:-} ${ID_LIKE:-}" in
|
|
*debian*|*ubuntu*) echo "debian" ;;
|
|
*rhel*|*fedora*|*centos*|*rocky*|*almalinux*) echo "rhel" ;;
|
|
*) fail "Unsupported OS: ID=${ID:-unknown}, ID_LIKE=${ID_LIKE:-unknown}." ;;
|
|
esac
|
|
}
|
|
|
|
install_prerequisites() {
|
|
# Note: Only pyTorrent runtime dependencies are installed; rTorrent is left untouched.
|
|
local family="$1"
|
|
case "${family}" in
|
|
debian)
|
|
export DEBIAN_FRONTEND=noninteractive
|
|
apt-get update
|
|
apt-get install -y --no-install-recommends ca-certificates curl git rsync sudo python3 python3-venv python3-dev python3-pip gcc pkg-config
|
|
;;
|
|
rhel)
|
|
local manager
|
|
manager="$(command -v dnf || command -v yum || true)"
|
|
[[ -n "${manager}" ]] || fail "dnf or yum is required."
|
|
"${manager}" install -y ca-certificates curl git rsync sudo python3 python3-devel python3-pip gcc pkgconf-pkg-config
|
|
;;
|
|
esac
|
|
}
|
|
|
|
ask_configuration() {
|
|
# Note: Interactive mode collects only pyTorrent-specific choices for an existing rTorrent host.
|
|
prompt APP_USER "pyTorrent system user" "pytorrent"
|
|
prompt APP_DIR "pyTorrent install directory" "/opt/pytorrent"
|
|
prompt SERVICE_NAME "systemd service name" "pytorrent"
|
|
prompt APP_HOST "Application bind host" "0.0.0.0"
|
|
prompt APP_PORT "Application port (use a high port like 8090; ports below 1024 may be blocked or require extra privileges)" "8090"
|
|
prompt PROFILE_NAME "pyTorrent profile name" "Local rTorrent"
|
|
|
|
if [[ -n "${RTORRENT_SOCKET}" ]]; then
|
|
INSTALL_SCGI_PROXY="${INSTALL_SCGI_PROXY:-ask}"
|
|
fi
|
|
if [[ "${INSTALL_SCGI_PROXY}" == "ask" ]]; then
|
|
prompt INSTALL_SCGI_PROXY "Install rtorrent-scgi-proxy for Unix socket backend? yes/no" "no"
|
|
INSTALL_SCGI_PROXY="$(normalize_yes_no "${INSTALL_SCGI_PROXY}")"
|
|
fi
|
|
if [[ "${INSTALL_SCGI_PROXY}" == "yes" ]]; then
|
|
prompt RT_PROXY_LISTEN "SCGI proxy listen address" "127.0.0.1:5050"
|
|
if [[ -z "${RT_PROXY_TOKEN}" ]]; then
|
|
RT_PROXY_TOKEN="$(python3 - <<'PY'
|
|
import secrets
|
|
print(secrets.token_urlsafe(32))
|
|
PY
|
|
)"
|
|
fi
|
|
prompt RT_PROXY_ALLOW_NET "SCGI proxy allowed client network/IP/CIDR" "127.0.0.1"
|
|
if [[ -n "${RTORRENT_SOCKET}" ]]; then
|
|
RT_PROXY_TARGET_NETWORK="unix"
|
|
RT_PROXY_TARGET_ADDRESS="${RTORRENT_SOCKET}"
|
|
fi
|
|
prompt RT_PROXY_TARGET_NETWORK "SCGI proxy backend network: tcp or unix" "${RT_PROXY_TARGET_NETWORK}"
|
|
prompt RT_PROXY_TARGET_ADDRESS "SCGI proxy backend address" "${RT_PROXY_TARGET_ADDRESS}"
|
|
SCGI_URL="scgi://${RT_PROXY_LISTEN}/proxy/${RT_PROXY_TOKEN}"
|
|
fi
|
|
prompt SCGI_URL "rTorrent SCGI URL for pyTorrent profile" "${SCGI_URL}"
|
|
|
|
if [[ "${AUTH_MODE}" == "ask" ]]; then
|
|
prompt AUTH_MODE "Enable pyTorrent authentication? yes/no" "no"
|
|
AUTH_MODE="$(bool_value "${AUTH_MODE}")"
|
|
fi
|
|
if [[ "${AUTH_MODE}" == "true" ]]; then
|
|
prompt AUTH_PROVIDER "Authentication provider: local, proxy or tinyauth" "local"
|
|
if [[ "${AUTH_PROVIDER}" == "local" ]]; then
|
|
prompt AUTH_USER "Local auth username to create/update" "pytorrent"
|
|
prompt_secret AUTH_PASSWORD "Password for local auth user" "pytorrent"
|
|
prompt_secret ADMIN_PASSWORD "Optional new admin password; leave default to keep current/default" "${ADMIN_PASSWORD}"
|
|
else
|
|
log "External auth selected. Configure trusted proxy headers according to auth.md."
|
|
fi
|
|
fi
|
|
|
|
prompt LOG_ENABLE "Enable file logging? yes/no" "true"
|
|
LOG_ENABLE="$(bool_value "${LOG_ENABLE}")"
|
|
if [[ "${LOG_ENABLE}" == "true" ]]; then
|
|
prompt LOG_DIR "Log directory" "data/logs"
|
|
prompt LOG_RETENTION_HOURS "Log retention in hours" "24"
|
|
fi
|
|
prompt LIBS_MODE "Frontend libraries mode: offline or online" "offline"
|
|
|
|
if [[ "${REVERSE_PROXY}" == "ask" ]]; then
|
|
prompt REVERSE_PROXY "Will pyTorrent run behind a reverse proxy? yes/no" "no"
|
|
REVERSE_PROXY="$(normalize_yes_no "${REVERSE_PROXY}")"
|
|
fi
|
|
if [[ "${REVERSE_PROXY}" == "yes" ]]; then
|
|
prompt PROXY_DOMAINS "Reverse proxy domains/origins, comma separated" "${PROXY_DOMAINS}"
|
|
prompt CORS_ORIGINS "Extra CORS origins, comma separated" "${CORS_ORIGINS}"
|
|
prompt LOCAL_ORIGINS "Extra local IP:port origins, comma separated" "${LOCAL_ORIGINS}"
|
|
fi
|
|
}
|
|
|
|
ensure_app_user() {
|
|
# Note: The service runs as a dedicated unprivileged user by default.
|
|
if ! id -u "${APP_USER}" >/dev/null 2>&1; then
|
|
local shell_path="/usr/sbin/nologin"
|
|
[[ -x "${shell_path}" ]] || shell_path="/sbin/nologin"
|
|
useradd --system --create-home --home-dir "/var/lib/${APP_USER}" --shell "${shell_path}" "${APP_USER}"
|
|
fi
|
|
}
|
|
|
|
copy_application() {
|
|
# Note: Copy the current repository without development artifacts or a previous virtualenv.
|
|
local project_dir
|
|
project_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
mkdir -p "${APP_DIR}"
|
|
rsync -a --delete --exclude '.git' --exclude 'venv' --exclude '__pycache__' --exclude '*.pyc' "${project_dir}/" "${APP_DIR}/"
|
|
chown -R "${APP_USER}:${APP_USER}" "${APP_DIR}" "/var/lib/${APP_USER}" || true
|
|
}
|
|
|
|
install_python_app() {
|
|
# Note: A private virtualenv keeps pyTorrent dependencies isolated from system Python packages.
|
|
cd "${APP_DIR}"
|
|
"${PYTHON_BIN}" -m venv venv
|
|
venv/bin/pip install --upgrade pip wheel
|
|
venv/bin/pip install -r requirements.txt
|
|
mkdir -p data instance logs
|
|
chown -R "${APP_USER}:${APP_USER}" "${APP_DIR}"
|
|
}
|
|
|
|
upsert_env_value() {
|
|
local key="$1" value="$2" file="${3:-${APP_DIR}/.env}"
|
|
touch "${file}"
|
|
if grep -qE "^${key}=" "${file}"; then
|
|
sed -i "s|^${key}=.*|${key}=${value}|" "${file}"
|
|
else
|
|
printf '%s=%s\n' "${key}" "${value}" >> "${file}"
|
|
fi
|
|
}
|
|
|
|
make_secret() {
|
|
python3 - <<'PY'
|
|
import secrets
|
|
print(secrets.token_urlsafe(48))
|
|
PY
|
|
}
|
|
|
|
normalize_origin() {
|
|
local item="$1"
|
|
item="${item# }"
|
|
item="${item% }"
|
|
[[ -n "${item}" ]] || return 0
|
|
if [[ "${item}" == http://* || "${item}" == https://* ]]; then
|
|
printf '%s\n' "${item%/}"
|
|
else
|
|
printf 'https://%s\n' "${item%/}"
|
|
fi
|
|
}
|
|
|
|
local_ip_origin() {
|
|
local ip
|
|
ip="$(hostname -I 2>/dev/null | awk '{print $1}' || true)"
|
|
[[ -n "${ip}" ]] && printf 'http://%s:%s\n' "${ip}" "${APP_PORT}"
|
|
}
|
|
|
|
build_origins() {
|
|
# Note: Reverse proxy mode must allow public HTTPS origins and direct local IP:port origins for Socket.IO/API checks.
|
|
{
|
|
IFS=',' read -ra domains <<< "${PROXY_DOMAINS}"
|
|
for item in "${domains[@]}"; do normalize_origin "${item}"; done
|
|
IFS=',' read -ra extra <<< "${CORS_ORIGINS}"
|
|
for item in "${extra[@]}"; do normalize_origin "${item}"; done
|
|
printf 'http://localhost:%s\n' "${APP_PORT}"
|
|
printf 'http://127.0.0.1:%s\n' "${APP_PORT}"
|
|
local_ip_origin
|
|
IFS=',' read -ra local_extra <<< "${LOCAL_ORIGINS}"
|
|
for item in "${local_extra[@]}"; do normalize_origin "${item}"; done
|
|
} | awk 'NF && !seen[$0]++' | paste -sd, -
|
|
}
|
|
|
|
write_env() {
|
|
# Note: The installer preserves .env comments but overwrites selected runtime keys.
|
|
cd "${APP_DIR}"
|
|
if [[ ! -f .env && -f .env.example ]]; then
|
|
cp .env.example .env
|
|
fi
|
|
upsert_env_value "PYTORRENT_SECRET_KEY" "$(make_secret)"
|
|
upsert_env_value "PYTORRENT_HOST" "${APP_HOST}"
|
|
upsert_env_value "PYTORRENT_PORT" "${APP_PORT}"
|
|
upsert_env_value "PYTORRENT_LOG_ENABLE" "${LOG_ENABLE}"
|
|
upsert_env_value "PYTORRENT_LOG_DIR" "${LOG_DIR}"
|
|
upsert_env_value "PYTORRENT_LOG_RETENTION_HOURS" "${LOG_RETENTION_HOURS}"
|
|
if [[ "${LOG_ENABLE}" == "true" ]]; then
|
|
upsert_env_value "PYTORRENT_GUNICORN_ACCESS_LOG" "${LOG_DIR%/}/gunicorn-access.log"
|
|
upsert_env_value "PYTORRENT_GUNICORN_ERROR_LOG" "${LOG_DIR%/}/gunicorn-error.log"
|
|
else
|
|
upsert_env_value "PYTORRENT_GUNICORN_ACCESS_LOG" "/dev/null"
|
|
upsert_env_value "PYTORRENT_GUNICORN_ERROR_LOG" "-"
|
|
fi
|
|
if [[ "${LIBS_MODE}" == "offline" ]]; then
|
|
upsert_env_value "PYTORRENT_USE_OFFLINE_LIBS" "true"
|
|
elif [[ "${LIBS_MODE}" == "online" ]]; then
|
|
upsert_env_value "PYTORRENT_USE_OFFLINE_LIBS" "false"
|
|
else
|
|
fail "Invalid --libs value: ${LIBS_MODE}"
|
|
fi
|
|
if [[ "${AUTH_MODE}" == "true" ]]; then
|
|
upsert_env_value "PYTORRENT_AUTH_ENABLE" "true"
|
|
upsert_env_value "PYTORRENT_AUTH_PROVIDER" "${AUTH_PROVIDER}"
|
|
if [[ "${AUTH_PROVIDER}" == "proxy" || "${AUTH_PROVIDER}" == "tinyauth" ]]; then
|
|
upsert_env_value "PYTORRENT_AUTH_PROXY_AUTO_CREATE" "true"
|
|
upsert_env_value "PYTORRENT_AUTH_PROXY_AUTO_CREATE_ROLE" "admin"
|
|
upsert_env_value "PYTORRENT_AUTH_PROXY_AUTO_CREATE_PERMISSION" "rw"
|
|
fi
|
|
else
|
|
upsert_env_value "PYTORRENT_AUTH_ENABLE" "false"
|
|
fi
|
|
if [[ "${REVERSE_PROXY}" == "yes" ]]; then
|
|
local origins
|
|
origins="$(build_origins)"
|
|
upsert_env_value "PYTORRENT_PROXY_FIX_ENABLE" "true"
|
|
upsert_env_value "PYTORRENT_SESSION_COOKIE_SECURE" "true"
|
|
upsert_env_value "PYTORRENT_SOCKETIO_CORS_ALLOWED_ORIGINS" "${origins}"
|
|
upsert_env_value "PYTORRENT_API_ALLOWED_ORIGINS" "${origins}"
|
|
else
|
|
upsert_env_value "PYTORRENT_PROXY_FIX_ENABLE" "false"
|
|
upsert_env_value "PYTORRENT_SESSION_COOKIE_SECURE" "false"
|
|
fi
|
|
if [[ "${LOG_ENABLE}" == "true" ]]; then
|
|
if [[ "${LOG_DIR}" == /* ]]; then
|
|
mkdir -p "${LOG_DIR}"
|
|
chown -R "${APP_USER}:${APP_USER}" "${LOG_DIR}" || true
|
|
else
|
|
mkdir -p "${APP_DIR}/${LOG_DIR}"
|
|
chown -R "${APP_USER}:${APP_USER}" "${APP_DIR}/${LOG_DIR}" || true
|
|
fi
|
|
fi
|
|
chown "${APP_USER}:${APP_USER}" .env || true
|
|
}
|
|
|
|
install_frontend_libs() {
|
|
# Note: Offline mode downloads local JS/CSS assets during installation; online mode uses CDN links.
|
|
if [[ "${LIBS_MODE}" == "offline" && -f "${APP_DIR}/scripts/download_frontend_libs.py" ]]; then
|
|
sudo -u "${APP_USER}" "${APP_DIR}/venv/bin/python" "${APP_DIR}/scripts/download_frontend_libs.py" || true
|
|
fi
|
|
if [[ -f "${APP_DIR}/scripts/download_geoip.sh" ]]; then
|
|
sudo -u "${APP_USER}" bash "${APP_DIR}/scripts/download_geoip.sh" "${APP_DIR}/data/GeoLite2-City.mmdb" || true
|
|
fi
|
|
}
|
|
|
|
configure_database() {
|
|
# Note: Configure the initial database, local users and rTorrent profile without needing an API token.
|
|
sudo -u "${APP_USER}" env \
|
|
AUTH_MODE="${AUTH_MODE}" \
|
|
AUTH_PROVIDER="${AUTH_PROVIDER}" \
|
|
AUTH_USER="${AUTH_USER}" \
|
|
AUTH_PASSWORD="${AUTH_PASSWORD}" \
|
|
ADMIN_PASSWORD="${ADMIN_PASSWORD}" \
|
|
PROFILE_NAME="${PROFILE_NAME}" \
|
|
SCGI_URL="${SCGI_URL}" \
|
|
SKIP_PROFILE="${SKIP_PROFILE}" \
|
|
"${APP_DIR}/venv/bin/python" - <<'PY'
|
|
import os
|
|
from pytorrent.db import connect, init_db, utcnow
|
|
from pytorrent.services.auth import password_hash
|
|
|
|
init_db()
|
|
now = utcnow()
|
|
auth_enabled = os.environ.get("AUTH_MODE") == "true"
|
|
auth_provider = os.environ.get("AUTH_PROVIDER", "local")
|
|
with connect() as conn:
|
|
if auth_enabled and auth_provider == "local":
|
|
username = os.environ.get("AUTH_USER", "pytorrent").strip() or "pytorrent"
|
|
password = os.environ.get("AUTH_PASSWORD", "pytorrent")
|
|
row = conn.execute("SELECT id FROM users WHERE username=?", (username,)).fetchone()
|
|
if row:
|
|
conn.execute(
|
|
"UPDATE users SET password_hash=?, role='admin', is_active=1, updated_at=? WHERE username=?",
|
|
(password_hash(password), now, username),
|
|
)
|
|
else:
|
|
conn.execute(
|
|
"INSERT INTO users(username,password_hash,role,is_active,created_at,updated_at) VALUES(?,?,?,?,?,?)",
|
|
(username, password_hash(password), "admin", 1, now, now),
|
|
)
|
|
admin_password = os.environ.get("ADMIN_PASSWORD", "")
|
|
if admin_password:
|
|
conn.execute(
|
|
"UPDATE users SET password_hash=?, role='admin', is_active=1, updated_at=? WHERE username='admin'",
|
|
(password_hash(admin_password), now),
|
|
)
|
|
if os.environ.get("SKIP_PROFILE") != "1":
|
|
profile_name = os.environ.get("PROFILE_NAME", "Local rTorrent")
|
|
scgi_url = os.environ.get("SCGI_URL", "scgi://127.0.0.1:5000")
|
|
existing = conn.execute(
|
|
"SELECT id FROM rtorrent_profiles WHERE name=? OR scgi_url=? ORDER BY id LIMIT 1",
|
|
(profile_name, scgi_url),
|
|
).fetchone()
|
|
if existing:
|
|
pid = int(existing["id"])
|
|
conn.execute(
|
|
"UPDATE rtorrent_profiles SET name=?, scgi_url=?, is_default=1, updated_at=? WHERE id=?",
|
|
(profile_name, scgi_url, now, pid),
|
|
)
|
|
else:
|
|
cur = conn.execute(
|
|
"""INSERT INTO rtorrent_profiles(user_id,name,scgi_url,is_default,timeout_seconds,max_parallel_jobs,light_parallel_jobs,light_job_timeout_seconds,heavy_job_timeout_seconds,pending_job_timeout_seconds,is_remote,created_at,updated_at)
|
|
VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?)""",
|
|
(1, profile_name, scgi_url, 1, 10, 5, 4, 300, 7200, 900, 0, now, now),
|
|
)
|
|
pid = int(cur.lastrowid)
|
|
conn.execute("UPDATE rtorrent_profiles SET is_default=0 WHERE id<>?", (pid,))
|
|
conn.execute(
|
|
"UPDATE user_preferences SET active_rtorrent_id=?, updated_at=? WHERE user_id=1",
|
|
(pid, now),
|
|
)
|
|
print("Database initialized")
|
|
PY
|
|
}
|
|
|
|
write_systemd_service() {
|
|
# Note: The systemd unit mirrors the repository service but uses installer-selected paths and user.
|
|
cat > "/etc/systemd/system/${SERVICE_NAME}.service" <<SERVICE
|
|
[Unit]
|
|
Description=pyTorrent Web UI
|
|
After=network-online.target
|
|
Wants=network-online.target
|
|
|
|
[Service]
|
|
Type=simple
|
|
User=${APP_USER}
|
|
Group=${APP_USER}
|
|
WorkingDirectory=${APP_DIR}
|
|
Environment="PYTHONUNBUFFERED=1"
|
|
EnvironmentFile=${APP_DIR}/.env
|
|
ExecStart=${APP_DIR}/venv/bin/gunicorn -c ${APP_DIR}/gunicorn.conf.py --worker-class gthread --workers 1 --threads 32 --bind \${PYTORRENT_HOST}:\${PYTORRENT_PORT} wsgi:app
|
|
Restart=always
|
|
RestartSec=3
|
|
KillSignal=SIGINT
|
|
TimeoutStopSec=20
|
|
NoNewPrivileges=true
|
|
PrivateTmp=true
|
|
|
|
[Install]
|
|
WantedBy=multi-user.target
|
|
SERVICE
|
|
systemctl daemon-reload
|
|
systemctl enable "${SERVICE_NAME}"
|
|
systemctl restart "${SERVICE_NAME}"
|
|
}
|
|
|
|
install_scgi_proxy() {
|
|
# Note: The proxy exposes a TCP SCGI endpoint for pyTorrent when rTorrent listens on a Unix socket.
|
|
[[ "${INSTALL_SCGI_PROXY}" == "yes" ]] || return 0
|
|
if ! id -u "${RT_PROXY_USER}" >/dev/null 2>&1; then
|
|
local shell_path="/usr/sbin/nologin"
|
|
[[ -x "${shell_path}" ]] || shell_path="/sbin/nologin"
|
|
useradd --system --no-create-home --shell "${shell_path}" "${RT_PROXY_USER}"
|
|
fi
|
|
curl -fL "${RT_PROXY_BINARY_URL}" -o /usr/local/bin/rtorrent-scgi-proxy
|
|
chmod 0755 /usr/local/bin/rtorrent-scgi-proxy
|
|
cat > /etc/rtorrent-scgi-proxy.env <<ENV
|
|
LISTEN_ADDR=${RT_PROXY_LISTEN}
|
|
TOKEN=${RT_PROXY_TOKEN}
|
|
TARGET_NETWORK=${RT_PROXY_TARGET_NETWORK}
|
|
TARGET_ADDRESS=${RT_PROXY_TARGET_ADDRESS}
|
|
TARGET_URI=${RT_PROXY_TARGET_URI}
|
|
ALLOW_NET=${RT_PROXY_ALLOW_NET}
|
|
READ_TIMEOUT=15s
|
|
WRITE_TIMEOUT=30s
|
|
DIAL_TIMEOUT=5s
|
|
MAX_HEADER_BYTES=65536
|
|
MAX_CONTENT_BYTES=10485760
|
|
ENV
|
|
chmod 0600 /etc/rtorrent-scgi-proxy.env
|
|
chown root:root /etc/rtorrent-scgi-proxy.env
|
|
cat > /etc/systemd/system/rtorrent-scgi-proxy.service <<SERVICE
|
|
[Unit]
|
|
Description=rTorrent SCGI proxy
|
|
After=network-online.target
|
|
Wants=network-online.target
|
|
|
|
[Service]
|
|
Type=simple
|
|
User=${RT_PROXY_USER}
|
|
Group=${RT_PROXY_USER}
|
|
EnvironmentFile=/etc/rtorrent-scgi-proxy.env
|
|
ExecStart=/usr/local/bin/rtorrent-scgi-proxy
|
|
Restart=on-failure
|
|
RestartSec=2
|
|
|
|
NoNewPrivileges=yes
|
|
PrivateTmp=yes
|
|
ProtectSystem=strict
|
|
ProtectHome=yes
|
|
ProtectKernelTunables=yes
|
|
ProtectKernelModules=yes
|
|
ProtectControlGroups=yes
|
|
MemoryDenyWriteExecute=yes
|
|
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
|
|
LockPersonality=yes
|
|
|
|
[Install]
|
|
WantedBy=multi-user.target
|
|
SERVICE
|
|
systemctl daemon-reload
|
|
systemctl enable --now rtorrent-scgi-proxy
|
|
}
|
|
|
|
print_summary() {
|
|
# Note: Print only actionable installation facts, not release notes.
|
|
local base_url="http://127.0.0.1:${APP_PORT}"
|
|
log "Installed in ${APP_DIR}"
|
|
log "Service: ${SERVICE_NAME}"
|
|
log "Local URL: ${base_url}"
|
|
log "rTorrent profile SCGI URL: ${SCGI_URL}"
|
|
if [[ "${AUTH_MODE}" == "true" && "${AUTH_PROVIDER}" == "local" ]]; then
|
|
log "Local auth user: ${AUTH_USER}"
|
|
if [[ "${AUTH_PASSWORD}" == "pytorrent" ]]; then
|
|
log "Default local password is still set. Change it after first login."
|
|
fi
|
|
elif [[ "${AUTH_MODE}" == "true" ]]; then
|
|
log "External auth provider: ${AUTH_PROVIDER}. Finish proxy setup according to auth.md."
|
|
fi
|
|
if [[ "${REVERSE_PROXY}" == "yes" ]]; then
|
|
log "Reverse proxy CORS/API origins: $(build_origins)"
|
|
fi
|
|
}
|
|
|
|
main() {
|
|
parse_args "$@"
|
|
require_root
|
|
ask_configuration
|
|
local family
|
|
family="$(detect_os_family)"
|
|
install_prerequisites "${family}"
|
|
install_scgi_proxy
|
|
ensure_app_user
|
|
copy_application
|
|
install_python_app
|
|
write_env
|
|
install_frontend_libs
|
|
configure_database
|
|
write_systemd_service
|
|
systemctl status "${SERVICE_NAME}" --no-pager --lines=20 || true
|
|
print_summary
|
|
}
|
|
|
|
main "$@"
|