install scrpts and fiix ux #11

Merged
gru merged 1 commits from fix_ui into master 2026-05-31 07:41:04 +02:00
11 changed files with 849 additions and 48 deletions
+2 -1
View File
@@ -29,6 +29,7 @@ PYTORRENT_SMART_QUEUE_DIAGNOSTICS=none
PYTORRENT_SMART_QUEUE_DIAGNOSTICS_MAX_ITEMS=200
# Logs
PYTORRENT_LOG_ENABLE=false
PYTORRENT_LOG_DIR=data/logs
PYTORRENT_LOG_RETENTION_HOURS=24
PYTORRENT_GUNICORN_ACCESS_LOG=data/logs/gunicorn-access.log
@@ -68,4 +69,4 @@ PYTORRENT_SESSION_COOKIE_SECURE=false
# bypass auth on specific hosts (ex. local ip)
PYTORRENT_AUTH_BYPASS_HOSTS=10.11.1.11:8090,10.11.1.11
PYTORRENT_AUTH_BYPASS_USER=admin
PYTORRENT_AUTH_BYPASS_USER=admin
-1
View File
@@ -42,6 +42,5 @@ data/logs/*
todo.txt
pytorrent/static/libs/*
!pytorrent/static/libs/pytorrent-themes/
!pytorrent/static/libs/pytorrent-themes/**
+1
View File
@@ -104,6 +104,7 @@ JOBS_RETENTION_DAYS = _env_int("PYTORRENT_JOBS_RETENTION_DAYS", 30, 1)
SMART_QUEUE_HISTORY_RETENTION_DAYS = _env_int("PYTORRENT_SMART_QUEUE_HISTORY_RETENTION_DAYS", 30, 1)
LOG_RETENTION_DAYS = _env_int("PYTORRENT_LOG_RETENTION_DAYS", 1, 1)
LOG_RETENTION_HOURS = _env_int("PYTORRENT_LOG_RETENTION_HOURS", 24, 1)
LOG_ENABLE = _env_bool("PYTORRENT_LOG_ENABLE", True)
LOG_DIR = Path(os.getenv("PYTORRENT_LOG_DIR", "data/logs"))
if not LOG_DIR.is_absolute():
LOG_DIR = BASE_DIR / LOG_DIR
+4 -1
View File
@@ -8,7 +8,7 @@ from typing import Any
from flask import Flask, g, request
from .config import LOG_DIR, LOG_RETENTION_HOURS
from .config import LOG_DIR, LOG_ENABLE, LOG_RETENTION_HOURS
_CONFIGURED = False
@@ -33,6 +33,9 @@ def _make_handler(path: Path, level: int) -> TimedRotatingFileHandler:
def configure_logging(app: Flask | None = None) -> None:
"""Route pyTorrent app, error and access logs to the configured data log directory."""
global _CONFIGURED
if not LOG_ENABLE:
# Note: Installation can disable file logging while keeping normal service stdout/stderr available.
return
LOG_DIR.mkdir(parents=True, exist_ok=True)
if not _CONFIGURED:
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+85 -42
View File
@@ -4641,7 +4641,7 @@ body,
}
}
/* Note: Peers tables keep hostnames readable without letting the Host column dominate the layout. */
/* Note: Peers tables keep hostnames readable and keep progress columns stable. */
.peers-table {
min-width: 960px;
table-layout: fixed;
@@ -4656,56 +4656,76 @@ body,
}
.peers-table .peer-progress-wide {
min-width: 108px;
min-width: 0;
width: 100%;
}
.peers-table-hosts th:nth-child(1),
.peers-table-hosts td:nth-child(1) {
.peers-table:not(.mobile-details-peers-table):not(.peers-table-hosts) th:nth-child(1),
.peers-table:not(.mobile-details-peers-table):not(.peers-table-hosts) td:nth-child(1),
.peers-table:not(.mobile-details-peers-table).peers-table-hosts th:nth-child(1),
.peers-table:not(.mobile-details-peers-table).peers-table-hosts td:nth-child(1) {
width: 4%;
}
.peers-table-hosts th:nth-child(2),
.peers-table-hosts td:nth-child(2) {
.peers-table:not(.mobile-details-peers-table):not(.peers-table-hosts) th:nth-child(2),
.peers-table:not(.mobile-details-peers-table):not(.peers-table-hosts) td:nth-child(2),
.peers-table:not(.mobile-details-peers-table).peers-table-hosts th:nth-child(2),
.peers-table:not(.mobile-details-peers-table).peers-table-hosts td:nth-child(2) {
width: 13%;
}
.peers-table-hosts th:nth-child(3),
.peers-table-hosts td:nth-child(3) {
.peers-table:not(.mobile-details-peers-table).peers-table-hosts th:nth-child(3),
.peers-table:not(.mobile-details-peers-table).peers-table-hosts td:nth-child(3) {
width: 15%;
}
.peers-table-hosts th:nth-child(4),
.peers-table-hosts td:nth-child(4),
.peers-table-hosts th:nth-child(5),
.peers-table-hosts td:nth-child(5) {
.peers-table:not(.mobile-details-peers-table):not(.peers-table-hosts) th:nth-child(3),
.peers-table:not(.mobile-details-peers-table):not(.peers-table-hosts) td:nth-child(3),
.peers-table:not(.mobile-details-peers-table):not(.peers-table-hosts) th:nth-child(4),
.peers-table:not(.mobile-details-peers-table):not(.peers-table-hosts) td:nth-child(4),
.peers-table:not(.mobile-details-peers-table).peers-table-hosts th:nth-child(4),
.peers-table:not(.mobile-details-peers-table).peers-table-hosts td:nth-child(4),
.peers-table:not(.mobile-details-peers-table).peers-table-hosts th:nth-child(5),
.peers-table:not(.mobile-details-peers-table).peers-table-hosts td:nth-child(5) {
width: 8%;
}
.peers-table-hosts th:nth-child(6),
.peers-table-hosts td:nth-child(6) {
.peers-table:not(.mobile-details-peers-table):not(.peers-table-hosts) th:nth-child(5),
.peers-table:not(.mobile-details-peers-table):not(.peers-table-hosts) td:nth-child(5),
.peers-table:not(.mobile-details-peers-table).peers-table-hosts th:nth-child(6),
.peers-table:not(.mobile-details-peers-table).peers-table-hosts td:nth-child(6) {
width: 15%;
}
.peers-table-hosts th:nth-child(7),
.peers-table-hosts td:nth-child(7) {
width: 10%;
.peers-table:not(.mobile-details-peers-table):not(.peers-table-hosts) th:nth-child(6),
.peers-table:not(.mobile-details-peers-table):not(.peers-table-hosts) td:nth-child(6),
.peers-table:not(.mobile-details-peers-table).peers-table-hosts th:nth-child(7),
.peers-table:not(.mobile-details-peers-table).peers-table-hosts td:nth-child(7) {
width: 8rem;
}
.peers-table-hosts th:nth-child(8),
.peers-table-hosts td:nth-child(8),
.peers-table-hosts th:nth-child(9),
.peers-table-hosts td:nth-child(9) {
.peers-table:not(.mobile-details-peers-table):not(.peers-table-hosts) th:nth-child(7),
.peers-table:not(.mobile-details-peers-table):not(.peers-table-hosts) td:nth-child(7),
.peers-table:not(.mobile-details-peers-table):not(.peers-table-hosts) th:nth-child(8),
.peers-table:not(.mobile-details-peers-table):not(.peers-table-hosts) td:nth-child(8),
.peers-table:not(.mobile-details-peers-table).peers-table-hosts th:nth-child(8),
.peers-table:not(.mobile-details-peers-table).peers-table-hosts td:nth-child(8),
.peers-table:not(.mobile-details-peers-table).peers-table-hosts th:nth-child(9),
.peers-table:not(.mobile-details-peers-table).peers-table-hosts td:nth-child(9) {
width: 6%;
}
.peers-table-hosts th:nth-child(10),
.peers-table-hosts td:nth-child(10) {
.peers-table:not(.mobile-details-peers-table):not(.peers-table-hosts) th:nth-child(9),
.peers-table:not(.mobile-details-peers-table):not(.peers-table-hosts) td:nth-child(9),
.peers-table:not(.mobile-details-peers-table).peers-table-hosts th:nth-child(10),
.peers-table:not(.mobile-details-peers-table).peers-table-hosts td:nth-child(10) {
width: 5%;
}
.peers-table-hosts th:nth-child(11),
.peers-table-hosts td:nth-child(11) {
.peers-table:not(.mobile-details-peers-table):not(.peers-table-hosts) th:nth-child(10),
.peers-table:not(.mobile-details-peers-table):not(.peers-table-hosts) td:nth-child(10),
.peers-table:not(.mobile-details-peers-table).peers-table-hosts th:nth-child(11),
.peers-table:not(.mobile-details-peers-table).peers-table-hosts td:nth-child(11) {
width: 10%;
}
@@ -4718,7 +4738,7 @@ body,
white-space: nowrap;
}
/* Note: Mobile torrent details use a narrower fixed table so long reverse-DNS names cannot stretch the modal. */
/* Note: Mobile torrent details use a stable table so progress bars always render on a 0-100% track. */
.mobile-details-modal .modal-body {
overflow-x: hidden;
}
@@ -4728,36 +4748,54 @@ body,
}
.mobile-details-peers-table {
min-width: 720px;
margin-bottom: 0;
min-width: 780px;
}
.mobile-details-peers-table:not(.peers-table-hosts) th:nth-child(1),
.mobile-details-peers-table:not(.peers-table-hosts) td:nth-child(1),
.mobile-details-peers-table.peers-table-hosts th:nth-child(1),
.mobile-details-peers-table.peers-table-hosts td:nth-child(1) {
width: 5%;
}
.mobile-details-peers-table:not(.peers-table-hosts) th:nth-child(2),
.mobile-details-peers-table:not(.peers-table-hosts) td:nth-child(2),
.mobile-details-peers-table.peers-table-hosts th:nth-child(2),
.mobile-details-peers-table.peers-table-hosts td:nth-child(2) {
width: 14%;
}
.mobile-details-peers-table.peers-table-hosts th:nth-child(3),
.mobile-details-peers-table.peers-table-hosts td:nth-child(3),
.mobile-details-peers-table.peers-table-hosts th:nth-child(4),
.mobile-details-peers-table.peers-table-hosts td:nth-child(4) {
width: 15%;
}
.mobile-details-peers-table.peers-table-hosts th:nth-child(5),
.mobile-details-peers-table.peers-table-hosts td:nth-child(5) {
.mobile-details-peers-table.peers-table-hosts td:nth-child(3) {
width: 16%;
}
.mobile-details-peers-table.peers-table-hosts th:nth-child(6),
.mobile-details-peers-table.peers-table-hosts td:nth-child(6) {
width: 10%;
.mobile-details-peers-table:not(.peers-table-hosts) th:nth-child(3),
.mobile-details-peers-table:not(.peers-table-hosts) td:nth-child(3),
.mobile-details-peers-table.peers-table-hosts th:nth-child(4),
.mobile-details-peers-table.peers-table-hosts td:nth-child(4) {
width: 16%;
}
.mobile-details-peers-table:not(.peers-table-hosts) th:nth-child(4),
.mobile-details-peers-table:not(.peers-table-hosts) td:nth-child(4),
.mobile-details-peers-table.peers-table-hosts th:nth-child(5),
.mobile-details-peers-table.peers-table-hosts td:nth-child(5) {
width: 15%;
}
.mobile-details-peers-table:not(.peers-table-hosts) th:nth-child(5),
.mobile-details-peers-table:not(.peers-table-hosts) td:nth-child(5),
.mobile-details-peers-table.peers-table-hosts th:nth-child(6),
.mobile-details-peers-table.peers-table-hosts td:nth-child(6) {
width: 8rem;
}
.mobile-details-peers-table:not(.peers-table-hosts) th:nth-child(6),
.mobile-details-peers-table:not(.peers-table-hosts) td:nth-child(6),
.mobile-details-peers-table:not(.peers-table-hosts) th:nth-child(7),
.mobile-details-peers-table:not(.peers-table-hosts) td:nth-child(7),
.mobile-details-peers-table.peers-table-hosts th:nth-child(7),
.mobile-details-peers-table.peers-table-hosts td:nth-child(7),
.mobile-details-peers-table.peers-table-hosts th:nth-child(8),
@@ -4765,13 +4803,19 @@ body,
width: 7%;
}
.mobile-details-peers-table:not(.peers-table-hosts) th:nth-child(8),
.mobile-details-peers-table:not(.peers-table-hosts) td:nth-child(8),
.mobile-details-peers-table.peers-table-hosts th:nth-child(9),
.mobile-details-peers-table.peers-table-hosts td:nth-child(9),
.mobile-details-peers-table.peers-table-hosts th:nth-child(10),
.mobile-details-peers-table.peers-table-hosts td:nth-child(10) {
.mobile-details-peers-table.peers-table-hosts td:nth-child(9) {
width: 6%;
}
.mobile-details-peers-table:not(.peers-table-hosts) th:nth-child(9),
.mobile-details-peers-table:not(.peers-table-hosts) td:nth-child(9),
.mobile-details-peers-table.peers-table-hosts th:nth-child(10),
.mobile-details-peers-table.peers-table-hosts td:nth-child(10) {
width: 8%;
}
/* App modal widths stay consistent while Bootstrap still handles full-screen mobile breakpoints. */
.app-modal-dialog,
@@ -5237,7 +5281,6 @@ body,
overflow-wrap: anywhere;
}
.mobile-details-peers-table,
.mobile-details-files-table {
margin-bottom: 0;
}
+62
View File
@@ -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.
+77
View File
@@ -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" "$@"
+615
View File
@@ -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 "$@"