diff --git a/app.py b/app.py index c857c54..6650140 100644 --- a/app.py +++ b/app.py @@ -1,5 +1,5 @@  -from flask import Flask, render_template, request, redirect, url_for, flash, Response, jsonify, session, send_from_directory +from flask import Flask, render_template, request, redirect, url_for, flash, Response, jsonify, session, send_from_directory, send_file try: import psycopg except ModuleNotFoundError: @@ -16,6 +16,13 @@ import re import os import json import sys +from database_maintenance import ( + DatabaseMaintenanceBusy, + DatabaseMaintenanceError, + DatabaseMaintenanceManager, + InvalidBackupFile, + PostgreSqlToolNotFound, +) from handlers import computers as computers_handlers from handlers import projectors as projectors_handlers from routes.projectors import _normalize_board_fields @@ -79,6 +86,7 @@ APP_PORT = _integer_setting(APPLICATION_SETTINGS, "port", 5050) app = Flask(__name__) app.secret_key = "secret" app.config["DB_INITIALIZED"] = False +database_maintenance = DatabaseMaintenanceManager(DB_CONFIG) CLIENT_AGENT_TOKEN = "change-me-client-token" CLIENT_TEST_OFFLINE_AFTER_SECONDS = 35 @@ -2640,6 +2648,100 @@ def logout(): return redirect(url_for("login")) +def database_admin_api(view): + @wraps(view) + def wrapper(*args, **kwargs): + if "user" not in session: + return jsonify(ok=False, error="Требуется авторизация."), 401 + if session.get("role") != "admin": + return jsonify(ok=False, error="Требуются права администратора."), 403 + return view(*args, **kwargs) + + return wrapper + + +def _database_size_bytes(): + conn = get_conn() + try: + cur = conn.cursor() + cur.execute("SELECT pg_database_size(current_database())") + row = cur.fetchone() + return int(row[0] or 0) if row else 0 + finally: + conn.close() + + +@app.route("/api/database/backup/start", methods=["POST"]) +@database_admin_api +def database_backup_start(): + try: + try: + database_size = _database_size_bytes() + except Exception: + database_size = 0 + job_id = database_maintenance.start_backup( + session.get("user"), + estimated_database_size=database_size, + ) + return jsonify(ok=True, job_id=job_id), 202 + except (DatabaseMaintenanceBusy, PostgreSqlToolNotFound) as exc: + return jsonify(ok=False, error=str(exc)), 409 + except DatabaseMaintenanceError as exc: + return jsonify(ok=False, error=str(exc)), 400 + except Exception: + app.logger.exception("Failed to start a database backup") + return jsonify(ok=False, error="Не удалось запустить резервное копирование."), 500 + + +@app.route("/api/database/restore/start", methods=["POST"]) +@database_admin_api +def database_restore_start(): + uploaded_file = request.files.get("backup_file") + if not uploaded_file or not uploaded_file.filename: + return jsonify(ok=False, error="Выберите файл резервной копии."), 400 + try: + job_id = database_maintenance.start_restore( + session.get("user"), + uploaded_file, + uploaded_file.filename, + ) + return jsonify(ok=True, job_id=job_id), 202 + except DatabaseMaintenanceBusy as exc: + return jsonify(ok=False, error=str(exc)), 409 + except (InvalidBackupFile, PostgreSqlToolNotFound, DatabaseMaintenanceError) as exc: + return jsonify(ok=False, error=str(exc)), 400 + except Exception: + app.logger.exception("Failed to start a database restore") + return jsonify(ok=False, error="Не удалось запустить восстановление базы данных."), 500 + + +@app.route("/api/database/jobs/") +@database_admin_api +def database_job_status(job_id): + job = database_maintenance.get_job(job_id, session.get("user")) + if not job: + return jsonify(ok=False, error="Операция не найдена."), 404 + if job.get("kind") == "restore" and job.get("status") == "success": + app.config["DB_INITIALIZED"] = False + return jsonify(ok=True, job=job) + + +@app.route("/api/database/backup//download") +@database_admin_api +def database_backup_download(job_id): + backup = database_maintenance.get_backup_download(job_id, session.get("user")) + if not backup: + return jsonify(ok=False, error="Файл резервной копии не найден."), 404 + path, filename = backup + return send_file( + path, + mimetype="application/octet-stream", + as_attachment=True, + download_name=filename, + conditional=True, + ) + + @app.route("/network") @login_required def network():