from __future__ import annotations import os import time import shutil import zipfile import logging from pathlib import Path from flask import Blueprint, render_template, request, redirect, url_for, flash, jsonify, session, send_from_directory, send_file from flask_login import login_required, current_user from concurrent.futures import ThreadPoolExecutor from watchdog.observers import Observer from natsort import natsorted from backend.services.ip_processor import ( save_ip_addresses, process_ips_concurrently, get_progress, on_complete, ) from backend.services.watchdog_handler import FileCreatedHandler from config import Config main_bp = Blueprint("main", __name__) executor = ThreadPoolExecutor(max_workers=Config.MAX_WORKERS) def register_main_routes(app, socketio): app.register_blueprint(main_bp) @app.context_processor def inject_user(): return dict(current_user=current_user) @app.before_request def make_session_permanent(): session.permanent = True if current_user.is_authenticated: session.modified = True @main_bp.route("/") @main_bp.route("/index", methods=["GET"]) @login_required def index(): script_dir = Path(Config.SCRIPT_FOLDER) xml_dir = Path(Config.XML_FOLDER) info_dir = Path(Config.IDRAC_INFO_FOLDER) backup_dir = Path(Config.BACKUP_FOLDER) # 1. 스크립트 목록 조회 및 카테고리 분류 all_scripts = [f.name for f in script_dir.glob("*") if f.is_file() and f.name != ".env"] all_scripts = natsorted(all_scripts) grouped_scripts = {} for script in all_scripts: upper = script.upper() category = "General" if upper.startswith("GPU"): category = "GPU" elif upper.startswith("LOM"): category = "LOM" elif upper.startswith("TYPE") or upper.startswith("XE"): category = "Server Models" elif "MAC" in upper: category = "MAC Info" elif "GUID" in upper: category = "GUID Info" elif "SET_" in upper or "CONFIG" in upper: category = "Configuration" if category not in grouped_scripts: grouped_scripts[category] = [] grouped_scripts[category].append(script) # 카테고리 정렬 (General은 마지막에) sorted_categories = sorted(grouped_scripts.keys()) if "General" in sorted_categories: sorted_categories.remove("General") sorted_categories.append("General") grouped_scripts_sorted = {k: grouped_scripts[k] for k in sorted_categories} # 2. XML 파일 목록 xml_files = [f.name for f in xml_dir.glob("*.xml")] # 3. 페이지네이션 및 파일 목록 page = int(request.args.get("page", 1)) info_files = [f.name for f in info_dir.glob("*") if f.is_file()] info_files = natsorted(info_files) start = (page - 1) * Config.FILES_PER_PAGE end = start + Config.FILES_PER_PAGE files_to_display = [{"name": Path(f).stem, "file": f} for f in info_files[start:end]] total_pages = (len(info_files) + Config.FILES_PER_PAGE - 1) // Config.FILES_PER_PAGE start_page = ((page - 1) // 10) * 10 + 1 end_page = min(start_page + 9, total_pages) # 4. 백업 폴더 목록 backup_dirs = [d for d in backup_dir.iterdir() if d.is_dir()] backup_dirs.sort(key=lambda p: p.stat().st_mtime, reverse=True) backup_page = int(request.args.get("backup_page", 1)) start_b = (backup_page - 1) * Config.BACKUP_FILES_PER_PAGE end_b = start_b + Config.BACKUP_FILES_PER_PAGE backup_slice = backup_dirs[start_b:end_b] total_backup_pages = (len(backup_dirs) + Config.BACKUP_FILES_PER_PAGE - 1) // Config.BACKUP_FILES_PER_PAGE backup_files = {} for d in backup_slice: files = [f.name for f in d.iterdir() if f.is_file()] backup_files[d.name] = {"files": files, "count": len(files)} return render_template( "index.html", files_to_display=files_to_display, page=page, total_pages=total_pages, start_page=start_page, end_page=end_page, backup_files=backup_files, total_backup_pages=total_backup_pages, backup_page=backup_page, scripts=all_scripts, # 기존 리스트 호환 grouped_scripts=grouped_scripts_sorted, # 카테고리별 분류 xml_files=xml_files, ) @main_bp.route("/process_ips", methods=["POST"]) @login_required def process_ips(): ips = request.form.get("ips") selected_script = request.form.get("script") selected_xml_file = request.form.get("xmlFile") if not ips or not selected_script: return jsonify({"error": "IP 주소와 스크립트를 모두 입력하세요."}), 400 xml_file_path = None if selected_script == "02-set_config.py" and selected_xml_file: xml_path = Path(Config.XML_FOLDER) / selected_xml_file if not xml_path.exists(): return jsonify({"error": "선택한 XML 파일이 존재하지 않습니다."}), 400 xml_file_path = str(xml_path) job_id = str(time.time()) session["job_id"] = job_id ip_files = save_ip_addresses(ips, Config.UPLOAD_FOLDER) total_files = len(ip_files) handler = FileCreatedHandler(job_id, total_files) observer = Observer() observer.schedule(handler, Config.IDRAC_INFO_FOLDER, recursive=False) observer.start() future = executor.submit( process_ips_concurrently, ip_files, job_id, observer, selected_script, xml_file_path ) future.add_done_callback(lambda x: on_complete(job_id)) logging.info(f"[AJAX] 작업 시작: {job_id}, script: {selected_script}") return jsonify({"job_id": job_id}) @main_bp.route("/progress_status/") @login_required def progress_status(job_id: str): return jsonify({"progress": get_progress(job_id)}) @main_bp.route("/backup", methods=["POST"]) @login_required def backup_files(): prefix = request.form.get("backup_prefix", "") if not prefix.startswith("PO"): flash("Backup 이름은 PO로 시작해야 합니다.") return redirect(url_for("main.index")) folder_name = f"{prefix}_{time.strftime('%Y%m%d')}" backup_path = Path(Config.BACKUP_FOLDER) / folder_name backup_path.mkdir(parents=True, exist_ok=True) info_dir = Path(Config.IDRAC_INFO_FOLDER) for file in info_dir.iterdir(): if file.is_file(): shutil.move(str(file), str(backup_path / file.name)) flash("백업 완료되었습니다.") logging.info(f"백업 완료: {folder_name}") return redirect(url_for("main.index")) @main_bp.route("/download/") @login_required def download_file(filename: str): # send_from_directory는 내부적으로 안전 검사를 수행 return send_from_directory(Config.IDRAC_INFO_FOLDER, filename, as_attachment=True) @main_bp.route("/delete/", methods=["POST"]) @login_required def delete_file(filename: str): file_path = Path(Config.IDRAC_INFO_FOLDER) / filename if file_path.exists(): try: file_path.unlink() flash(f"{filename} 삭제됨.") logging.info(f"파일 삭제됨: {filename}") except Exception as e: logging.error(f"파일 삭제 오류: {e}") flash("파일 삭제 중 오류가 발생했습니다.", "danger") else: flash("파일이 존재하지 않습니다.") return redirect(url_for("main.index")) @main_bp.route("/download_zip", methods=["POST"]) @login_required def download_zip(): zip_filename = request.form.get("zip_filename", "export") zip_path = Path(Config.TEMP_ZIP_FOLDER) / f"{zip_filename}.zip" with zipfile.ZipFile(zip_path, "w", compression=zipfile.ZIP_DEFLATED) as zipf: for file in Path(Config.IDRAC_INFO_FOLDER).glob("*"): if file.is_file(): zipf.write(file, arcname=file.name) try: response = send_file(str(zip_path), as_attachment=True) return response finally: # 응답 후 임시 ZIP 삭제 try: if zip_path.exists(): zip_path.unlink() except Exception as e: logging.warning(f"임시 ZIP 삭제 실패: {e}") @main_bp.route("/download_backup//") @login_required def download_backup_file(date: str, filename: str): backup_path = Path(Config.BACKUP_FOLDER) / date return send_from_directory(str(backup_path), filename, as_attachment=True)