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