Files
hyungi 3db351002c ops(hygiene): jwt_secret fail-loud + 로그 회전 + sqlite gitignore + eval override 제거
JWT_SECRET 빈값이면 부팅 RuntimeError (구: 빈 키로 전 토큰 서명하며 침묵 부팅 = 인증붕괴).
core.utils setup_logger FileHandler→RotatingFileHandler(10MB×3) — logs 무한증가 차단.
.gitignore *.sqlite3 + 0바이트 db.sqlite3 제거. Phase 2A/2B closed eval override 2파일 git rm
(참조 0, history 보존). lockfile 은 제외(별도).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 05:54:09 +00:00

112 lines
3.1 KiB
Python

"""공통 유틸리티 — v1 pkm_utils.py에서 AppleScript 제거, 나머지 포팅"""
import hashlib
import logging
from logging.handlers import RotatingFileHandler
from pathlib import Path
def setup_logger(name: str, log_dir: str = "logs") -> logging.Logger:
"""로거 설정"""
Path(log_dir).mkdir(exist_ok=True)
logger = logging.getLogger(name)
logger.setLevel(logging.INFO)
if not logger.handlers:
# 파일 핸들러
fh = RotatingFileHandler(
f"{log_dir}/{name}.log", maxBytes=10 * 1024 * 1024, backupCount=3, encoding="utf-8"
)
fh.setFormatter(logging.Formatter(
"%(asctime)s [%(levelname)s] %(message)s",
datefmt="%Y-%m-%d %H:%M:%S"
))
logger.addHandler(fh)
# 콘솔 핸들러
ch = logging.StreamHandler()
ch.setFormatter(logging.Formatter("[%(levelname)s] %(message)s"))
logger.addHandler(ch)
return logger
def file_hash(path: str | Path) -> str:
"""파일 SHA-256 해시 계산"""
sha256 = hashlib.sha256()
with open(path, "rb") as f:
for chunk in iter(lambda: f.read(8192), b""):
sha256.update(chunk)
return sha256.hexdigest()
def count_log_errors(log_path: str) -> int:
"""로그 파일에서 ERROR 건수 카운트"""
try:
with open(log_path, encoding="utf-8") as f:
return sum(1 for line in f if "[ERROR]" in line)
except FileNotFoundError:
return 0
# ─── CalDAV 헬퍼 ───
def escape_ical_text(text: str | None) -> str:
"""iCalendar TEXT 값 이스케이프 (RFC 5545 §3.3.11).
SUMMARY, DESCRIPTION, LOCATION 등 TEXT 프로퍼티에 사용.
"""
if not text:
return ""
text = text.replace("\r\n", "\n").replace("\r", "\n") # CRLF 정규화
text = text.replace("\\", "\\\\") # 백슬래시 먼저
text = text.replace("\n", "\\n")
text = text.replace(",", "\\,")
text = text.replace(";", "\\;")
return text
def create_caldav_todo(
caldav_url: str,
username: str,
password: str,
title: str,
description: str = "",
due_days: int = 7,
) -> str | None:
"""Synology Calendar에 VTODO 생성, UID 반환"""
import uuid
from datetime import datetime, timedelta, timezone
import caldav
try:
client = caldav.DAVClient(url=caldav_url, username=username, password=password)
principal = client.principal()
calendars = principal.calendars()
if not calendars:
return None
calendar = calendars[0]
uid = str(uuid.uuid4())
due = datetime.now(timezone.utc) + timedelta(days=due_days)
due_str = due.strftime("%Y%m%dT%H%M%SZ")
vtodo = f"""BEGIN:VCALENDAR
VERSION:2.0
BEGIN:VTODO
UID:{uid}
SUMMARY:{escape_ical_text(title)}
DESCRIPTION:{escape_ical_text(description)}
DUE:{due_str}
STATUS:NEEDS-ACTION
PRIORITY:5
END:VTODO
END:VCALENDAR"""
calendar.save_event(vtodo)
return uid
except Exception as e:
logging.getLogger("caldav").error(f"CalDAV VTODO 생성 실패: {e}")
return None