gunicorn #1
@@ -6,6 +6,7 @@ PYTORRENT_DEBUG=0
|
|||||||
PYTORRENT_POLL_INTERVAL=1.0
|
PYTORRENT_POLL_INTERVAL=1.0
|
||||||
PYTORRENT_WORKERS=16
|
PYTORRENT_WORKERS=16
|
||||||
PYTORRENT_GEOIP_DB=data/GeoLite2-City.mmdb
|
PYTORRENT_GEOIP_DB=data/GeoLite2-City.mmdb
|
||||||
|
PYTORRENT_ALLOW_UNSAFE_WERKZEUG=0
|
||||||
|
|
||||||
# Retention / Smart Queue
|
# Retention / Smart Queue
|
||||||
PYTORRENT_TRAFFIC_HISTORY_RETENTION_DAYS=90
|
PYTORRENT_TRAFFIC_HISTORY_RETENTION_DAYS=90
|
||||||
|
|||||||
17
README.md
17
README.md
@@ -37,6 +37,23 @@ python app.py
|
|||||||
|
|
||||||
Domyślnie: `http://127.0.0.1:8090`.
|
Domyślnie: `http://127.0.0.1:8090`.
|
||||||
|
|
||||||
|
## Uruchomienie produkcyjne
|
||||||
|
|
||||||
|
Preferowany wariant bez deweloperskiego Werkzeug:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
. venv/bin/activate
|
||||||
|
gunicorn --worker-class gthread --workers 1 --threads 32 --bind 0.0.0.0:8090 --access-logfile - --error-logfile - wsgi:app
|
||||||
|
```
|
||||||
|
|
||||||
|
Note: aplikacja zostaje przy `async_mode="threading"`, więc WebSocket, `start_background_task`, kolejka operacji i poller działają w tym samym modelu co wcześniej.
|
||||||
|
|
||||||
|
Alternatywy przeanalizowane, ale nie wdrożone:
|
||||||
|
|
||||||
|
- `eventlet` przez Gunicorn: działa z Flask-SocketIO, ale wymaga green threads i monkey-patching; większe ryzyko regresji dla operacji plikowych/SCGI.
|
||||||
|
- `gevent` przez Gunicorn: dobry wariant produkcyjny, ale wymaga dodatkowych zależności i testów zgodności.
|
||||||
|
- wiele workerów Gunicorn: wymaga Redis/RabbitMQ/Kafka jako message queue dla Socket.IO, więc nie jest zamiennikiem 1:1.
|
||||||
|
|
||||||
## Profil SCGI
|
## Profil SCGI
|
||||||
|
|
||||||
Przykład:
|
Przykład:
|
||||||
|
|||||||
11
app.py
11
app.py
@@ -1,7 +1,14 @@
|
|||||||
from pytorrent import create_app, socketio
|
from pytorrent import create_app, socketio
|
||||||
from pytorrent.config import HOST, PORT, DEBUG
|
from pytorrent.config import ALLOW_UNSAFE_WERKZEUG, DEBUG, HOST, PORT
|
||||||
|
|
||||||
app = create_app()
|
app = create_app()
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
socketio.run(app, host=HOST, port=PORT, debug=DEBUG, allow_unsafe_werkzeug=True)
|
# Note: This entrypoint is kept for local development; production should use gunicorn via wsgi:app.
|
||||||
|
socketio.run(
|
||||||
|
app,
|
||||||
|
host=HOST,
|
||||||
|
port=PORT,
|
||||||
|
debug=DEBUG,
|
||||||
|
allow_unsafe_werkzeug=ALLOW_UNSAFE_WERKZEUG,
|
||||||
|
)
|
||||||
|
|||||||
@@ -8,22 +8,20 @@ Wants=network-online.target
|
|||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
Type=simple
|
Type=simple
|
||||||
#User=root
|
|
||||||
#Group=root
|
|
||||||
User=pytorrent
|
User=pytorrent
|
||||||
Group=pytorrent
|
Group=pytorrent
|
||||||
WorkingDirectory=/opt/pyTorrent
|
WorkingDirectory=/opt/pyTorrent
|
||||||
Environment="PYTHONUNBUFFERED=1"
|
Environment="PYTHONUNBUFFERED=1"
|
||||||
EnvironmentFile=/opt/pyTorrent/.env
|
EnvironmentFile=/opt/pyTorrent/.env
|
||||||
ExecStart=/opt/pyTorrent/venv/bin/python /opt/pyTorrent/app.py
|
# Note: production starts through Gunicorn, so Socket.IO keeps WebSocket/thread support without unsafe Werkzeug.
|
||||||
|
ExecStart=/opt/pyTorrent/venv/bin/gunicorn --worker-class gthread --workers 1 --threads 32 --bind ${PYTORRENT_HOST}:${PYTORRENT_PORT} --access-logfile - --error-logfile - wsgi:app
|
||||||
Restart=always
|
Restart=always
|
||||||
RestartSec=3
|
RestartSec=3
|
||||||
KillSignal=SIGINT
|
KillSignal=SIGINT
|
||||||
TimeoutStopSec=20
|
TimeoutStopSec=20
|
||||||
|
|
||||||
# opcjonalnie
|
|
||||||
NoNewPrivileges=true
|
NoNewPrivileges=true
|
||||||
PrivateTmp=true
|
PrivateTmp=true
|
||||||
|
|
||||||
[Install]
|
[Install]
|
||||||
WantedBy=multi-user.target
|
WantedBy=multi-user.target
|
||||||
|
|||||||
@@ -7,6 +7,14 @@ from dotenv import load_dotenv
|
|||||||
BASE_DIR = Path(__file__).resolve().parent.parent
|
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||||
load_dotenv(BASE_DIR / ".env")
|
load_dotenv(BASE_DIR / ".env")
|
||||||
|
|
||||||
|
|
||||||
|
def _env_bool(name: str, default: bool = False) -> bool:
|
||||||
|
value = os.getenv(name)
|
||||||
|
if value is None:
|
||||||
|
return default
|
||||||
|
return value.strip().lower() in {"1", "true", "yes", "on"}
|
||||||
|
|
||||||
|
|
||||||
SECRET_KEY = os.getenv("PYTORRENT_SECRET_KEY", "dev-change-me")
|
SECRET_KEY = os.getenv("PYTORRENT_SECRET_KEY", "dev-change-me")
|
||||||
DB_PATH = Path(os.getenv("PYTORRENT_DB_PATH", str(BASE_DIR / "data" / "pytorrent.sqlite3")))
|
DB_PATH = Path(os.getenv("PYTORRENT_DB_PATH", str(BASE_DIR / "data" / "pytorrent.sqlite3")))
|
||||||
if not DB_PATH.is_absolute():
|
if not DB_PATH.is_absolute():
|
||||||
@@ -14,7 +22,9 @@ if not DB_PATH.is_absolute():
|
|||||||
|
|
||||||
HOST = os.getenv("PYTORRENT_HOST", "0.0.0.0")
|
HOST = os.getenv("PYTORRENT_HOST", "0.0.0.0")
|
||||||
PORT = int(os.getenv("PYTORRENT_PORT", "8090"))
|
PORT = int(os.getenv("PYTORRENT_PORT", "8090"))
|
||||||
DEBUG = os.getenv("PYTORRENT_DEBUG", "0") == "1"
|
DEBUG = _env_bool("PYTORRENT_DEBUG", False)
|
||||||
|
# Note: Keep Werkzeug opt-in only for explicit local/dev use, never by default in services.
|
||||||
|
ALLOW_UNSAFE_WERKZEUG = _env_bool("PYTORRENT_ALLOW_UNSAFE_WERKZEUG", DEBUG)
|
||||||
POLL_INTERVAL = float(os.getenv("PYTORRENT_POLL_INTERVAL", "1.0"))
|
POLL_INTERVAL = float(os.getenv("PYTORRENT_POLL_INTERVAL", "1.0"))
|
||||||
WORKERS = int(os.getenv("PYTORRENT_WORKERS", "16"))
|
WORKERS = int(os.getenv("PYTORRENT_WORKERS", "16"))
|
||||||
GEOIP_DB = Path(os.getenv("PYTORRENT_GEOIP_DB", str(BASE_DIR / "data" / "GeoLite2-City.mmdb")))
|
GEOIP_DB = Path(os.getenv("PYTORRENT_GEOIP_DB", str(BASE_DIR / "data" / "GeoLite2-City.mmdb")))
|
||||||
|
|||||||
@@ -180,7 +180,13 @@ body {
|
|||||||
opacity: .78;
|
opacity: .78;
|
||||||
}
|
}
|
||||||
.shortcut { font-size: .78rem; color: var(--bs-secondary-color); padding: .15rem .5rem; }
|
.shortcut { font-size: .78rem; color: var(--bs-secondary-color); padding: .15rem .5rem; }
|
||||||
.content { min-width: 0; min-height: 0; display: grid; grid-template-rows: 1fr 255px; }
|
.content {
|
||||||
|
min-width: 0;
|
||||||
|
min-height: 0;
|
||||||
|
display: grid;
|
||||||
|
grid-template-rows: minmax(0, 1fr) 255px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
.table-wrap { overflow: auto; contain: content; }
|
.table-wrap { overflow: auto; contain: content; }
|
||||||
.torrent-table { margin: 0; white-space: nowrap; table-layout: auto; }
|
.torrent-table { margin: 0; white-space: nowrap; table-layout: auto; }
|
||||||
.torrent-table thead th { position: sticky; top: 0; z-index: 2; background: var(--bs-tertiary-bg); border-bottom: 1px solid var(--bs-border-color); user-select: none; }
|
.torrent-table thead th { position: sticky; top: 0; z-index: 2; background: var(--bs-tertiary-bg); border-bottom: 1px solid var(--bs-border-color); user-select: none; }
|
||||||
@@ -195,7 +201,13 @@ body {
|
|||||||
.virtual-spacer td { padding: 0 !important; border: 0 !important; }
|
.virtual-spacer td { padding: 0 !important; border: 0 !important; }
|
||||||
.empty { height: 120px; text-align: center; vertical-align: middle; color: var(--bs-secondary-color); }
|
.empty { height: 120px; text-align: center; vertical-align: middle; color: var(--bs-secondary-color); }
|
||||||
.progress.thin { height: 7px; min-width: 130px; margin-bottom: 1px; background: rgba(255,255,255,.08); }
|
.progress.thin { height: 7px; min-width: 130px; margin-bottom: 1px; background: rgba(255,255,255,.08); }
|
||||||
.details { min-height: 0; overflow: hidden; background: rgba(var(--bs-secondary-bg-rgb), .78); }
|
.details {
|
||||||
|
grid-row: 2;
|
||||||
|
grid-column: 1;
|
||||||
|
min-height: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
background: rgba(var(--bs-secondary-bg-rgb), .78);
|
||||||
|
}
|
||||||
.detail-pane { height: 210px; overflow: auto; padding: .65rem; }
|
.detail-pane { height: 210px; overflow: auto; padding: .65rem; }
|
||||||
.loading-line { display: flex; align-items: center; gap: .5rem; color: var(--bs-secondary-color); padding: .75rem; }
|
.loading-line { display: flex; align-items: center; gap: .5rem; color: var(--bs-secondary-color); padding: .75rem; }
|
||||||
.muted-pane { color: var(--bs-secondary-color); }
|
.muted-pane { color: var(--bs-secondary-color); }
|
||||||
@@ -373,10 +385,31 @@ body {
|
|||||||
background: rgba(var(--bs-secondary-bg-rgb), .85);
|
background: rgba(var(--bs-secondary-bg-rgb), .85);
|
||||||
}
|
}
|
||||||
.badge-degraded { background: #f59e0b !important; color: #111 !important; }
|
.badge-degraded { background: #f59e0b !important; color: #111 !important; }
|
||||||
body.mobile-mode .table-wrap { display: none !important; }
|
/* Note: Manual mobile mode is defined once here; media queries below only adapt breakpoints. */
|
||||||
body.mobile-mode #mobileList { display: block !important; }
|
body.mobile-mode .table-wrap,
|
||||||
body.mobile-mode .content { grid-template-rows: 1fr 210px; }
|
body.mobile-mode .details {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
body.mobile-mode #mobileList {
|
||||||
|
display: block !important;
|
||||||
|
min-height: 0;
|
||||||
|
height: 100%;
|
||||||
|
overflow: auto;
|
||||||
|
position: relative;
|
||||||
|
z-index: 2;
|
||||||
|
padding-bottom: 1rem;
|
||||||
|
}
|
||||||
|
body.mobile-mode .content {
|
||||||
|
display: grid !important;
|
||||||
|
grid-template-rows: minmax(0, 1fr) !important;
|
||||||
|
min-height: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
body.mobile-mode .torrent-table { display: none; }
|
body.mobile-mode .torrent-table { display: none; }
|
||||||
|
body.mobile-mode .main-grid {
|
||||||
|
min-height: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
@media (max-width: 640px) {
|
@media (max-width: 640px) {
|
||||||
.nav-btn span { display: none; }
|
.nav-btn span { display: none; }
|
||||||
}
|
}
|
||||||
@@ -437,7 +470,6 @@ body.mobile-mode .torrent-table { display: none; }
|
|||||||
opacity: .72;
|
opacity: .72;
|
||||||
}
|
}
|
||||||
.path-row::before{content:'\f07b';font-family:'Font Awesome 6 Free';font-weight:900;color:var(--bs-warning)}
|
.path-row::before{content:'\f07b';font-family:'Font Awesome 6 Free';font-weight:900;color:var(--bs-warning)}
|
||||||
body.mobile-mode #mobileList{min-height:0;height:100%;overflow:auto;display:block!important}
|
|
||||||
body.mobile-mode .mobile-card{display:block}.mobile-card .mobile-actions button{min-width:34px}
|
body.mobile-mode .mobile-card{display:block}.mobile-card .mobile-actions button{min-width:34px}
|
||||||
#toolSmart .form-label{font-size:.75rem;color:var(--bs-secondary-color);margin-bottom:.2rem}
|
#toolSmart .form-label{font-size:.75rem;color:var(--bs-secondary-color);margin-bottom:.2rem}
|
||||||
#toolSmart .btn{padding:.25rem .55rem;border-radius:.5rem;white-space:nowrap}
|
#toolSmart .btn{padding:.25rem .55rem;border-radius:.5rem;white-space:nowrap}
|
||||||
@@ -462,10 +494,6 @@ body.mobile-mode .mobile-card{display:block}.mobile-card .mobile-actions button{
|
|||||||
.empty-mini{padding:.7rem .8rem;border:1px dashed var(--bs-border-color);border-radius:.7rem;color:var(--bs-secondary-color);background:rgba(var(--bs-secondary-bg-rgb),.35)}
|
.empty-mini{padding:.7rem .8rem;border:1px dashed var(--bs-border-color);border-radius:.7rem;color:var(--bs-secondary-color);background:rgba(var(--bs-secondary-bg-rgb),.35)}
|
||||||
.label-manager-row{display:flex;align-items:center;justify-content:space-between;gap:.5rem;border:1px solid var(--bs-border-color);border-radius:.65rem;padding:.4rem .5rem;margin-bottom:.4rem;background:rgba(var(--bs-secondary-bg-rgb),.35)}
|
.label-manager-row{display:flex;align-items:center;justify-content:space-between;gap:.5rem;border:1px solid var(--bs-border-color);border-radius:.65rem;padding:.4rem .5rem;margin-bottom:.4rem;background:rgba(var(--bs-secondary-bg-rgb),.35)}
|
||||||
.tool-tab i{margin-right:.25rem;opacity:.82}
|
.tool-tab i{margin-right:.25rem;opacity:.82}
|
||||||
body.mobile-mode .content{display:grid!important;grid-template-rows:minmax(0,1fr)!important;min-height:0;overflow:hidden}
|
|
||||||
body.mobile-mode .details{display:none!important}
|
|
||||||
body.mobile-mode #mobileList{display:block!important;height:100%!important;min-height:220px;overflow:auto;position:relative;z-index:2;padding-bottom:1rem}
|
|
||||||
body.mobile-mode .main-grid{min-height:0;overflow:hidden}
|
|
||||||
@media (max-width:640px){.history-card{padding:.5rem}#trafficHistoryChart,#trafficSpeedChart{height:320px}.statusbar{font-size:.75rem;gap:.6rem}.mobile-list{padding:.45rem}.mobile-card{margin-bottom:.45rem}}
|
@media (max-width:640px){.history-card{padding:.5rem}#trafficHistoryChart,#trafficSpeedChart{height:320px}.statusbar{font-size:.75rem;gap:.6rem}.mobile-list{padding:.45rem}.mobile-card{margin-bottom:.45rem}}
|
||||||
|
|
||||||
/* Requested fixes: clean progress, mobile auto list, pagers, rTorrent config, peers refresh */
|
/* Requested fixes: clean progress, mobile auto list, pagers, rTorrent config, peers refresh */
|
||||||
@@ -473,7 +501,6 @@ body.mobile-mode .main-grid{min-height:0;overflow:hidden}
|
|||||||
.torrent-progress .progress-bar{min-width:0!important;position:relative;transition:width .25s ease,background-color .25s ease}
|
.torrent-progress .progress-bar{min-width:0!important;position:relative;transition:width .25s ease,background-color .25s ease}
|
||||||
.torrent-progress>span{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;font-size:10px;font-weight:700;line-height:1;color:var(--bs-body-color);text-shadow:none;white-space:nowrap;pointer-events:none}
|
.torrent-progress>span{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;font-size:10px;font-weight:700;line-height:1;color:var(--bs-body-color);text-shadow:none;white-space:nowrap;pointer-events:none}
|
||||||
.torrent-progress .progress-bar+span{color:var(--bs-body-color)}
|
.torrent-progress .progress-bar+span{color:var(--bs-body-color)}
|
||||||
body.mobile-mode #mobileList{display:block!important}
|
|
||||||
@media (max-width:700px){
|
@media (max-width:700px){
|
||||||
body:not(.desktop-mode) .table-wrap{display:none!important}
|
body:not(.desktop-mode) .table-wrap{display:none!important}
|
||||||
body:not(.desktop-mode) #mobileList{display:block!important;min-height:260px;height:100%;overflow:auto}
|
body:not(.desktop-mode) #mobileList{display:block!important;min-height:260px;height:100%;overflow:auto}
|
||||||
@@ -560,14 +587,9 @@ body.mobile-mode #mobileList{display:block!important}
|
|||||||
padding: .75rem;
|
padding: .75rem;
|
||||||
background: var(--bs-tertiary-bg);
|
background: var(--bs-tertiary-bg);
|
||||||
}
|
}
|
||||||
/* Stable main layout: bulk actions overlay the list area, details stay pinned at the bottom. */
|
/* Note: Bulk actions overlay the list area; base .content/.details rules keep the layout pinned. */
|
||||||
.content {
|
|
||||||
position: relative;
|
|
||||||
grid-template-rows: minmax(0, 1fr) 255px !important;
|
|
||||||
}
|
|
||||||
#bulkBar { grid-row: 1; grid-column: 1; align-self: start; }
|
#bulkBar { grid-row: 1; grid-column: 1; align-self: start; }
|
||||||
#tableWrap, #mobileList { grid-row: 1; grid-column: 1; min-height: 0; }
|
#tableWrap, #mobileList { grid-row: 1; grid-column: 1; min-height: 0; }
|
||||||
.details { grid-row: 2; grid-column: 1; min-height: 0; }
|
|
||||||
.bulk-bar:not(.d-none) + .table-wrap { padding-top: 38px; }
|
.bulk-bar:not(.d-none) + .table-wrap { padding-top: 38px; }
|
||||||
@media (max-width: 900px) {
|
@media (max-width: 900px) {
|
||||||
.bulk-bar { gap: .3rem; }
|
.bulk-bar { gap: .3rem; }
|
||||||
|
|||||||
@@ -4,3 +4,4 @@ python-dotenv>=1.0
|
|||||||
geoip2>=4.8
|
geoip2>=4.8
|
||||||
psutil>=5.9
|
psutil>=5.9
|
||||||
simple-websocket>=1.0
|
simple-websocket>=1.0
|
||||||
|
gunicorn>=22.0
|
||||||
|
|||||||
@@ -1,16 +1,23 @@
|
|||||||
[Unit]
|
[Unit]
|
||||||
Description=pyTorrent web UI for rTorrent
|
Description=pyTorrent web UI for rTorrent
|
||||||
After=network.target
|
After=network-online.target
|
||||||
|
Wants=network-online.target
|
||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
Type=simple
|
Type=simple
|
||||||
WorkingDirectory=/opt/pytorrent
|
WorkingDirectory=/opt/pytorrent
|
||||||
|
Environment="PYTHONUNBUFFERED=1"
|
||||||
EnvironmentFile=/opt/pytorrent/.env
|
EnvironmentFile=/opt/pytorrent/.env
|
||||||
ExecStart=/opt/pytorrent/venv/bin/python /opt/pytorrent/app.py
|
# Note: threaded Gunicorn preserves Flask-SocketIO background tasks without running Werkzeug in production.
|
||||||
|
ExecStart=/opt/pytorrent/venv/bin/gunicorn --worker-class gthread --workers 1 --threads 32 --bind ${PYTORRENT_HOST}:${PYTORRENT_PORT} --access-logfile - --error-logfile - wsgi:app
|
||||||
Restart=always
|
Restart=always
|
||||||
RestartSec=3
|
RestartSec=3
|
||||||
|
KillSignal=SIGINT
|
||||||
|
TimeoutStopSec=20
|
||||||
User=www-data
|
User=www-data
|
||||||
Group=www-data
|
Group=www-data
|
||||||
|
NoNewPrivileges=true
|
||||||
|
PrivateTmp=true
|
||||||
|
|
||||||
[Install]
|
[Install]
|
||||||
WantedBy=multi-user.target
|
WantedBy=multi-user.target
|
||||||
|
|||||||
Reference in New Issue
Block a user