55 lines
1.8 KiB
Python
55 lines
1.8 KiB
Python
|
|
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:
|
||
|
|
return datetime.now(timezone.utc)
|
||
|
|
|
||
|
|
|
||
|
|
def configure_logging(level: str, log_file: Path | None = None) -> None:
|
||
|
|
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:
|
||
|
|
return Path(__file__).resolve().parents[1]
|
||
|
|
|
||
|
|
|
||
|
|
def safe_join(root: Path, untrusted_path: str) -> Path:
|
||
|
|
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
|