SmartEDT/backend/utils.py

75 lines
2.5 KiB
Python
Raw Normal View History

"""后端通用工具函数。
该模块放置与业务无关的通用能力
- UTC 时间获取
- 日志初始化控制台 + 可选文件滚动
- 受限路径拼接防目录穿越
"""
from __future__ import annotations
import logging
import os
from datetime import datetime, timezone
from logging.handlers import TimedRotatingFileHandler
from pathlib import Path
def utc_now() -> datetime:
"""返回当前 UTC 时间timezone-aware"""
return datetime.now(timezone.utc)
def configure_logging(level: str, log_file: Path | None = None) -> None:
"""配置全局日志。
Args:
level: 日志级别字符串例如 "INFO" / "DEBUG"
log_file: 可选的日志文件路径提供时启用按天滚动
"""
level_value = getattr(logging, level.upper(), logging.INFO)
logging_handlers: list[logging.Handler] = [logging.StreamHandler()]
if log_file is not None:
log_file.parent.mkdir(parents=True, exist_ok=True)
rotating_handler = TimedRotatingFileHandler(
log_file,
when="midnight",
interval=1,
backupCount=90,
encoding="utf-8",
delay=True,
)
rotating_handler.suffix = "%Y-%m-%d"
logging_handlers.append(rotating_handler)
logging.basicConfig(
level=level_value,
format="%(asctime)s %(levelname)s %(name)s %(message)s",
handlers=logging_handlers,
force=True,
)
for logger_name in ("uvicorn", "uvicorn.error", "uvicorn.access", "uvicorn.asgi"):
logger = logging.getLogger(logger_name)
logger.handlers = []
logger.propagate = True
def project_root() -> Path:
"""返回项目根目录backend 的上一级)。"""
return Path(__file__).resolve().parents[1]
def safe_join(root: Path, untrusted_path: str) -> Path:
"""将不可信路径拼接到 root 下,并阻止目录穿越/绝对路径/UNC 路径。
主要用于下载/文件访问等接口避免访问到文件根目录之外
"""
if untrusted_path.startswith(("\\\\", "//")):
raise ValueError("UNC path is not allowed")
if os.path.isabs(untrusted_path):
raise ValueError("Absolute path is not allowed")
candidate = (root / untrusted_path).resolve()
root_resolved = root.resolve()
if root_resolved not in candidate.parents and candidate != root_resolved:
raise ValueError("Path traversal detected")
return candidate