chore: initialize insar management system v2

This commit is contained in:
2026-04-14 13:16:01 +08:00
commit ecc72ec9cd
361 changed files with 2142522 additions and 0 deletions
+189
View File
@@ -0,0 +1,189 @@
"""
日志管理 API
提供日志文件的列表、查看、删除功能。
"""
import itertools
import logging
import os
from typing import List, Optional
from datetime import datetime
from pathlib import Path
logger = logging.getLogger(__name__)
from fastapi import APIRouter, Depends, HTTPException, Query
from pydantic import BaseModel
from ..config import settings
from ..models import AuthUserORM
from .dependencies import _get_current_user
router = APIRouter(prefix="/logs", tags=["logs"])
# 日志根目录
LOG_ROOT = Path(settings.PROJECT_ROOT) / "logs"
class LogFileInfo(BaseModel):
"""日志文件信息"""
name: str
path: str
size: int
modified_at: str
type: str # app, task, error
class LogContentResponse(BaseModel):
"""日志内容响应"""
file_name: str
total_lines: int
content: str
offset: int
limit: int
def _get_log_type(file_path: Path) -> str:
"""根据路径判断日志类型"""
path_str = str(file_path)
if "logs/app" in path_str or "logs\\app" in path_str:
return "app"
elif "logs/error" in path_str or "logs\\error" in path_str:
return "error"
elif "logs/tasks" in path_str or "logs\\tasks" in path_str:
return "task"
return "other"
def _is_safe_path(file_path: str) -> bool:
"""检查路径是否安全(防止路径遍历攻击)"""
try:
requested_path = (LOG_ROOT / file_path).resolve()
return requested_path.is_relative_to(LOG_ROOT)
except (ValueError, RuntimeError):
return False
@router.get("/list", response_model=List[LogFileInfo])
async def list_logs(
log_type: Optional[str] = Query(None, description="日志类型: app, task, error"),
current_user: AuthUserORM = Depends(_get_current_user)
):
"""
列出所有日志文件
- 支持按类型过滤
- 返回文件名、大小、修改时间
"""
if not LOG_ROOT.exists():
return []
log_files = []
# 遍历日志目录
for log_file in LOG_ROOT.rglob("*.log"):
if log_file.is_file() and log_file.name != ".gitkeep":
file_type = _get_log_type(log_file)
# 类型过滤
if log_type and file_type != log_type:
continue
# 获取相对路径
relative_path = log_file.relative_to(LOG_ROOT)
log_files.append(LogFileInfo(
name=log_file.name,
path=str(relative_path).replace("\\", "/"),
size=log_file.stat().st_size,
modified_at=datetime.fromtimestamp(log_file.stat().st_mtime).strftime("%Y-%m-%d %H:%M:%S"),
type=file_type
))
# 按修改时间倒序排序
log_files.sort(key=lambda x: x.modified_at, reverse=True)
return log_files
@router.get("/content/{log_path:path}", response_model=LogContentResponse)
async def get_log_content(
log_path: str,
offset: int = Query(0, ge=0, description="起始行号"),
limit: int = Query(1000, ge=1, le=10000, description="读取行数"),
current_user: AuthUserORM = Depends(_get_current_user)
):
"""
读取日志内容
- 支持分页读取(大文件)
- offset: 起始行号(从 0 开始)
- limit: 读取行数(最多 10000 行)
"""
# 安全检查
if not _is_safe_path(log_path):
raise HTTPException(status_code=400, detail="非法的文件路径")
log_file = LOG_ROOT / log_path
if not log_file.exists():
raise HTTPException(status_code=404, detail="日志文件不存在")
if not log_file.is_file():
raise HTTPException(status_code=400, detail="不是有效的文件")
try:
with open(log_file, "r", encoding="utf-8", errors="replace") as f:
# Count total lines without loading all into memory
total_lines = sum(1 for _ in f)
with open(log_file, "r", encoding="utf-8", errors="replace") as f:
# Skip to offset using islice, then read 'limit' lines
content_lines = list(itertools.islice(f, offset, offset + limit))
return LogContentResponse(
file_name=log_file.name,
total_lines=total_lines,
content="".join(content_lines),
offset=offset,
limit=limit
)
except Exception as e:
logger.exception("读取日志失败: %s", log_file.name)
raise HTTPException(status_code=500, detail="读取日志失败,请查看后端日志")
@router.delete("/{log_path:path}")
async def delete_log(
log_path: str,
current_user: AuthUserORM = Depends(_get_current_user)
):
"""
删除日志文件
- 仅管理员可以删除
- 需要确认操作
"""
# 权限检查
if current_user.role != "admin":
raise HTTPException(status_code=403, detail="只有管理员可以删除日志")
# 安全检查
if not _is_safe_path(log_path):
raise HTTPException(status_code=400, detail="非法的文件路径")
log_file = LOG_ROOT / log_path
if not log_file.exists():
raise HTTPException(status_code=404, detail="日志文件不存在")
if not log_file.is_file():
raise HTTPException(status_code=400, detail="不是有效的文件")
try:
log_file.unlink()
return {"message": f"日志文件 {log_file.name} 已删除"}
except Exception as e:
logger.exception("删除日志失败: %s", log_file.name)
raise HTTPException(status_code=500, detail="删除日志失败,请查看后端日志")