Initial commit
This commit is contained in:
@@ -0,0 +1,20 @@
|
||||
from __future__ import annotations
|
||||
from flask import Flask
|
||||
from .home import register_home_routes
|
||||
from .auth import register_auth_routes
|
||||
from .admin import register_admin_routes
|
||||
from .main import register_main_routes
|
||||
from .xml import register_xml_routes
|
||||
from .utilities import register_util_routes
|
||||
from .file_view import register_file_view
|
||||
|
||||
|
||||
def register_routes(app: Flask, socketio=None) -> None:
|
||||
"""블루프린트 일괄 등록. socketio는 main 라우트에서만 사용."""
|
||||
register_home_routes(app)
|
||||
register_auth_routes(app)
|
||||
register_admin_routes(app)
|
||||
register_main_routes(app, socketio)
|
||||
register_xml_routes(app)
|
||||
register_util_routes(app)
|
||||
register_file_view(app)
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,126 @@
|
||||
# backend/routes/admin.py
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from functools import wraps
|
||||
from typing import Callable
|
||||
|
||||
from flask import (
|
||||
Blueprint,
|
||||
render_template,
|
||||
redirect,
|
||||
url_for,
|
||||
flash,
|
||||
abort,
|
||||
request,
|
||||
current_app,
|
||||
)
|
||||
from flask_login import login_required, current_user
|
||||
|
||||
from backend.models.user import User, db
|
||||
|
||||
admin_bp = Blueprint("admin", __name__)
|
||||
|
||||
|
||||
# Blueprint 등록
|
||||
def register_admin_routes(app):
|
||||
app.register_blueprint(admin_bp)
|
||||
|
||||
|
||||
# 관리자 권한 데코레이터
|
||||
def admin_required(view_func: Callable):
|
||||
@wraps(view_func)
|
||||
def wrapper(*args, **kwargs):
|
||||
if not current_user.is_authenticated:
|
||||
return redirect(url_for("auth.login"))
|
||||
if not getattr(current_user, "is_admin", False):
|
||||
flash("관리자 권한이 필요합니다.", "danger")
|
||||
return redirect(url_for("main.index"))
|
||||
return view_func(*args, **kwargs)
|
||||
return wrapper
|
||||
|
||||
|
||||
# 관리자 대시보드
|
||||
@admin_bp.route("/admin", methods=["GET"])
|
||||
@login_required
|
||||
@admin_required
|
||||
def admin_panel():
|
||||
users = db.session.query(User).order_by(User.id.asc()).all()
|
||||
return render_template("admin.html", users=users)
|
||||
|
||||
|
||||
# 사용자 승인
|
||||
@admin_bp.route("/admin/approve/<int:user_id>", methods=["GET"])
|
||||
@login_required
|
||||
@admin_required
|
||||
def approve_user(user_id: int):
|
||||
user = db.session.get(User, user_id)
|
||||
if not user:
|
||||
abort(404)
|
||||
user.is_active = True
|
||||
db.session.commit()
|
||||
flash("사용자가 승인되었습니다.", "success")
|
||||
logging.info("✅ 승인된 사용자: %s (id=%s)", user.username, user.id)
|
||||
return redirect(url_for("admin.admin_panel"))
|
||||
|
||||
|
||||
# 사용자 삭제
|
||||
@admin_bp.route("/admin/delete/<int:user_id>", methods=["GET"])
|
||||
@login_required
|
||||
@admin_required
|
||||
def delete_user(user_id: int):
|
||||
user = db.session.get(User, user_id)
|
||||
if not user:
|
||||
abort(404)
|
||||
username = user.username
|
||||
db.session.delete(user)
|
||||
db.session.commit()
|
||||
flash("사용자가 삭제되었습니다.", "success")
|
||||
logging.info("🗑 삭제된 사용자: %s (id=%s)", username, user_id)
|
||||
return redirect(url_for("admin.admin_panel"))
|
||||
|
||||
|
||||
# ▼▼▼ 사용자 비밀번호 변경(관리자용) ▼▼▼
|
||||
@admin_bp.route("/admin/users/<int:user_id>/reset_password", methods=["POST"])
|
||||
@login_required
|
||||
@admin_required
|
||||
def reset_password(user_id: int):
|
||||
"""
|
||||
admin.html에서 각 사용자 행 아래 폼으로부터 POST:
|
||||
- name="new_password"
|
||||
- name="confirm_password"
|
||||
CSRF는 템플릿에서 {{ csrf_token() }} 또는 {{ form.hidden_tag() }}로 포함되어야 합니다.
|
||||
"""
|
||||
new_pw = (request.form.get("new_password") or "").strip()
|
||||
confirm = (request.form.get("confirm_password") or "").strip()
|
||||
|
||||
# 서버측 검증
|
||||
if not new_pw or not confirm:
|
||||
flash("비밀번호와 확인 값을 모두 입력하세요.", "warning")
|
||||
return redirect(url_for("admin.admin_panel"))
|
||||
if new_pw != confirm:
|
||||
flash("비밀번호 확인이 일치하지 않습니다.", "warning")
|
||||
return redirect(url_for("admin.admin_panel"))
|
||||
if len(new_pw) < 8:
|
||||
flash("비밀번호는 최소 8자 이상이어야 합니다.", "warning")
|
||||
return redirect(url_for("admin.admin_panel"))
|
||||
|
||||
user = db.session.get(User, user_id)
|
||||
if not user:
|
||||
abort(404)
|
||||
|
||||
try:
|
||||
# passlib(Argon2id) 기반 set_password 사용 (models.user에 구현됨)
|
||||
user.set_password(new_pw)
|
||||
db.session.commit()
|
||||
flash(f"사용자(ID={user.id}) 비밀번호를 변경했습니다.", "success")
|
||||
current_app.logger.info(
|
||||
"ADMIN: reset password for user_id=%s by admin_id=%s",
|
||||
user.id, current_user.id
|
||||
)
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
current_app.logger.exception("ADMIN: reset password failed: %s", e)
|
||||
flash("비밀번호 변경 중 오류가 발생했습니다.", "danger")
|
||||
|
||||
return redirect(url_for("admin.admin_panel"))
|
||||
@@ -0,0 +1,180 @@
|
||||
# backend/routes/auth.py
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import threading
|
||||
from typing import Optional
|
||||
from urllib.parse import urlparse, urljoin
|
||||
|
||||
from flask import (
|
||||
Blueprint,
|
||||
render_template,
|
||||
redirect,
|
||||
url_for,
|
||||
flash,
|
||||
request,
|
||||
session,
|
||||
current_app,
|
||||
)
|
||||
from flask_login import login_user, logout_user, current_user, login_required
|
||||
|
||||
from backend.forms.auth_forms import RegistrationForm, LoginForm
|
||||
from backend.models.user import User, db
|
||||
|
||||
# ── (선택) Telegram: 미설정이면 조용히 패스
|
||||
try:
|
||||
from telegram import Bot
|
||||
from telegram.constants import ParseMode
|
||||
except Exception: # 라이브러리 미설치/미사용 환경
|
||||
Bot = None
|
||||
ParseMode = None
|
||||
|
||||
auth_bp = Blueprint("auth", __name__)
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
# 유틸
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
def _is_safe_url(target: str) -> bool:
|
||||
"""로그인 후 next 파라미터의 안전성 확인(동일 호스트만 허용)."""
|
||||
ref = urlparse(request.host_url)
|
||||
test = urlparse(urljoin(request.host_url, target))
|
||||
return (test.scheme in ("http", "https")) and (ref.netloc == test.netloc)
|
||||
|
||||
|
||||
def _notify(text: str) -> None:
|
||||
"""텔레그램 알림 (설정 없으면 바로 return)."""
|
||||
token = (current_app.config.get("TELEGRAM_BOT_TOKEN") or "").strip()
|
||||
chat_id = (current_app.config.get("TELEGRAM_CHAT_ID") or "").strip()
|
||||
if not (token and chat_id and Bot and ParseMode):
|
||||
return
|
||||
|
||||
def _send():
|
||||
try:
|
||||
bot = Bot(token=token)
|
||||
bot.send_message(chat_id=chat_id, text=text, parse_mode=ParseMode.HTML)
|
||||
except Exception as e:
|
||||
current_app.logger.warning("Telegram send failed: %s", e)
|
||||
|
||||
threading.Thread(target=_send, daemon=True).start()
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
# Blueprint 등록 훅
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
def register_auth_routes(app):
|
||||
"""app.py에서 register_routes(app, socketio) 호출 시 사용."""
|
||||
app.register_blueprint(auth_bp)
|
||||
|
||||
@app.before_request
|
||||
def _touch_session():
|
||||
# 요청마다 세션 갱신(만료 슬라이딩) + 로그아웃 플래그 정리
|
||||
session.modified = True
|
||||
if current_user.is_authenticated and session.get("just_logged_out"):
|
||||
session.pop("just_logged_out", None)
|
||||
flash("세션이 만료되어 자동 로그아웃 되었습니다.", "info")
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
# 회원가입
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
@auth_bp.route("/register", methods=["GET", "POST"])
|
||||
def register():
|
||||
if current_user.is_authenticated:
|
||||
current_app.logger.info("REGISTER: already auth → /index")
|
||||
return redirect(url_for("main.index"))
|
||||
|
||||
form = RegistrationForm()
|
||||
if form.validate_on_submit():
|
||||
# 모델 내부에서 email/username 정규화됨(find_by_*)
|
||||
if User.find_by_email(form.email.data):
|
||||
flash("이미 등록된 이메일입니다.", "warning")
|
||||
current_app.logger.info("REGISTER: dup email %s", form.email.data)
|
||||
return render_template("register.html", form=form)
|
||||
|
||||
if User.find_by_username(form.username.data):
|
||||
flash("이미 사용 중인 사용자명입니다.", "warning")
|
||||
current_app.logger.info("REGISTER: dup username %s", form.username.data)
|
||||
return render_template("register.html", form=form)
|
||||
|
||||
user = User(username=form.username.data, email=form.email.data, is_active=False)
|
||||
user.set_password(form.password.data) # passlib: 기본 Argon2id
|
||||
db.session.add(user)
|
||||
db.session.commit()
|
||||
|
||||
_notify(
|
||||
f"🆕 <b>신규 가입 요청</b>\n"
|
||||
f"📛 사용자: <code>{user.username}</code>\n"
|
||||
f"📧 이메일: <code>{user.email}</code>"
|
||||
)
|
||||
current_app.logger.info("REGISTER: created id=%s email=%s", user.id, user.email)
|
||||
flash("회원가입이 완료되었습니다. 관리자의 승인을 기다려주세요.", "success")
|
||||
return redirect(url_for("auth.login"))
|
||||
else:
|
||||
if request.method == "POST":
|
||||
current_app.logger.info("REGISTER: form errors=%s", form.errors)
|
||||
|
||||
return render_template("register.html", form=form)
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
# 로그인
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
@auth_bp.route("/login", methods=["GET", "POST"])
|
||||
def login():
|
||||
if current_user.is_authenticated:
|
||||
current_app.logger.info("LOGIN: already auth → /index")
|
||||
return redirect(url_for("main.index"))
|
||||
|
||||
form = LoginForm()
|
||||
if form.validate_on_submit():
|
||||
current_app.logger.info("LOGIN: form ok email=%s", form.email.data)
|
||||
user: Optional[User] = User.find_by_email(form.email.data)
|
||||
|
||||
if not user:
|
||||
flash("이메일 또는 비밀번호가 올바르지 않습니다.", "danger")
|
||||
current_app.logger.info("LOGIN: user not found")
|
||||
return render_template("login.html", form=form)
|
||||
|
||||
pass_ok = user.check_password(form.password.data) # passlib verify(+자동 재해시)
|
||||
current_app.logger.info(
|
||||
"LOGIN: found id=%s active=%s pass_ok=%s",
|
||||
user.id, user.is_active, pass_ok
|
||||
)
|
||||
|
||||
if not pass_ok:
|
||||
flash("이메일 또는 비밀번호가 올바르지 않습니다.", "danger")
|
||||
return render_template("login.html", form=form)
|
||||
|
||||
if not user.is_active:
|
||||
flash("계정이 아직 승인되지 않았습니다.", "warning")
|
||||
return render_template("login.html", form=form)
|
||||
|
||||
# 성공
|
||||
login_user(user, remember=form.remember.data)
|
||||
session.permanent = True
|
||||
_notify(f"🔐 <b>로그인 성공</b>\n👤 <code>{user.username}</code>")
|
||||
current_app.logger.info("LOGIN: SUCCESS → redirect")
|
||||
|
||||
nxt = request.args.get("next")
|
||||
if nxt and _is_safe_url(nxt):
|
||||
return redirect(nxt)
|
||||
return redirect(url_for("main.index"))
|
||||
else:
|
||||
if request.method == "POST":
|
||||
current_app.logger.info("LOGIN: form errors=%s", form.errors)
|
||||
|
||||
return render_template("login.html", form=form)
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
# 로그아웃
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
@auth_bp.route("/logout", methods=["GET"])
|
||||
@login_required
|
||||
def logout():
|
||||
if current_user.is_authenticated:
|
||||
current_app.logger.info("LOGOUT: user=%s", current_user.username)
|
||||
logout_user()
|
||||
session["just_logged_out"] = True
|
||||
return redirect(url_for("auth.login"))
|
||||
@@ -0,0 +1,95 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
||||
from flask import Blueprint, request, jsonify, Response
|
||||
from flask_login import login_required
|
||||
|
||||
from config import Config
|
||||
import chardet
|
||||
|
||||
file_view_bp = Blueprint("file_view", __name__)
|
||||
|
||||
|
||||
def register_file_view(app):
|
||||
"""블루프린트 등록"""
|
||||
app.register_blueprint(file_view_bp)
|
||||
|
||||
|
||||
def _safe_within(base: Path, target: Path) -> bool:
|
||||
"""
|
||||
target 이 base 디렉터리 내부인지 검사 (경로 탈출 방지)
|
||||
"""
|
||||
try:
|
||||
target.resolve().relative_to(base.resolve())
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def _decode_bytes(raw: bytes) -> str:
|
||||
"""
|
||||
파일 바이트 → 문자열 디코딩 (감지 → utf-8 → cp949 순서로 시도)
|
||||
"""
|
||||
enc = (chardet.detect(raw).get("encoding") or "utf-8").strip().lower()
|
||||
for cand in (enc, "utf-8", "cp949"):
|
||||
try:
|
||||
return raw.decode(cand)
|
||||
except Exception:
|
||||
continue
|
||||
# 최후의 수단: 손실 허용 디코딩
|
||||
return raw.decode("utf-8", errors="replace")
|
||||
|
||||
|
||||
@file_view_bp.route("/view_file", methods=["GET"])
|
||||
@login_required
|
||||
def view_file():
|
||||
"""
|
||||
파일 내용을 읽어 반환.
|
||||
- /view_file?folder=idrac_info&filename=abc.txt
|
||||
- /view_file?folder=backup&date=<백업폴더명>&filename=abc.txt
|
||||
- ?raw=1 을 붙이면 text/plain 으로 원문을 반환 (모달 표시용)
|
||||
"""
|
||||
folder = request.args.get("folder", "").strip()
|
||||
date = request.args.get("date", "").strip()
|
||||
filename = request.args.get("filename", "").strip()
|
||||
want_raw = request.args.get("raw")
|
||||
|
||||
if not filename:
|
||||
return jsonify({"error": "파일 이름이 없습니다."}), 400
|
||||
|
||||
# 파일명/폴더명은 유니코드 보존. 상위 경로만 제거하여 보안 유지
|
||||
safe_name = Path(filename).name
|
||||
|
||||
if folder == "backup":
|
||||
base = Path(Config.BACKUP_FOLDER)
|
||||
safe_date = Path(date).name if date else ""
|
||||
target = (base / safe_date / safe_name).resolve()
|
||||
else:
|
||||
base = Path(Config.IDRAC_INFO_FOLDER)
|
||||
target = (base / safe_name).resolve()
|
||||
|
||||
logging.info(
|
||||
"file_view: folder=%s date=%s filename=%s | base=%s | target=%s",
|
||||
folder, date, filename, str(base), str(target)
|
||||
)
|
||||
|
||||
if not _safe_within(base, target) or not target.is_file():
|
||||
logging.warning("file_view: 파일 없음: %s", str(target))
|
||||
return jsonify({"error": "파일을 찾을 수 없습니다."}), 404
|
||||
|
||||
try:
|
||||
raw = target.read_bytes()
|
||||
content = _decode_bytes(raw)
|
||||
|
||||
if want_raw:
|
||||
# 텍스트 원문 반환 (모달에서 fetch().text()로 사용)
|
||||
return Response(content, mimetype="text/plain; charset=utf-8")
|
||||
|
||||
# JSON으로 감싸서 반환 (기존 사용처 호환)
|
||||
return jsonify({"content": content})
|
||||
|
||||
except Exception as e:
|
||||
logging.error("file_view: 파일 읽기 실패: %s → %s", str(target), e)
|
||||
return jsonify({"error": "파일 열기 중 오류 발생"}), 500
|
||||
@@ -0,0 +1,13 @@
|
||||
from __future__ import annotations
|
||||
from flask import Blueprint, render_template
|
||||
|
||||
home_bp = Blueprint("home", __name__, url_prefix="/home")
|
||||
|
||||
|
||||
def register_home_routes(app):
|
||||
app.register_blueprint(home_bp)
|
||||
|
||||
|
||||
@home_bp.route("/", methods=["GET"])
|
||||
def home():
|
||||
return render_template("home.html")
|
||||
@@ -0,0 +1,208 @@
|
||||
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)
|
||||
|
||||
scripts = [f.name for f in script_dir.glob("*") if f.is_file() and f.name != ".env"]
|
||||
scripts = natsorted(scripts)
|
||||
xml_files = [f.name for f in xml_dir.glob("*.xml")]
|
||||
|
||||
# 페이지네이션
|
||||
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
|
||||
|
||||
# 백업 폴더 목록 (디렉터리만)
|
||||
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,
|
||||
backup_files=backup_files,
|
||||
total_backup_pages=total_backup_pages,
|
||||
backup_page=backup_page,
|
||||
scripts=scripts,
|
||||
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/<job_id>")
|
||||
@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/<filename>")
|
||||
@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/<filename>", 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/<date>/<filename>")
|
||||
@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)
|
||||
@@ -0,0 +1,195 @@
|
||||
from __future__ import annotations
|
||||
import os
|
||||
import sys
|
||||
import shutil
|
||||
import subprocess
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from flask import Blueprint, request, redirect, url_for, flash, jsonify, send_file
|
||||
from flask_login import login_required
|
||||
from config import Config
|
||||
|
||||
utils_bp = Blueprint("utils", __name__)
|
||||
|
||||
|
||||
def register_util_routes(app):
|
||||
app.register_blueprint(utils_bp)
|
||||
|
||||
|
||||
@utils_bp.route("/move_mac_files", methods=["POST"])
|
||||
@login_required
|
||||
def move_mac_files():
|
||||
src = Path(Config.IDRAC_INFO_FOLDER)
|
||||
dst = Path(Config.MAC_FOLDER)
|
||||
dst.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
moved = 0
|
||||
errors = []
|
||||
|
||||
try:
|
||||
for file in src.iterdir():
|
||||
if not file.is_file():
|
||||
continue
|
||||
|
||||
try:
|
||||
# 파일이 존재하는지 확인
|
||||
if not file.exists():
|
||||
errors.append(f"{file.name}: 파일이 존재하지 않음")
|
||||
continue
|
||||
|
||||
# 대상 파일이 이미 존재하는 경우 건너뛰기
|
||||
target = dst / file.name
|
||||
if target.exists():
|
||||
logging.warning(f"⚠️ 파일이 이미 존재하여 건너뜀: {file.name}")
|
||||
continue
|
||||
|
||||
shutil.move(str(file), str(target))
|
||||
moved += 1
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"{file.name}: {str(e)}"
|
||||
errors.append(error_msg)
|
||||
logging.error(f"❌ 파일 이동 실패: {error_msg}")
|
||||
|
||||
# 결과 로깅
|
||||
if moved > 0:
|
||||
logging.info(f"✅ MAC 파일 이동 완료 ({moved}개)")
|
||||
|
||||
if errors:
|
||||
logging.warning(f"⚠️ 일부 파일 이동 실패: {errors}")
|
||||
|
||||
# 하나라도 성공하면 success: true 반환
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"moved": moved,
|
||||
"errors": errors if errors else None
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"❌ MAC 이동 중 치명적 오류: {e}")
|
||||
return jsonify({"success": False, "error": str(e)})
|
||||
|
||||
|
||||
@utils_bp.route("/move_guid_files", methods=["POST"])
|
||||
@login_required
|
||||
def move_guid_files():
|
||||
src = Path(Config.IDRAC_INFO_FOLDER)
|
||||
dst = Path(Config.GUID_FOLDER)
|
||||
dst.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
moved = 0
|
||||
errors = []
|
||||
|
||||
try:
|
||||
for file in src.iterdir():
|
||||
if not file.is_file():
|
||||
continue
|
||||
|
||||
try:
|
||||
# 파일이 존재하는지 확인
|
||||
if not file.exists():
|
||||
errors.append(f"{file.name}: 파일이 존재하지 않음")
|
||||
continue
|
||||
|
||||
# 대상 파일이 이미 존재하는 경우 건너뛰기
|
||||
target = dst / file.name
|
||||
if target.exists():
|
||||
logging.warning(f"⚠️ 파일이 이미 존재하여 건너뜀: {file.name}")
|
||||
continue
|
||||
|
||||
shutil.move(str(file), str(target))
|
||||
moved += 1
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"{file.name}: {str(e)}"
|
||||
errors.append(error_msg)
|
||||
logging.error(f"❌ 파일 이동 실패: {error_msg}")
|
||||
|
||||
# 결과 메시지
|
||||
if moved > 0:
|
||||
flash(f"GUID 파일이 성공적으로 이동되었습니다. ({moved}개)", "success")
|
||||
logging.info(f"✅ GUID 파일 이동 완료 ({moved}개)")
|
||||
|
||||
if errors:
|
||||
logging.warning(f"⚠️ 일부 파일 이동 실패: {errors}")
|
||||
flash(f"일부 파일 이동 실패: {len(errors)}개", "warning")
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"❌ GUID 이동 오류: {e}")
|
||||
flash(f"오류 발생: {e}", "danger")
|
||||
|
||||
return redirect(url_for("main.index"))
|
||||
|
||||
|
||||
@utils_bp.route("/update_server_list", methods=["POST"])
|
||||
@login_required
|
||||
def update_server_list():
|
||||
content = request.form.get("server_list_content")
|
||||
if not content:
|
||||
flash("내용을 입력하세요.", "warning")
|
||||
return redirect(url_for("main.index"))
|
||||
|
||||
path = Path(Config.SERVER_LIST_FOLDER) / "server_list.txt"
|
||||
try:
|
||||
path.write_text(content, encoding="utf-8")
|
||||
result = subprocess.run(
|
||||
[sys.executable, str(Path(Config.SERVER_LIST_FOLDER) / "excel.py")],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True,
|
||||
cwd=str(Path(Config.SERVER_LIST_FOLDER)),
|
||||
timeout=300,
|
||||
)
|
||||
logging.info(f"서버 리스트 스크립트 실행 결과: {result.stdout}")
|
||||
flash("서버 리스트가 업데이트되었습니다.", "success")
|
||||
except subprocess.CalledProcessError as e:
|
||||
logging.error(f"서버 리스트 스크립트 오류: {e.stderr}")
|
||||
flash(f"스크립트 실행 실패: {e.stderr}", "danger")
|
||||
except Exception as e:
|
||||
logging.error(f"서버 리스트 처리 오류: {e}")
|
||||
flash(f"서버 리스트 처리 중 오류 발생: {e}", "danger")
|
||||
|
||||
return redirect(url_for("main.index"))
|
||||
|
||||
|
||||
@utils_bp.route("/update_guid_list", methods=["POST"])
|
||||
@login_required
|
||||
def update_guid_list():
|
||||
content = request.form.get("server_list_content")
|
||||
if not content:
|
||||
flash("내용을 입력하세요.", "warning")
|
||||
return redirect(url_for("main.index"))
|
||||
|
||||
path = Path(Config.SERVER_LIST_FOLDER) / "guid_list.txt"
|
||||
try:
|
||||
path.write_text(content, encoding="utf-8")
|
||||
result = subprocess.run(
|
||||
[sys.executable, str(Path(Config.SERVER_LIST_FOLDER) / "GUIDtxtT0Execl.py")],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True,
|
||||
cwd=str(Path(Config.SERVER_LIST_FOLDER)),
|
||||
timeout=300,
|
||||
)
|
||||
logging.info(f"GUID 리스트 스크립트 실행 결과: {result.stdout}")
|
||||
flash("GUID 리스트가 업데이트되었습니다.", "success")
|
||||
except subprocess.CalledProcessError as e:
|
||||
logging.error(f"GUID 리스트 스크립트 오류: {e.stderr}")
|
||||
flash(f"스크립트 실행 실패: {e.stderr}", "danger")
|
||||
except Exception as e:
|
||||
logging.error(f"GUID 리스트 처리 오류: {e}")
|
||||
flash(f"GUID 리스트 처리 중 오류 발생: {e}", "danger")
|
||||
|
||||
return redirect(url_for("main.index"))
|
||||
|
||||
|
||||
@utils_bp.route("/download_excel")
|
||||
@login_required
|
||||
def download_excel():
|
||||
path = Path(Config.SERVER_LIST_FOLDER) / "mac_info.xlsx"
|
||||
if not path.is_file():
|
||||
flash("엑셀 파일을 찾을 수 없습니다.", "danger")
|
||||
return redirect(url_for("main.index"))
|
||||
|
||||
logging.info(f"엑셀 파일 다운로드: {path}")
|
||||
return send_file(str(path), as_attachment=True, download_name="mac_info.xlsx")
|
||||
@@ -0,0 +1,105 @@
|
||||
from __future__ import annotations
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from flask import Blueprint, render_template, request, redirect, url_for, flash
|
||||
from flask_login import login_required
|
||||
from werkzeug.utils import secure_filename
|
||||
from config import Config
|
||||
|
||||
xml_bp = Blueprint("xml", __name__)
|
||||
|
||||
|
||||
def register_xml_routes(app):
|
||||
app.register_blueprint(xml_bp)
|
||||
|
||||
|
||||
def allowed_file(filename: str) -> bool:
|
||||
return "." in filename and filename.rsplit(".", 1)[1].lower() in Config.ALLOWED_EXTENSIONS
|
||||
|
||||
|
||||
@xml_bp.route("/xml_management")
|
||||
@login_required
|
||||
def xml_management():
|
||||
xml_dir = Path(Config.XML_FOLDER)
|
||||
try:
|
||||
files = [f.name for f in xml_dir.iterdir() if f.is_file()]
|
||||
except FileNotFoundError:
|
||||
files = []
|
||||
flash("XML 폴더가 존재하지 않습니다.", "danger")
|
||||
return render_template("manage_xml.html", xml_files=files)
|
||||
|
||||
|
||||
@xml_bp.route("/upload_xml", methods=["POST"])
|
||||
@login_required
|
||||
def upload_xml():
|
||||
file = request.files.get("xmlFile")
|
||||
if not file or not file.filename:
|
||||
flash("업로드할 파일을 선택하세요.", "warning")
|
||||
return redirect(url_for("xml.xml_management"))
|
||||
|
||||
if allowed_file(file.filename):
|
||||
filename = secure_filename(file.filename)
|
||||
save_path = Path(Config.XML_FOLDER) / filename
|
||||
try:
|
||||
save_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
file.save(str(save_path))
|
||||
# 텍스트 파일이므로 0644 권장
|
||||
try:
|
||||
save_path.chmod(0o644)
|
||||
except Exception:
|
||||
pass # Windows 등에서 무시
|
||||
logging.info(f"XML 업로드됨: {filename}")
|
||||
flash("파일이 성공적으로 업로드되었습니다.", "success")
|
||||
except Exception as e:
|
||||
logging.error(f"파일 업로드 오류: {e}")
|
||||
flash("파일 저장 중 오류가 발생했습니다.", "danger")
|
||||
else:
|
||||
flash("XML 확장자만 업로드할 수 있습니다.", "warning")
|
||||
|
||||
return redirect(url_for("xml.xml_management"))
|
||||
|
||||
|
||||
@xml_bp.route("/delete_xml/<filename>", methods=["POST"])
|
||||
@login_required
|
||||
def delete_xml(filename: str):
|
||||
path = Path(Config.XML_FOLDER) / secure_filename(filename)
|
||||
if path.exists():
|
||||
try:
|
||||
path.unlink()
|
||||
flash(f"{filename} 파일이 삭제되었습니다.", "success")
|
||||
logging.info(f"XML 삭제됨: {filename}")
|
||||
except Exception as e:
|
||||
logging.error(f"XML 삭제 오류: {e}")
|
||||
flash("파일 삭제 중 오류 발생", "danger")
|
||||
else:
|
||||
flash("해당 파일이 존재하지 않습니다.", "warning")
|
||||
return redirect(url_for("xml.xml_management"))
|
||||
|
||||
|
||||
@xml_bp.route("/edit_xml/<filename>", methods=["GET", "POST"])
|
||||
@login_required
|
||||
def edit_xml(filename: str):
|
||||
path = Path(Config.XML_FOLDER) / secure_filename(filename)
|
||||
if not path.exists():
|
||||
flash("파일을 찾을 수 없습니다.", "danger")
|
||||
return redirect(url_for("xml.xml_management"))
|
||||
|
||||
if request.method == "POST":
|
||||
new_content = request.form.get("content", "")
|
||||
try:
|
||||
path.write_text(new_content, encoding="utf-8")
|
||||
logging.info(f"XML 수정됨: {filename}")
|
||||
flash("파일이 성공적으로 수정되었습니다.", "success")
|
||||
return redirect(url_for("xml.xml_management"))
|
||||
except Exception as e:
|
||||
logging.error(f"XML 저장 실패: {e}")
|
||||
flash("파일 저장 중 오류가 발생했습니다.", "danger")
|
||||
|
||||
try:
|
||||
content = path.read_text(encoding="utf-8")
|
||||
except Exception as e:
|
||||
logging.error(f"XML 열기 실패: {e}")
|
||||
flash("파일 열기 중 오류가 발생했습니다.", "danger")
|
||||
content = ""
|
||||
|
||||
return render_template("edit_xml.html", filename=filename, content=content)
|
||||
Reference in New Issue
Block a user