install scrpts and fiix ux
This commit is contained in:
@@ -196,3 +196,65 @@ PYTORRENT_DEBUG_INSTALL=1
|
||||
```
|
||||
|
||||
On RHEL-compatible systems the installer also tries to enable CRB/PowerTools and installs `libcurl-devel`, `redhat-rpm-config`, `patch`, `diffutils`, `findutils`, `file`, and `libstdc++-devel`, because minimal Alma/Rocky images often do not include enough build tooling.
|
||||
|
||||
## pyTorrent-only installer
|
||||
|
||||
Use this installer when rTorrent is already configured and pyTorrent only needs a web UI service and one rTorrent profile.
|
||||
|
||||
Interactive local run:
|
||||
|
||||
```bash
|
||||
sudo bash scripts/install_pytorrent_only.sh
|
||||
```
|
||||
|
||||
Bootstrap run from repository:
|
||||
|
||||
```bash
|
||||
curl -fsSL https://git.linuxiarz.pl/gru/pyTorrent/raw/branch/master/scripts/install_pytorrent.sh | sudo bash
|
||||
```
|
||||
|
||||
Non-interactive example for an existing TCP SCGI backend:
|
||||
|
||||
```bash
|
||||
sudo bash scripts/install_pytorrent_only.sh \
|
||||
--yes \
|
||||
--user pytorrent \
|
||||
--port 8090 \
|
||||
--scgi-url scgi://127.0.0.1:5000 \
|
||||
--auth enable \
|
||||
--auth-provider local \
|
||||
--auth-user pytorrent \
|
||||
--auth-password 'change-this-password' \
|
||||
--logs enable \
|
||||
--libs offline
|
||||
```
|
||||
|
||||
Reverse proxy example:
|
||||
|
||||
```bash
|
||||
sudo bash scripts/install_pytorrent_only.sh \
|
||||
--yes \
|
||||
--port 8090 \
|
||||
--reverse-proxy yes \
|
||||
--proxy-domains torrent.example.com,pythong.example.com \
|
||||
--local-origins http://10.10.10.22:8890
|
||||
```
|
||||
|
||||
Unix socket rTorrent backend via rtorrent-scgi-proxy:
|
||||
|
||||
```bash
|
||||
sudo bash scripts/install_pytorrent_only.sh \
|
||||
--yes \
|
||||
--rtorrent-socket /run/rtorrent/rtorrent.sock \
|
||||
--install-scgi-proxy yes \
|
||||
--proxy-listen 127.0.0.1:5050 \
|
||||
--proxy-allow-net 127.0.0.1 \
|
||||
--scgi-url scgi://127.0.0.1:5050/proxy/change-me-long-random-token
|
||||
```
|
||||
|
||||
Notes:
|
||||
|
||||
- The default application port is `8090`; high ports are recommended because ports below `1024` usually require extra privileges or may be blocked by the system.
|
||||
- Offline frontend libraries are the default. Use `--libs online` only when CDN loading is preferred.
|
||||
- Local auth is configured directly by the installer. External auth providers require a trusted reverse proxy setup; see `auth.md`.
|
||||
- Reverse proxy mode enables `PYTORRENT_PROXY_FIX_ENABLE`, secure cookies and CORS/API origins for the HTTPS domains plus localhost/local IP origins.
|
||||
|
||||
Executable
+77
@@ -0,0 +1,77 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Bootstrap installer for pyTorrent only.
|
||||
# Intended usage:
|
||||
# curl -fsSL https://git.linuxiarz.pl/gru/pyTorrent/raw/branch/master/scripts/install_pytorrent.sh | sudo bash
|
||||
|
||||
if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then
|
||||
cat <<'USAGE'
|
||||
Usage: curl -fsSL https://git.linuxiarz.pl/gru/pyTorrent/raw/branch/master/scripts/install_pytorrent.sh | sudo bash -s -- [options]
|
||||
|
||||
This bootstrap downloads pyTorrent and forwards all options to scripts/install_pytorrent_only.sh.
|
||||
Run scripts/install_pytorrent_only.sh --help inside the repository for the full option list.
|
||||
USAGE
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [[ "${EUID}" -ne 0 ]]; then
|
||||
echo "Run as root, for example: curl -fsSL <url> | sudo bash" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
REPO_URL="${PYTORRENT_REPO_URL:-https://git.linuxiarz.pl/gru/pyTorrent}"
|
||||
REPO_BRANCH="${PYTORRENT_REPO_BRANCH:-master}"
|
||||
WORK_DIR="${PYTORRENT_BOOTSTRAP_DIR:-/tmp/pytorrent-only-installer}"
|
||||
KEEP_WORK_DIR="${PYTORRENT_KEEP_BOOTSTRAP_DIR:-0}"
|
||||
ARCHIVE_URL="${PYTORRENT_ARCHIVE_URL:-${REPO_URL%/}/archive/${REPO_BRANCH}.tar.gz}"
|
||||
PROJECT_DIR="${WORK_DIR}/src"
|
||||
ARCHIVE_PATH="${WORK_DIR}/pytorrent.tar.gz"
|
||||
|
||||
log() { printf '[pyTorrent bootstrap] %s\n' "$*"; }
|
||||
fail() { printf '[pyTorrent bootstrap] ERROR: %s\n' "$*" >&2; exit 1; }
|
||||
command_exists() { command -v "$1" >/dev/null 2>&1; }
|
||||
|
||||
prepare_downloader() {
|
||||
# Note: Bootstrap installs only tools required to fetch and unpack the repository.
|
||||
if command_exists apt-get; then
|
||||
apt-get update
|
||||
apt-get install -y --no-install-recommends ca-certificates curl tar gzip python3 sudo
|
||||
elif command_exists dnf; then
|
||||
dnf install -y ca-certificates curl tar gzip python3 sudo
|
||||
elif command_exists yum; then
|
||||
yum install -y ca-certificates curl tar gzip python3 sudo
|
||||
fi
|
||||
if command_exists curl; then DOWNLOADER="curl"; return; fi
|
||||
if command_exists wget; then DOWNLOADER="wget"; return; fi
|
||||
fail "curl or wget is required."
|
||||
}
|
||||
|
||||
download_file() {
|
||||
local url="$1" destination="$2"
|
||||
if [[ "${DOWNLOADER}" == "curl" ]]; then
|
||||
curl -fL "${url}" -o "${destination}"
|
||||
else
|
||||
wget -O "${destination}" "${url}"
|
||||
fi
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
if [[ "${KEEP_WORK_DIR}" != "1" ]]; then
|
||||
rm -rf "${WORK_DIR}"
|
||||
else
|
||||
log "Keeping bootstrap directory: ${WORK_DIR}"
|
||||
fi
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
prepare_downloader
|
||||
rm -rf "${WORK_DIR}"
|
||||
mkdir -p "${PROJECT_DIR}"
|
||||
log "Downloading pyTorrent from ${ARCHIVE_URL}"
|
||||
download_file "${ARCHIVE_URL}" "${ARCHIVE_PATH}"
|
||||
tar -xzf "${ARCHIVE_PATH}" -C "${PROJECT_DIR}" --strip-components=1
|
||||
[[ -f "${PROJECT_DIR}/scripts/install_pytorrent_only.sh" ]] || fail "Missing scripts/install_pytorrent_only.sh in downloaded repository."
|
||||
chmod +x "${PROJECT_DIR}/scripts/install_pytorrent_only.sh"
|
||||
log "Running pyTorrent-only installer"
|
||||
bash "${PROJECT_DIR}/scripts/install_pytorrent_only.sh" "$@"
|
||||
Executable
+615
@@ -0,0 +1,615 @@
|
||||
#!/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 "$@"
|
||||
Reference in New Issue
Block a user