From acb1e2260f82c4e081aa825aaeb8f743a924de2b Mon Sep 17 00:00:00 2001 From: hzhang Date: Wed, 5 Mar 2025 17:33:17 +0000 Subject: [PATCH] add: load backup --- api/backup.py | 117 ++++++++++++++++++++++++++++++++++++++++-- api/tree.py | 58 +++++++++++++++++++++ app.py | 19 ++++--- db/models/Resource.py | 1 - 4 files changed, 183 insertions(+), 12 deletions(-) create mode 100644 api/tree.py diff --git a/api/backup.py b/api/backup.py index 12ff05d..3d0d55a 100644 --- a/api/backup.py +++ b/api/backup.py @@ -1,17 +1,19 @@ import shutil from datetime import datetime - -from flask import Blueprint, send_file, jsonify +import tempfile +import zipfile +from flask import Blueprint, send_file, jsonify, request import os from api import require_auth from db import get_db from db.models.Markdown import Markdown from db.models.Path import Path - +import threading import logging logger = logging.getLogger(__name__) backup_bp = Blueprint('backup', __name__, url_prefix='/api/backup') +backup_lock = threading.Lock() @backup_bp.route('/', methods=['GET']) @require_auth(roles=['admin']) def get_backup(): @@ -64,3 +66,112 @@ def traverse(path_id, paths): for child in children: traverse(child.id, paths) cd_back() + + +@backup_bp.route('/load', methods=['POST']) +@require_auth(roles=['admin']) +def load_backup(): + if not backup_lock.acquire(blocking=False): + return jsonify({"error": "Another backup restore is in progress. Please try again later."}), 429 + + try: + if 'file' not in request.files: + return jsonify({"error": "No file provided"}), 400 + uploaded_file = request.files['file'] + + temp_dir = tempfile.mkdtemp() + zip_path = os.path.join(temp_dir, "backup.zip") + uploaded_file.save(zip_path) + + with zipfile.ZipFile(zip_path, 'r') as zip_ref: + zip_ref.extractall(temp_dir) + + root_dir = temp_dir + if not os.path.exists(root_dir): + return jsonify({"error": "Invalid backup format"}), 400 + + with get_db() as session: + path_mapping = {} + restore_tree(root_dir, None, session, path_mapping) + session.commit() + + shutil.rmtree(temp_dir) + + return jsonify({"success": True, "message": "Backup restored and merged successfully"}) + + except Exception as e: + logger.error(f"Failed to load backup: {e}") + return jsonify({"error": f"Failed to load backup {e}"}), 500 + finally: + backup_lock.release() + + +def restore_tree(dir_path, parent_id, session, path_mapping): + + dir_name = os.path.basename(dir_path) + + existing_path = session.query(Path).filter_by(parent_id=parent_id, name=dir_name).first() + if parent_id is None: + new_path_id = 1 + elif existing_path: + new_path_id = existing_path.id + else: + order = '' + meta_file_path = os.path.join(dir_path, ".meta") + if os.path.exists(meta_file_path): + with open(meta_file_path, "r") as meta_file: + for line in meta_file: + key, value = line.strip().split(": ", 1) + if key == "order": + order = value + + new_path = Path(name=dir_name, parent_id=parent_id, order=order) + session.add(new_path) + session.flush() + new_path_id = new_path.id + + path_mapping[dir_path] = new_path_id + + for file in os.listdir(dir_path): + file_path = os.path.join(dir_path, file) + if file.endswith(".md"): + md_title = file[:-3] + + mdmeta_path = file_path + "meta" + created_at = datetime.now() + order = '' + shortcut = "" + + if os.path.exists(mdmeta_path): + with open(mdmeta_path, "r") as meta_file: + for line in meta_file: + key, value = line.strip().split(": ", 1) + if key == "created_at": + created_at = datetime.strptime(value, "%Y-%m-%d %H:%M:%S") + elif key == "order": + order = value + elif key == "shortcut": + shortcut = value + + with open(file_path, "r", encoding="utf-8") as md_file: + content = md_file.read() + + unique_title = get_unique_markdown_title(session, md_title, new_path_id) + + new_md = Markdown(title=unique_title, content=content, path_id=new_path_id, + created_at=created_at, order=order, shortcut=shortcut) + session.add(new_md) + + + for item in os.listdir(dir_path): + item_path = os.path.join(dir_path, item) + if os.path.isdir(item_path): + restore_tree(item_path, new_path_id, session, path_mapping) + + +def get_unique_markdown_title(session, title, path_id): + existing_titles = {md.title for md in session.query(Markdown.title).filter_by(path_id=path_id).all()} + unique_title = title + while unique_title in existing_titles: + unique_title += ".bp" + return unique_title \ No newline at end of file diff --git a/api/tree.py b/api/tree.py new file mode 100644 index 0000000..d9b0ab4 --- /dev/null +++ b/api/tree.py @@ -0,0 +1,58 @@ +from flask import Blueprint, request, jsonify +from sqlalchemy.orm import Session + +import api +from api import require_auth, etag_response +from db import get_db +from db.models.Markdown import Markdown +from db.models.Path import Path +from api import limiter +import logging +logger = logging.getLogger(__name__) + +tree_bp = Blueprint('tree', __name__, url_prefix='/api/tree') + + +def build_tree(db: Session, parent_id: int = None): + path_nodes = db.query(Path).filter(Path.parent_id == parent_id).all() + md_nodes = db.query(Markdown.id, Markdown.title, Markdown.order, Markdown.shortcut).filter(Markdown.path_id == parent_id).all() + t0 = [ + { + "type": "markdown", + "id": node.id, + "title": node.title, + "order": node.order, + "shortcut": node.shortcut + } for node in md_nodes + ] + t1 = [ + { + "type": "path", + "id": node.id, + "name": node.name, + "order": node.order, + "children": build_tree(db, node.id) + } for node in path_nodes + ] + for node in t1: + for child in node["children"]: + if "title" in child.keys() and child["title"] == "index": + node["index"] = True + break + return t0 + t1 + +@tree_bp.route('/', methods=['GET']) +@limiter.limit(api.get_rate_limit) +@etag_response +def get_tree(): + with get_db() as session: + children = build_tree(session, 1) + return jsonify( + { + "type": "path", + "id": 1, + "name": "Root", + "index": any("title" in child.keys() and child["title"] == "index" for child in children), + "children": children + } + ), 200 \ No newline at end of file diff --git a/app.py b/app.py index c236826..4810f75 100644 --- a/app.py +++ b/app.py @@ -1,4 +1,3 @@ -# app.py from pprint import pprint from logging_handlers.DatabaseLogHandler import DatabaseLogHandler from api import limiter @@ -26,13 +25,17 @@ except Exception as e: app = Flask(__name__) app.secret_key = env_provider.SESSION_SECRET_KEY -CORS(app, resources={r"/api/*": {"origins": [ - env_provider.KC_HOST, - env_provider.FRONTEND_HOST, - r"https?://localhost:\d+", - r"https?://127\.0\.0\.1:\d+", - r"https?://localhost" -]}}, +CORS(app, resources={ + r"/api/*": { + "origins": [ + env_provider.KC_HOST, + env_provider.FRONTEND_HOST, + r"https?://localhost:\d+", + r"https?://127\.0\.0\.1:\d+", + r"https?://localhost" + ] + } +}, expose_headers=['Content-Disposition'] ) diff --git a/db/models/Resource.py b/db/models/Resource.py index cc63339..82e7037 100644 --- a/db/models/Resource.py +++ b/db/models/Resource.py @@ -1,4 +1,3 @@ -#db/models/Resource.py from sqlalchemy import Column, Text, LargeBinary, String from db.models import Base