136 lines
4.7 KiB
Python
136 lines
4.7 KiB
Python
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
from flask import Flask, jsonify, render_template, request, url_for
|
|
from flask_socketio import SocketIO
|
|
from werkzeug.middleware.proxy_fix import ProxyFix
|
|
from .config import (
|
|
SECRET_KEY,
|
|
SESSION_COOKIE_SECURE,
|
|
PROXY_FIX_ENABLE,
|
|
PROXY_FIX_X_FOR,
|
|
PROXY_FIX_X_PROTO,
|
|
PROXY_FIX_X_HOST,
|
|
PROXY_FIX_X_PORT,
|
|
PROXY_FIX_X_PREFIX,
|
|
SOCKETIO_CORS_ALLOWED_ORIGINS,
|
|
)
|
|
from .db import init_db
|
|
from .services.frontend_assets import asset_path, bootstrap_css_path, validate_offline_assets
|
|
from .utils import file_md5
|
|
|
|
socketio = SocketIO(cors_allowed_origins=SOCKETIO_CORS_ALLOWED_ORIGINS, ping_timeout=30, async_mode="threading")
|
|
_static_md5_cache: dict[tuple, str] = {}
|
|
|
|
|
|
def _wants_json_response() -> bool:
|
|
"""Return true for API/error clients that should not receive an HTML page."""
|
|
best = request.accept_mimetypes.best_match(["application/json", "text/html"])
|
|
return request.path.startswith("/api/") or best == "application/json"
|
|
|
|
|
|
def register_error_pages(app: Flask) -> None:
|
|
# Notatka: własne strony błędów zastępują generyczne 404/500 i zachowują JSON dla API.
|
|
@app.errorhandler(404)
|
|
def not_found(error):
|
|
if _wants_json_response():
|
|
return jsonify({"ok": False, "error": "Not found"}), 404
|
|
return render_template(
|
|
"error.html",
|
|
code=404,
|
|
title="Page not found",
|
|
message="The requested pyTorrent view does not exist or is not available.",
|
|
icon="fa-compass-drafting",
|
|
), 404
|
|
|
|
@app.errorhandler(500)
|
|
def server_error(error):
|
|
if _wants_json_response():
|
|
return jsonify({"ok": False, "error": "Internal server error"}), 500
|
|
return render_template(
|
|
"error.html",
|
|
code=500,
|
|
title="Application error",
|
|
message="pyTorrent hit an internal error while handling this request.",
|
|
icon="fa-bug",
|
|
), 500
|
|
|
|
|
|
def create_app() -> Flask:
|
|
validate_offline_assets()
|
|
app = Flask(__name__)
|
|
if PROXY_FIX_ENABLE:
|
|
app.wsgi_app = ProxyFix(
|
|
app.wsgi_app,
|
|
x_for=PROXY_FIX_X_FOR,
|
|
x_proto=PROXY_FIX_X_PROTO,
|
|
x_host=PROXY_FIX_X_HOST,
|
|
x_port=PROXY_FIX_X_PORT,
|
|
x_prefix=PROXY_FIX_X_PREFIX,
|
|
)
|
|
app.secret_key = SECRET_KEY
|
|
app.config.update(
|
|
SESSION_COOKIE_HTTPONLY=True,
|
|
SESSION_COOKIE_SAMESITE="Lax",
|
|
SESSION_COOKIE_SECURE=SESSION_COOKIE_SECURE,
|
|
)
|
|
|
|
@app.context_processor
|
|
def static_helpers():
|
|
def static_url(filename: str) -> str:
|
|
path = Path(app.static_folder or "") / filename
|
|
try:
|
|
stat = path.stat()
|
|
key = (filename, stat.st_mtime_ns, stat.st_size)
|
|
version = _static_md5_cache.get(key)
|
|
if not version:
|
|
_static_md5_cache.clear()
|
|
version = file_md5(path)
|
|
_static_md5_cache[key] = version
|
|
return url_for("static", filename=filename, v=version)
|
|
except OSError:
|
|
return url_for("static", filename=filename)
|
|
|
|
def frontend_asset_url(key: str) -> str:
|
|
# Notatka: helper przełącza szablony między CDN i lokalnymi plikami bez duplikowania logiki.
|
|
path = asset_path(key)
|
|
return path if path.startswith("http") else static_url(path)
|
|
|
|
def bootstrap_theme_url(theme: str | None = None) -> str:
|
|
path = bootstrap_css_path(theme)
|
|
return path if path.startswith("http") else static_url(path)
|
|
|
|
return {
|
|
"static_url": static_url,
|
|
"frontend_asset_url": frontend_asset_url,
|
|
"bootstrap_theme_url": bootstrap_theme_url,
|
|
}
|
|
|
|
@app.after_request
|
|
def cache_headers(response):
|
|
response.headers.pop('Content-Disposition', None)
|
|
|
|
if request.endpoint == "static":
|
|
response.headers["Cache-Control"] = "public, max-age=31536000, immutable"
|
|
else:
|
|
response.headers["Cache-Control"] = "no-store, private"
|
|
return response
|
|
|
|
from .routes.main import bp as main_bp
|
|
from .routes.api import bp as api_bp
|
|
app.register_blueprint(main_bp)
|
|
app.register_blueprint(api_bp)
|
|
register_error_pages(app)
|
|
init_db()
|
|
from .services.auth import install_guards
|
|
install_guards(app)
|
|
|
|
socketio.init_app(app)
|
|
from .services.workers import set_socketio
|
|
set_socketio(socketio)
|
|
from .services.websocket import register_socketio_handlers
|
|
register_socketio_handlers(socketio)
|
|
from .services.startup_config import schedule_startup_config_apply
|
|
schedule_startup_config_apply(socketio)
|
|
return app
|