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