install scrpts and fiix ux
This commit is contained in:
+2
-1
@@ -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
|
||||
|
||||
@@ -42,6 +42,5 @@ data/logs/*
|
||||
|
||||
|
||||
todo.txt
|
||||
pytorrent/static/libs/*
|
||||
!pytorrent/static/libs/pytorrent-themes/
|
||||
!pytorrent/static/libs/pytorrent-themes/**
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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