mirror of
https://github.com/fastapiadmin/FastapiAdmin.git
synced 2026-09-21 04:46:26 +00:00
style: 移除Python文件中的编码声明并优化代码格式
refactor: 重构前端组件和样式,添加AI助手功能 docs: 更新README文档,添加ruff代码检查说明 feat: 新增AI助手相关API和前端组件 chore: 更新.gitignore文件,添加ruff缓存配置 fix: 修复前端布局和设置相关的问题 perf: 优化代码结构和性能,移除冗余代码 test: 更新测试文件,移除编码声明 build: 更新依赖版本,调整requirements.txt
This commit is contained in:
@@ -1,5 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from fastapi import APIRouter
|
||||
|
||||
from .cache.controller import CacheRouter
|
||||
|
||||
@@ -1,2 +1 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
|
||||
+21
-22
@@ -1,18 +1,17 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
from fastapi.responses import JSONResponse
|
||||
from redis.asyncio.client import Redis
|
||||
|
||||
from app.common.response import SuccessResponse
|
||||
from app.core.exceptions import CustomException
|
||||
from app.core.dependencies import AuthPermission, redis_getter
|
||||
from app.core.exceptions import CustomException
|
||||
from app.core.logger import log
|
||||
from app.core.router_class import OperationLogRoute
|
||||
|
||||
from .service import CacheService
|
||||
|
||||
|
||||
CacheRouter = APIRouter(route_class=OperationLogRoute, prefix="/cache", tags=["缓存监控"])
|
||||
|
||||
|
||||
@@ -23,11 +22,11 @@ CacheRouter = APIRouter(route_class=OperationLogRoute, prefix="/cache", tags=["
|
||||
description="获取缓存监控信息"
|
||||
)
|
||||
async def get_monitor_cache_info_controller(
|
||||
redis: Redis = Depends(redis_getter)
|
||||
redis: Annotated[Redis, Depends(redis_getter)]
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
获取缓存监控统计信息
|
||||
|
||||
|
||||
返回:
|
||||
- JSONResponse: 包含缓存监控统计信息的JSON响应
|
||||
"""
|
||||
@@ -45,7 +44,7 @@ async def get_monitor_cache_info_controller(
|
||||
async def get_monitor_cache_name_controller() -> JSONResponse:
|
||||
"""
|
||||
获取缓存名称列表
|
||||
|
||||
|
||||
返回:
|
||||
- JSONResponse: 包含缓存名称列表的JSON响应
|
||||
"""
|
||||
@@ -62,14 +61,14 @@ async def get_monitor_cache_name_controller() -> JSONResponse:
|
||||
)
|
||||
async def get_monitor_cache_key_controller(
|
||||
cache_name: str,
|
||||
redis: Redis = Depends(redis_getter)
|
||||
redis: Annotated[Redis, Depends(redis_getter)]
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
获取指定缓存名称下的键名列表
|
||||
|
||||
|
||||
参数:
|
||||
- cache_name (str): 缓存名称
|
||||
|
||||
|
||||
返回:
|
||||
- JSONResponse: 包含缓存键名列表的JSON响应
|
||||
"""
|
||||
@@ -85,17 +84,17 @@ async def get_monitor_cache_key_controller(
|
||||
description="获取缓存值"
|
||||
)
|
||||
async def get_monitor_cache_value_controller(
|
||||
cache_name: str,
|
||||
cache_name: str,
|
||||
cache_key: str,
|
||||
redis: Redis = Depends(redis_getter)
|
||||
)-> JSONResponse:
|
||||
redis: Annotated[Redis, Depends(redis_getter)]
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
获取指定缓存键的值
|
||||
|
||||
|
||||
参数:
|
||||
- cache_name (str): 缓存名称
|
||||
- cache_key (str): 缓存键
|
||||
|
||||
|
||||
返回:
|
||||
- JSONResponse: 包含缓存值的JSON响应
|
||||
"""
|
||||
@@ -112,14 +111,14 @@ async def get_monitor_cache_value_controller(
|
||||
)
|
||||
async def clear_monitor_cache_name_controller(
|
||||
cache_name: str,
|
||||
redis: Redis = Depends(redis_getter)
|
||||
redis: Annotated[Redis, Depends(redis_getter)]
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
清除指定缓存名称下的所有缓存
|
||||
|
||||
|
||||
参数:
|
||||
- cache_name (str): 缓存名称
|
||||
|
||||
|
||||
返回:
|
||||
- JSONResponse: 包含清除结果的JSON响应
|
||||
"""
|
||||
@@ -138,14 +137,14 @@ async def clear_monitor_cache_name_controller(
|
||||
)
|
||||
async def clear_monitor_cache_key_controller(
|
||||
cache_key: str,
|
||||
redis: Redis = Depends(redis_getter)
|
||||
redis: Annotated[Redis, Depends(redis_getter)]
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
清除指定缓存键
|
||||
|
||||
|
||||
参数:
|
||||
- cache_key (str): 缓存键
|
||||
|
||||
|
||||
返回:
|
||||
- JSONResponse: 包含清除结果的JSON响应
|
||||
"""
|
||||
@@ -163,11 +162,11 @@ async def clear_monitor_cache_key_controller(
|
||||
description="清除所有缓存"
|
||||
)
|
||||
async def clear_monitor_cache_all_controller(
|
||||
redis: Redis = Depends(redis_getter)
|
||||
redis: Annotated[Redis, Depends(redis_getter)]
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
清除所有缓存
|
||||
|
||||
|
||||
返回:
|
||||
- JSONResponse: 包含清除结果的JSON响应
|
||||
"""
|
||||
|
||||
+1
-2
@@ -1,6 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from typing import Any
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
|
||||
|
||||
|
||||
+24
-30
@@ -1,11 +1,9 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from redis.asyncio.client import Redis
|
||||
|
||||
from app.common.enums import RedisInitKeyConfig
|
||||
from app.core.redis_crud import RedisCURD
|
||||
|
||||
from .schema import CacheMonitorSchema, CacheInfoSchema
|
||||
from .schema import CacheInfoSchema, CacheMonitorSchema
|
||||
|
||||
|
||||
class CacheService:
|
||||
@@ -14,13 +12,13 @@ class CacheService:
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
async def get_cache_monitor_statistical_info_service(cls, redis: Redis)->dict:
|
||||
async def get_cache_monitor_statistical_info_service(cls, redis: Redis) -> dict:
|
||||
"""
|
||||
获取缓存监控信息。
|
||||
|
||||
|
||||
参数:
|
||||
- redis (Redis): Redis 对象。
|
||||
|
||||
|
||||
返回:
|
||||
- dict: 缓存监控信息字典。
|
||||
"""
|
||||
@@ -29,42 +27,38 @@ class CacheService:
|
||||
command_stats_dict = await RedisCURD(redis).commandstats()
|
||||
|
||||
command_stats = [
|
||||
dict(name=key.split('_')[1], value=str(value.get('calls'))) for key, value in command_stats_dict.items()
|
||||
{'name': key.split('_')[1], 'value': str(value.get('calls'))} for key, value in command_stats_dict.items()
|
||||
]
|
||||
result = CacheMonitorSchema(command_stats=command_stats, db_size=db_size, info=info)
|
||||
|
||||
return result.model_dump()
|
||||
|
||||
@classmethod
|
||||
async def get_cache_monitor_cache_name_service(cls)->list:
|
||||
async def get_cache_monitor_cache_name_service(cls) -> list:
|
||||
"""
|
||||
获取缓存名称列表信息。
|
||||
|
||||
|
||||
返回:
|
||||
- list: 缓存名称列表信息。
|
||||
"""
|
||||
name_list = []
|
||||
for key_config in RedisInitKeyConfig:
|
||||
name_list.append(
|
||||
CacheInfoSchema(
|
||||
name_list = [CacheInfoSchema(
|
||||
cache_key='',
|
||||
cache_name=key_config.key,
|
||||
cache_value='',
|
||||
remark=key_config.remark,
|
||||
).model_dump()
|
||||
)
|
||||
).model_dump() for key_config in RedisInitKeyConfig]
|
||||
|
||||
return name_list
|
||||
|
||||
@classmethod
|
||||
async def get_cache_monitor_cache_key_service(cls, redis: Redis, cache_name: str)->list:
|
||||
async def get_cache_monitor_cache_key_service(cls, redis: Redis, cache_name: str) -> list:
|
||||
"""
|
||||
获取缓存键名列表信息。
|
||||
|
||||
|
||||
参数:
|
||||
- redis (Redis): Redis 对象。
|
||||
- cache_name (str): 缓存名称。
|
||||
|
||||
|
||||
返回:
|
||||
- list: 缓存键名列表信息。
|
||||
"""
|
||||
@@ -74,15 +68,15 @@ class CacheService:
|
||||
return cache_key_list
|
||||
|
||||
@classmethod
|
||||
async def get_cache_monitor_cache_value_service(cls, redis: Redis, cache_name: str, cache_key: str)->dict:
|
||||
async def get_cache_monitor_cache_value_service(cls, redis: Redis, cache_name: str, cache_key: str) -> dict:
|
||||
"""
|
||||
获取缓存内容信息。
|
||||
|
||||
|
||||
参数:
|
||||
- redis (Redis): Redis 对象。
|
||||
- cache_name (str): 缓存名称。
|
||||
- cache_key (str): 缓存键名。
|
||||
|
||||
|
||||
返回:
|
||||
- dict: 缓存内容信息字典。
|
||||
"""
|
||||
@@ -91,14 +85,14 @@ class CacheService:
|
||||
return CacheInfoSchema(cache_key=cache_key, cache_name=cache_name, cache_value=cache_value, remark='').model_dump()
|
||||
|
||||
@classmethod
|
||||
async def clear_cache_monitor_cache_name_service(cls, redis: Redis, cache_name: str)->bool:
|
||||
async def clear_cache_monitor_cache_name_service(cls, redis: Redis, cache_name: str) -> bool:
|
||||
"""
|
||||
清除指定缓存名称对应的所有键值。
|
||||
|
||||
|
||||
参数:
|
||||
- redis (Redis): Redis 对象。
|
||||
- cache_name (str): 缓存名称。
|
||||
|
||||
|
||||
返回:
|
||||
- bool: 是否清理成功。
|
||||
"""
|
||||
@@ -109,14 +103,14 @@ class CacheService:
|
||||
return True
|
||||
|
||||
@classmethod
|
||||
async def clear_cache_monitor_cache_key_service(cls, redis: Redis, cache_key: str)->bool:
|
||||
async def clear_cache_monitor_cache_key_service(cls, redis: Redis, cache_key: str) -> bool:
|
||||
"""
|
||||
清除匹配指定键名的所有键值。
|
||||
|
||||
|
||||
参数:
|
||||
- redis (Redis): Redis 对象。
|
||||
- cache_key (str): 缓存键名。
|
||||
|
||||
|
||||
返回:
|
||||
- bool: 是否清理成功。
|
||||
"""
|
||||
@@ -127,13 +121,13 @@ class CacheService:
|
||||
return True
|
||||
|
||||
@classmethod
|
||||
async def clear_cache_monitor_all_service(cls, redis: Redis)->bool:
|
||||
async def clear_cache_monitor_all_service(cls, redis: Redis) -> bool:
|
||||
"""
|
||||
清除所有缓存。
|
||||
|
||||
|
||||
参数:
|
||||
- redis (Redis): Redis 对象。
|
||||
|
||||
|
||||
返回:
|
||||
- bool: 是否清理成功。
|
||||
"""
|
||||
|
||||
@@ -1,2 +1 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
|
||||
@@ -1,69 +1,68 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import APIRouter, Body, Depends
|
||||
from fastapi.responses import JSONResponse
|
||||
from redis.asyncio.client import Redis
|
||||
|
||||
from app.common.request import PaginationService
|
||||
from app.common.response import SuccessResponse,ErrorResponse
|
||||
from app.core.dependencies import AuthPermission, redis_getter
|
||||
from app.common.response import ErrorResponse, SuccessResponse
|
||||
from app.core.base_params import PaginationQueryParam
|
||||
from app.core.router_class import OperationLogRoute
|
||||
from app.core.dependencies import AuthPermission, redis_getter
|
||||
from app.core.logger import log
|
||||
from app.core.router_class import OperationLogRoute
|
||||
|
||||
from .schema import OnlineQueryParam
|
||||
from .service import OnlineService
|
||||
|
||||
|
||||
OnlineRouter = APIRouter(route_class=OperationLogRoute, prefix="/online", tags=["在线用户"])
|
||||
|
||||
|
||||
@OnlineRouter.get(
|
||||
'/list',
|
||||
'/list',
|
||||
dependencies=[Depends(AuthPermission(['module_monitor:online:query']))],
|
||||
summary="获取在线用户列表",
|
||||
description="获取在线用户列表"
|
||||
)
|
||||
async def get_online_list_controller(
|
||||
redis: Redis = Depends(redis_getter),
|
||||
paging_query: PaginationQueryParam = Depends(),
|
||||
search: OnlineQueryParam = Depends()
|
||||
)->JSONResponse:
|
||||
redis: Annotated[Redis, Depends(redis_getter)],
|
||||
paging_query: Annotated[PaginationQueryParam, Depends()],
|
||||
search: Annotated[OnlineQueryParam, Depends()]
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
获取在线用户列表
|
||||
|
||||
|
||||
参数:
|
||||
- redis (Redis): Redis异步客户端实例。
|
||||
- paging_query (PaginationQueryParam): 分页查询参数模型。
|
||||
- search (OnlineQueryParam): 查询参数模型。
|
||||
|
||||
|
||||
返回:
|
||||
- JSONResponse: 包含在线用户列表的JSON响应。
|
||||
"""
|
||||
result_dict_list = await OnlineService.get_online_list_service(redis=redis, search=search)
|
||||
result_dict = await PaginationService.paginate(data_list= result_dict_list, page_no= paging_query.page_no, page_size = paging_query.page_size)
|
||||
result_dict = await PaginationService.paginate(data_list=result_dict_list, page_no=paging_query.page_no, page_size=paging_query.page_size)
|
||||
log.info('获取成功')
|
||||
|
||||
return SuccessResponse(data=result_dict,msg='获取成功')
|
||||
return SuccessResponse(data=result_dict, msg='获取成功')
|
||||
|
||||
|
||||
@OnlineRouter.delete(
|
||||
'/delete',
|
||||
'/delete',
|
||||
dependencies=[Depends(AuthPermission(['module_monitor:online:delete']))],
|
||||
summary="强制下线",
|
||||
description="强制下线"
|
||||
)
|
||||
async def delete_online_controller(
|
||||
session_id: str = Body(..., description="会话编号"),
|
||||
redis: Redis = Depends(redis_getter),
|
||||
)->JSONResponse:
|
||||
session_id: Annotated[str, Body(description="会话编号")],
|
||||
redis: Annotated[Redis, Depends(redis_getter)],
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
强制下线指定在线用户
|
||||
|
||||
|
||||
参数:
|
||||
- session_id (str): 在线用户会话ID。
|
||||
- redis (Redis): Redis异步客户端实例。
|
||||
|
||||
|
||||
返回:
|
||||
- JSONResponse: 包含操作结果的JSON响应。
|
||||
"""
|
||||
@@ -71,25 +70,25 @@ async def delete_online_controller(
|
||||
if is_ok:
|
||||
log.info("强制下线成功")
|
||||
return SuccessResponse(msg="强制下线成功")
|
||||
else:
|
||||
log.info("强制下线失败")
|
||||
return ErrorResponse(msg="强制下线失败")
|
||||
log.info("强制下线失败")
|
||||
return ErrorResponse(msg="强制下线失败")
|
||||
|
||||
|
||||
@OnlineRouter.delete(
|
||||
'/clear',
|
||||
'/clear',
|
||||
dependencies=[Depends(AuthPermission(['module_monitor:online:delete']))],
|
||||
summary="清除所有在线用户",
|
||||
description="清除所有在线用户"
|
||||
)
|
||||
async def clear_online_controller(
|
||||
redis: Redis = Depends(redis_getter),
|
||||
)->JSONResponse:
|
||||
redis: Annotated[Redis, Depends(redis_getter)],
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
清除所有在线用户
|
||||
|
||||
|
||||
参数:
|
||||
- redis (Redis): Redis异步客户端实例。
|
||||
|
||||
|
||||
返回:
|
||||
- JSONResponse: 包含操作结果的JSON响应。
|
||||
"""
|
||||
@@ -97,6 +96,5 @@ async def clear_online_controller(
|
||||
if is_ok:
|
||||
log.info("清除所有在线用户成功")
|
||||
return SuccessResponse(msg="清除所有在线用户成功")
|
||||
else:
|
||||
log.info("清除所有在线用户失败")
|
||||
return ErrorResponse(msg="清除所有在线用户失败")
|
||||
log.info("清除所有在线用户失败")
|
||||
return ErrorResponse(msg="清除所有在线用户失败")
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
from fastapi import Query
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
|
||||
from app.core.validator import DateTimeStr
|
||||
|
||||
@@ -30,11 +28,11 @@ class OnlineQueryParam:
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: str | None = Query(None, description="登录名称"),
|
||||
name: str | None = Query(None, description="登录名称"),
|
||||
ipaddr: str | None = Query(None, description="登陆IP地址"),
|
||||
login_location: str | None = Query(None, description="登录所属地"),
|
||||
) -> None:
|
||||
|
||||
|
||||
# 模糊查询字段
|
||||
self.name = ("like", f"%{name}%") if name else None
|
||||
self.login_location = ("like", f"%{login_location}%") if login_location else None
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import json
|
||||
|
||||
from redis.asyncio.client import Redis
|
||||
|
||||
from app.common.enums import RedisInitKeyConfig
|
||||
from app.core.logger import log
|
||||
from app.core.redis_crud import RedisCURD
|
||||
from app.core.security import decode_access_token
|
||||
from app.core.logger import log
|
||||
|
||||
from .schema import OnlineQueryParam
|
||||
|
||||
@@ -18,11 +17,11 @@ class OnlineService:
|
||||
async def get_online_list_service(cls, redis: Redis, search: OnlineQueryParam | None = None) -> list[dict]:
|
||||
"""
|
||||
获取在线用户列表信息(支持分页和搜索)
|
||||
|
||||
|
||||
参数:
|
||||
- redis (Redis): Redis异步客户端实例。
|
||||
- search (OnlineQueryParam | None): 查询参数模型。
|
||||
|
||||
|
||||
返回:
|
||||
- list[dict]: 在线用户详情字典列表。
|
||||
"""
|
||||
@@ -36,7 +35,7 @@ class OnlineService:
|
||||
continue
|
||||
try:
|
||||
payload = decode_access_token(token=token)
|
||||
session_info = json.loads(payload.sub)
|
||||
session_info = json.loads(payload.sub)
|
||||
if cls._match_search_conditions(session_info, search):
|
||||
online_users.append(session_info)
|
||||
except Exception as e:
|
||||
@@ -44,19 +43,18 @@ class OnlineService:
|
||||
continue
|
||||
# 按照 login_time 倒序排序
|
||||
online_users.sort(key=lambda x: x.get('login_time', ''), reverse=True)
|
||||
|
||||
return online_users
|
||||
|
||||
return online_users
|
||||
|
||||
@classmethod
|
||||
async def delete_online_service(cls, redis: Redis, session_id: str) -> bool:
|
||||
"""
|
||||
强制下线指定在线用户
|
||||
|
||||
|
||||
参数:
|
||||
- redis (Redis): Redis异步客户端实例。
|
||||
- session_id (str): 在线用户会话ID。
|
||||
|
||||
|
||||
返回:
|
||||
- bool: 如果操作成功则返回True,否则返回False。
|
||||
"""
|
||||
@@ -64,18 +62,17 @@ class OnlineService:
|
||||
await RedisCURD(redis).delete(f"{RedisInitKeyConfig.ACCESS_TOKEN.key}:{session_id}")
|
||||
await RedisCURD(redis).delete(f"{RedisInitKeyConfig.REFRESH_TOKEN.key}:{session_id}")
|
||||
|
||||
|
||||
log.info(f"强制下线用户会话: {session_id}")
|
||||
return True
|
||||
|
||||
|
||||
@classmethod
|
||||
async def clear_online_service(cls, redis: Redis) -> bool:
|
||||
"""
|
||||
强制下线所有在线用户
|
||||
|
||||
|
||||
参数:
|
||||
- redis (Redis): Redis异步客户端实例。
|
||||
|
||||
|
||||
返回:
|
||||
- bool: 如果操作成功则返回True,否则返回False。
|
||||
"""
|
||||
@@ -83,19 +80,18 @@ class OnlineService:
|
||||
await RedisCURD(redis).clear(f"{RedisInitKeyConfig.ACCESS_TOKEN.key}:*")
|
||||
await RedisCURD(redis).clear(f"{RedisInitKeyConfig.REFRESH_TOKEN.key}:*")
|
||||
|
||||
log.info(f"清除所有在线用户会话成功")
|
||||
log.info("清除所有在线用户会话成功")
|
||||
return True
|
||||
|
||||
|
||||
@staticmethod
|
||||
def _match_search_conditions(online_info: dict, search: OnlineQueryParam | None = None) -> bool:
|
||||
"""
|
||||
检查是否匹配搜索条件
|
||||
|
||||
|
||||
参数:
|
||||
- online_info (dict): 在线用户信息字典。
|
||||
- search (OnlineQueryParam | None): 查询参数模型。
|
||||
|
||||
|
||||
返回:
|
||||
- bool: 如果匹配则返回True,否则返回False。
|
||||
"""
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
@@ -1,84 +1,84 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import APIRouter, Body, Depends, Query, Request, UploadFile, Form
|
||||
from fastapi.responses import JSONResponse, StreamingResponse, FileResponse
|
||||
from fastapi import APIRouter, Body, Depends, Form, Query, Request, UploadFile
|
||||
from fastapi.responses import FileResponse, JSONResponse, StreamingResponse
|
||||
|
||||
from app.common.response import StreamResponse, SuccessResponse
|
||||
from app.common.request import PaginationService
|
||||
from app.core.router_class import OperationLogRoute
|
||||
from app.utils.common_util import bytes2file_response
|
||||
from app.common.response import StreamResponse, SuccessResponse
|
||||
from app.core.base_params import PaginationQueryParam
|
||||
from app.core.dependencies import AuthPermission
|
||||
from app.core.logger import log
|
||||
from app.core.router_class import OperationLogRoute
|
||||
from app.utils.common_util import bytes2file_response
|
||||
|
||||
from .service import ResourceService
|
||||
from .schema import (
|
||||
ResourceMoveSchema,
|
||||
ResourceCopySchema,
|
||||
ResourceRenameSchema,
|
||||
ResourceCreateDirSchema,
|
||||
ResourceSearchQueryParam
|
||||
ResourceMoveSchema,
|
||||
ResourceRenameSchema,
|
||||
ResourceSearchQueryParam,
|
||||
)
|
||||
|
||||
from .service import ResourceService
|
||||
|
||||
ResourceRouter = APIRouter(route_class=OperationLogRoute, prefix="/resource", tags=["资源管理"])
|
||||
|
||||
|
||||
@ResourceRouter.get(
|
||||
"/list",
|
||||
summary="获取目录列表",
|
||||
"/list",
|
||||
summary="获取目录列表",
|
||||
description="获取指定目录下的文件和子目录列表",
|
||||
dependencies=[Depends(AuthPermission(["module_monitor:resource:query"]))]
|
||||
)
|
||||
async def get_directory_list_controller(
|
||||
request: Request,
|
||||
page: PaginationQueryParam = Depends(),
|
||||
search: ResourceSearchQueryParam = Depends(),
|
||||
page: Annotated[PaginationQueryParam, Depends()],
|
||||
search: Annotated[ResourceSearchQueryParam, Depends()],
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
获取目录列表
|
||||
|
||||
|
||||
参数:
|
||||
- request (Request): FastAPI请求对象,用于获取基础URL。
|
||||
- page (PaginationQueryParam): 分页查询参数模型。
|
||||
- search (ResourceSearchQueryParam): 资源查询参数模型。
|
||||
|
||||
|
||||
返回:
|
||||
- JSONResponse: 包含目录列表的JSON响应。
|
||||
"""
|
||||
# 获取资源列表(与案例模块保持一致的分页实现)
|
||||
result_dict_list = await ResourceService.get_resources_list_service(
|
||||
search=search,
|
||||
search=search,
|
||||
base_url=str(request.base_url)
|
||||
)
|
||||
# 使用分页服务进行分页处理(与案例模块保持一致)
|
||||
result_dict = await PaginationService.paginate(
|
||||
data_list=result_dict_list,
|
||||
page_no=page.page_no,
|
||||
data_list=result_dict_list,
|
||||
page_no=page.page_no,
|
||||
page_size=page.page_size
|
||||
)
|
||||
|
||||
|
||||
log.info(f"获取目录列表成功: {getattr(search, 'name', None) or ''}")
|
||||
return SuccessResponse(data=result_dict, msg="获取目录列表成功")
|
||||
|
||||
|
||||
@ResourceRouter.post(
|
||||
"/upload",
|
||||
summary="上传文件",
|
||||
"/upload",
|
||||
summary="上传文件",
|
||||
description="上传文件到指定目录",
|
||||
dependencies=[Depends(AuthPermission(["module_monitor:resource:upload"]))])
|
||||
async def upload_file_controller(
|
||||
file: UploadFile,
|
||||
request: Request,
|
||||
target_path: str | None = Form(None, description="目标目录路径")
|
||||
target_path: Annotated[str | None, Form(description="目标目录路径")] = None
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
上传文件
|
||||
|
||||
|
||||
参数:
|
||||
- file (UploadFile): 要上传的文件对象。
|
||||
- request (Request): FastAPI请求对象,用于获取基础URL。
|
||||
- target_path (str | None): 目标目录路径,默认为None。
|
||||
|
||||
|
||||
返回:
|
||||
- JSONResponse: 包含上传文件信息的JSON响应。
|
||||
"""
|
||||
@@ -92,22 +92,22 @@ async def upload_file_controller(
|
||||
|
||||
|
||||
@ResourceRouter.get(
|
||||
"/download",
|
||||
summary="下载文件",
|
||||
"/download",
|
||||
summary="下载文件",
|
||||
description="下载指定文件",
|
||||
dependencies=[Depends(AuthPermission(["module_monitor:resource:download"]))]
|
||||
)
|
||||
async def download_file_controller(
|
||||
request: Request,
|
||||
path: str = Query(..., description="文件路径")
|
||||
path: Annotated[str, Query(description="文件路径")]
|
||||
) -> FileResponse:
|
||||
"""
|
||||
下载文件
|
||||
|
||||
|
||||
参数:
|
||||
- request (Request): FastAPI请求对象,用于获取基础URL。
|
||||
- path (str): 文件路径。
|
||||
|
||||
|
||||
返回:
|
||||
- FileResponse: 包含文件内容的文件响应。
|
||||
"""
|
||||
@@ -115,11 +115,11 @@ async def download_file_controller(
|
||||
file_path=path,
|
||||
base_url=str(request.base_url)
|
||||
)
|
||||
|
||||
|
||||
# 获取文件名
|
||||
import os
|
||||
filename = os.path.basename(file_path)
|
||||
|
||||
|
||||
log.info(f"下载文件成功: {filename}")
|
||||
return FileResponse(
|
||||
path=file_path,
|
||||
@@ -129,20 +129,20 @@ async def download_file_controller(
|
||||
|
||||
|
||||
@ResourceRouter.delete(
|
||||
"/delete",
|
||||
summary="删除文件",
|
||||
"/delete",
|
||||
summary="删除文件",
|
||||
description="删除指定文件或目录",
|
||||
dependencies=[Depends(AuthPermission(["module_monitor:resource:delete"]))]
|
||||
)
|
||||
async def delete_files_controller(
|
||||
paths: list[str] = Body(..., description="文件路径列表")
|
||||
paths: Annotated[list[str], Body(description="文件路径列表")]
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
删除文件
|
||||
|
||||
|
||||
参数:
|
||||
- paths (list[str]): 文件路径列表。
|
||||
|
||||
|
||||
返回:
|
||||
- JSONResponse: 包含删除结果的JSON响应。
|
||||
"""
|
||||
@@ -152,8 +152,8 @@ async def delete_files_controller(
|
||||
|
||||
|
||||
@ResourceRouter.post(
|
||||
"/move",
|
||||
summary="移动文件",
|
||||
"/move",
|
||||
summary="移动文件",
|
||||
description="移动文件或目录",
|
||||
dependencies=[Depends(AuthPermission(["module_monitor:resource:move"]))]
|
||||
)
|
||||
@@ -162,10 +162,10 @@ async def move_file_controller(
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
移动文件
|
||||
|
||||
|
||||
参数:
|
||||
- data (ResourceMoveSchema): 移动文件参数模型。
|
||||
|
||||
|
||||
返回:
|
||||
- JSONResponse: 包含移动结果的JSON响应。
|
||||
"""
|
||||
@@ -175,8 +175,8 @@ async def move_file_controller(
|
||||
|
||||
|
||||
@ResourceRouter.post(
|
||||
"/copy",
|
||||
summary="复制文件",
|
||||
"/copy",
|
||||
summary="复制文件",
|
||||
description="复制文件或目录",
|
||||
dependencies=[Depends(AuthPermission(["module_monitor:resource:copy"]))]
|
||||
)
|
||||
@@ -185,10 +185,10 @@ async def copy_file_controller(
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
复制文件
|
||||
|
||||
|
||||
参数:
|
||||
- data (ResourceCopySchema): 复制文件参数模型。
|
||||
|
||||
|
||||
返回:
|
||||
- JSONResponse: 包含复制结果的JSON响应。
|
||||
"""
|
||||
@@ -198,8 +198,8 @@ async def copy_file_controller(
|
||||
|
||||
|
||||
@ResourceRouter.post(
|
||||
"/rename",
|
||||
summary="重命名文件",
|
||||
"/rename",
|
||||
summary="重命名文件",
|
||||
description="重命名文件或目录",
|
||||
dependencies=[Depends(AuthPermission(["module_monitor:resource:rename"]))]
|
||||
)
|
||||
@@ -208,10 +208,10 @@ async def rename_file_controller(
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
重命名文件
|
||||
|
||||
|
||||
参数:
|
||||
- data (ResourceRenameSchema): 重命名文件参数模型。
|
||||
|
||||
|
||||
返回:
|
||||
- JSONResponse: 包含重命名结果的JSON响应。
|
||||
"""
|
||||
@@ -221,8 +221,8 @@ async def rename_file_controller(
|
||||
|
||||
|
||||
@ResourceRouter.post(
|
||||
"/create-dir",
|
||||
summary="创建目录",
|
||||
"/create-dir",
|
||||
summary="创建目录",
|
||||
description="在指定路径创建新目录",
|
||||
dependencies=[Depends(AuthPermission(["module_monitor:resource:create_dir"]))]
|
||||
)
|
||||
@@ -231,10 +231,10 @@ async def create_directory_controller(
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
创建目录
|
||||
|
||||
|
||||
参数:
|
||||
- data (ResourceCreateDirSchema): 创建目录参数模型。
|
||||
|
||||
|
||||
返回:
|
||||
- JSONResponse: 包含创建目录结果的JSON响应。
|
||||
"""
|
||||
@@ -244,22 +244,22 @@ async def create_directory_controller(
|
||||
|
||||
|
||||
@ResourceRouter.post(
|
||||
"/export",
|
||||
summary="导出资源列表",
|
||||
"/export",
|
||||
summary="导出资源列表",
|
||||
description="导出资源列表",
|
||||
dependencies=[Depends(AuthPermission(["module_monitor:resource:export"]))]
|
||||
)
|
||||
async def export_resource_list_controller(
|
||||
request: Request,
|
||||
search: ResourceSearchQueryParam = Depends()
|
||||
search: Annotated[ResourceSearchQueryParam, Depends()]
|
||||
) -> StreamingResponse:
|
||||
"""
|
||||
导出资源列表
|
||||
|
||||
|
||||
参数:
|
||||
- request (Request): FastAPI请求对象,用于获取基础URL。
|
||||
- search (ResourceSearchQueryParam): 资源查询参数模型。
|
||||
|
||||
|
||||
返回:
|
||||
- StreamingResponse: 包含导出资源列表的流式响应。
|
||||
"""
|
||||
@@ -269,7 +269,7 @@ async def export_resource_list_controller(
|
||||
base_url=str(request.base_url)
|
||||
)
|
||||
export_result = await ResourceService.export_resource_service(data_list=result_dict_list)
|
||||
|
||||
|
||||
log.info("导出资源列表成功")
|
||||
return StreamResponse(
|
||||
data=bytes2file_response(export_result),
|
||||
@@ -277,4 +277,4 @@ async def export_resource_list_controller(
|
||||
headers={
|
||||
'Content-Disposition': 'attachment; filename=resource_list.xlsx'
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from datetime import datetime
|
||||
from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from fastapi import Query
|
||||
from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
|
||||
|
||||
|
||||
class ResourceItemSchema(BaseModel):
|
||||
"""资源项目模型"""
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
name: str = Field(..., description="文件名")
|
||||
file_url: str = Field(..., description="文件URL路径")
|
||||
relative_path: str = Field(..., description="相对路径")
|
||||
@@ -51,7 +50,7 @@ class ResourceItemSchema(BaseModel):
|
||||
class ResourceDirectorySchema(BaseModel):
|
||||
"""资源目录模型"""
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
path: str = Field(..., description="目录路径")
|
||||
name: str = Field(..., description="目录名称")
|
||||
items: list[ResourceItemSchema] = Field(default_factory=list, description="目录项")
|
||||
@@ -63,7 +62,7 @@ class ResourceDirectorySchema(BaseModel):
|
||||
class ResourceUploadSchema(BaseModel):
|
||||
"""资源上传响应模型"""
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
filename: str = Field(..., description="文件名")
|
||||
file_url: str = Field(..., description="访问URL")
|
||||
file_size: int = Field(..., description="文件大小")
|
||||
@@ -73,11 +72,11 @@ class ResourceUploadSchema(BaseModel):
|
||||
class ResourceMoveSchema(BaseModel):
|
||||
"""资源移动模型"""
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
source_path: str = Field(..., description="源路径")
|
||||
target_path: str = Field(..., description="目标路径")
|
||||
overwrite: bool = Field(False, description="是否覆盖")
|
||||
|
||||
|
||||
@field_validator('source_path', 'target_path')
|
||||
@classmethod
|
||||
def validate_paths(cls, value: str):
|
||||
@@ -88,16 +87,15 @@ class ResourceMoveSchema(BaseModel):
|
||||
|
||||
class ResourceCopySchema(ResourceMoveSchema):
|
||||
"""资源复制模型"""
|
||||
pass
|
||||
|
||||
|
||||
class ResourceRenameSchema(BaseModel):
|
||||
"""资源重命名模型"""
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
old_path: str = Field(..., description="原路径")
|
||||
new_name: str = Field(..., description="新名称")
|
||||
|
||||
|
||||
@field_validator('old_path', 'new_name')
|
||||
@classmethod
|
||||
def validate_inputs(cls, value: str):
|
||||
@@ -117,10 +115,10 @@ class ResourceRenameSchema(BaseModel):
|
||||
class ResourceCreateDirSchema(BaseModel):
|
||||
"""创建目录模型"""
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
parent_path: str = Field(..., description="父目录路径")
|
||||
dir_name: str = Field(..., description="目录名称", max_length=255)
|
||||
|
||||
|
||||
@field_validator('parent_path', 'dir_name')
|
||||
@classmethod
|
||||
def validate_inputs(cls, value: str, info):
|
||||
@@ -135,7 +133,7 @@ class ResourceCreateDirSchema(BaseModel):
|
||||
else: # 对于dir_name仍然严格检查
|
||||
if not value or len(value.strip()) == 0:
|
||||
raise ValueError("参数不能为空")
|
||||
if '..' in value or value.startswith('/') or value.startswith('\\'):
|
||||
if '..' in value or value.startswith(('/', '\\')):
|
||||
raise ValueError("参数包含不安全字符")
|
||||
return value.strip()
|
||||
|
||||
@@ -148,9 +146,9 @@ class ResourceSearchQueryParam:
|
||||
name: str | None = Query(None, description="搜索关键词"),
|
||||
path: str | None = Query(None, description="目录路径"),
|
||||
) -> None:
|
||||
|
||||
|
||||
# 模糊查询字段
|
||||
self.name = ("like", name) if name else None
|
||||
|
||||
|
||||
# 精确查询字段
|
||||
self.path = path
|
||||
self.path = path
|
||||
|
||||
@@ -1,26 +1,26 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import os
|
||||
import shutil
|
||||
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from fastapi import UploadFile
|
||||
|
||||
from app.config.setting import settings
|
||||
from app.core.exceptions import CustomException
|
||||
from app.core.logger import log
|
||||
from app.utils.excel_util import ExcelUtil
|
||||
from app.config.setting import settings
|
||||
|
||||
from .schema import (
|
||||
ResourceItemSchema,
|
||||
ResourceDirectorySchema,
|
||||
ResourceUploadSchema,
|
||||
ResourceMoveSchema,
|
||||
ResourceCopySchema,
|
||||
ResourceRenameSchema,
|
||||
ResourceCreateDirSchema,
|
||||
ResourceSearchQueryParam
|
||||
ResourceDirectorySchema,
|
||||
ResourceItemSchema,
|
||||
ResourceMoveSchema,
|
||||
ResourceRenameSchema,
|
||||
ResourceSearchQueryParam,
|
||||
ResourceUploadSchema,
|
||||
)
|
||||
|
||||
|
||||
@@ -28,90 +28,87 @@ class ResourceService:
|
||||
"""
|
||||
资源管理模块服务层 - 管理系统静态文件目录
|
||||
"""
|
||||
|
||||
|
||||
# 配置常量
|
||||
MAX_UPLOAD_SIZE = 100 * 1024 * 1024 # 100MB
|
||||
MAX_SEARCH_RESULTS = 1000 # 最大搜索结果数
|
||||
MAX_PATH_DEPTH = 20 # 最大路径深度
|
||||
|
||||
|
||||
@classmethod
|
||||
def _get_resource_root(cls) -> str:
|
||||
"""
|
||||
获取资源管理根目录
|
||||
|
||||
|
||||
返回:
|
||||
- str: 资源管理根目录路径。
|
||||
"""
|
||||
if not settings.STATIC_ENABLE:
|
||||
raise CustomException(msg='静态文件服务未启用')
|
||||
return str(settings.STATIC_ROOT)
|
||||
|
||||
|
||||
@classmethod
|
||||
def _get_safe_path(cls, path: str | None = None) -> str:
|
||||
"""
|
||||
获取安全的文件路径
|
||||
|
||||
|
||||
参数:
|
||||
- path (str | None): 原始文件路径。
|
||||
|
||||
|
||||
返回:
|
||||
- str: 安全的文件路径。
|
||||
"""
|
||||
resource_root = cls._get_resource_root()
|
||||
|
||||
|
||||
if not path:
|
||||
return resource_root
|
||||
|
||||
|
||||
# 支持前端传递的完整URL或以STATIC_URL/ROOT_PATH+STATIC_URL开头的URL路径,转换为相对资源路径
|
||||
if isinstance(path, str):
|
||||
static_prefix = settings.STATIC_URL.rstrip('/')
|
||||
root_prefix = settings.ROOT_PATH.rstrip('/') if getattr(settings, 'ROOT_PATH', '') else ''
|
||||
root_static_prefix = f"{root_prefix}{static_prefix}" if root_prefix else static_prefix
|
||||
|
||||
|
||||
def strip_prefix(p: str) -> str:
|
||||
if p.startswith(root_static_prefix):
|
||||
return p[len(root_static_prefix):].lstrip('/')
|
||||
if p.startswith(static_prefix):
|
||||
return p[len(static_prefix):].lstrip('/')
|
||||
return p
|
||||
|
||||
if path.startswith('http://') or path.startswith('https://'):
|
||||
|
||||
if path.startswith(('http://', 'https://')):
|
||||
parsed = urlparse(path)
|
||||
url_path = parsed.path or ''
|
||||
path = strip_prefix(url_path)
|
||||
else:
|
||||
path = strip_prefix(path)
|
||||
|
||||
|
||||
# 清理路径,移除危险字符
|
||||
path = path.strip().replace('..', '').replace('//', '/')
|
||||
|
||||
|
||||
# 规范化路径
|
||||
if os.path.isabs(path):
|
||||
safe_path = os.path.normpath(path)
|
||||
else:
|
||||
safe_path = os.path.normpath(os.path.join(resource_root, path))
|
||||
|
||||
safe_path = os.path.normpath(path) if os.path.isabs(path) else os.path.normpath(os.path.join(resource_root, path))
|
||||
|
||||
# 检查路径是否在允许的范围内
|
||||
resource_root_abs = os.path.normpath(os.path.abspath(resource_root))
|
||||
safe_path_abs = os.path.normpath(os.path.abspath(safe_path))
|
||||
|
||||
|
||||
if not safe_path_abs.startswith(resource_root_abs):
|
||||
raise CustomException(msg=f'访问路径不在允许范围内: {path}')
|
||||
|
||||
|
||||
# 防止路径遍历攻击
|
||||
if '..' in safe_path or safe_path.count('/') > cls.MAX_PATH_DEPTH:
|
||||
raise CustomException(msg=f'不安全的路径格式: {path}')
|
||||
|
||||
|
||||
return safe_path
|
||||
|
||||
|
||||
@classmethod
|
||||
def _path_exists(cls, path: str) -> bool:
|
||||
"""
|
||||
检查路径是否存在
|
||||
|
||||
|
||||
参数:
|
||||
- path (str): 要检查的路径。
|
||||
|
||||
|
||||
返回:
|
||||
- bool: 如果路径存在则返回True,否则返回False。
|
||||
"""
|
||||
@@ -120,16 +117,16 @@ class ResourceService:
|
||||
return os.path.exists(safe_path)
|
||||
except:
|
||||
return False
|
||||
|
||||
|
||||
@classmethod
|
||||
def _generate_http_url(cls, file_path: str, base_url: str | None = None) -> str:
|
||||
"""
|
||||
生成文件的HTTP URL
|
||||
|
||||
|
||||
参数:
|
||||
- file_path (str): 文件的绝对路径。
|
||||
- base_url (str | None): 基础URL,用于生成完整URL。
|
||||
|
||||
|
||||
返回:
|
||||
- str: 文件的HTTP URL。
|
||||
"""
|
||||
@@ -141,7 +138,7 @@ class ResourceService:
|
||||
except ValueError:
|
||||
# 如果无法计算相对路径,使用文件名
|
||||
url_path = os.path.basename(file_path)
|
||||
|
||||
|
||||
# 如果提供了base_url,使用它生成完整URL,否则使用settings.STATIC_URL
|
||||
if base_url:
|
||||
# 修复URL生成逻辑
|
||||
@@ -153,18 +150,18 @@ class ResourceService:
|
||||
http_url = f"{base_part}{static_part}/{file_part}".replace('//', '/').replace(':/', '://')
|
||||
else:
|
||||
http_url = f"{settings.STATIC_URL}/{url_path}".replace('//', '/')
|
||||
|
||||
|
||||
return http_url
|
||||
|
||||
|
||||
@classmethod
|
||||
def _get_file_info(cls, file_path: str, base_url: str | None = None) -> dict:
|
||||
"""
|
||||
获取文件或目录的详细信息,如名称、大小、创建时间、修改时间、路径、深度、HTTP URL、是否隐藏、是否为目录等。
|
||||
|
||||
|
||||
参数:
|
||||
- file_path (str): 文件或目录的路径。
|
||||
- base_url (str | None): 基础URL,用于生成完整URL。
|
||||
|
||||
|
||||
返回:
|
||||
- dict: 文件或目录的详细信息字典。
|
||||
"""
|
||||
@@ -172,36 +169,36 @@ class ResourceService:
|
||||
safe_path = cls._get_safe_path(file_path)
|
||||
if not os.path.exists(safe_path):
|
||||
return {}
|
||||
|
||||
|
||||
stat = os.stat(safe_path)
|
||||
path_obj = Path(safe_path)
|
||||
resource_root = cls._get_resource_root()
|
||||
|
||||
|
||||
# 计算相对路径
|
||||
try:
|
||||
relative_path = os.path.relpath(safe_path, resource_root)
|
||||
except ValueError:
|
||||
relative_path = os.path.basename(safe_path)
|
||||
|
||||
|
||||
# 计算深度
|
||||
try:
|
||||
depth = len(Path(safe_path).relative_to(resource_root).parts)
|
||||
len(Path(safe_path).relative_to(resource_root).parts)
|
||||
except ValueError:
|
||||
depth = 0
|
||||
|
||||
pass
|
||||
|
||||
# 生成HTTP URL路径而不是文件系统路径
|
||||
http_url = cls._generate_http_url(safe_path, base_url)
|
||||
|
||||
|
||||
# 检查是否为隐藏文件(文件名以点开头)
|
||||
is_hidden = path_obj.name.startswith('.')
|
||||
|
||||
|
||||
# 对于目录,设置is_directory字段(兼容前端)
|
||||
is_directory = os.path.isdir(safe_path)
|
||||
|
||||
|
||||
# 将datetime对象转换为ISO格式的字符串,确保JSON序列化成功
|
||||
created_time = datetime.fromtimestamp(stat.st_ctime).isoformat()
|
||||
modified_time = datetime.fromtimestamp(stat.st_mtime).isoformat()
|
||||
|
||||
|
||||
return {
|
||||
'name': path_obj.name,
|
||||
'file_url': http_url, # 统一使用file_url字段
|
||||
@@ -214,19 +211,19 @@ class ResourceService:
|
||||
'is_hidden': is_hidden
|
||||
}
|
||||
except Exception as e:
|
||||
log.error(f'获取文件信息失败: {str(e)}')
|
||||
log.error(f'获取文件信息失败: {e!s}')
|
||||
return {}
|
||||
|
||||
|
||||
@classmethod
|
||||
async def get_directory_list_service(cls, path: str | None = None, include_hidden: bool = False, base_url: str | None = None) -> dict:
|
||||
"""
|
||||
获取目录列表
|
||||
|
||||
|
||||
参数:
|
||||
- path (str | None): 目录路径。如果未指定,将使用静态文件根目录。
|
||||
- include_hidden (bool): 是否包含隐藏文件。
|
||||
- base_url (str | None): 基础URL,用于生成完整URL。
|
||||
|
||||
|
||||
返回:
|
||||
- dict: 包含目录列表和统计信息的字典。
|
||||
"""
|
||||
@@ -238,39 +235,39 @@ class ResourceService:
|
||||
else:
|
||||
safe_path = cls._get_safe_path(path)
|
||||
display_path = cls._generate_http_url(safe_path, base_url)
|
||||
|
||||
|
||||
if not os.path.exists(safe_path):
|
||||
raise CustomException(msg='目录不存在')
|
||||
|
||||
|
||||
if not os.path.isdir(safe_path):
|
||||
raise CustomException(msg='路径不是目录')
|
||||
|
||||
|
||||
items = []
|
||||
total_files = 0
|
||||
total_dirs = 0
|
||||
total_size = 0
|
||||
|
||||
|
||||
try:
|
||||
for item_name in os.listdir(safe_path):
|
||||
# 跳过隐藏文件
|
||||
if not include_hidden and item_name.startswith('.'):
|
||||
continue
|
||||
|
||||
|
||||
item_path = os.path.join(safe_path, item_name)
|
||||
file_info = cls._get_file_info(item_path, base_url)
|
||||
|
||||
|
||||
if file_info:
|
||||
items.append(ResourceItemSchema(**file_info))
|
||||
|
||||
|
||||
if file_info['is_file']:
|
||||
total_files += 1
|
||||
total_size += file_info.get('size', 0) or 0
|
||||
elif file_info['is_dir']:
|
||||
total_dirs += 1
|
||||
|
||||
|
||||
except PermissionError:
|
||||
raise CustomException(msg='没有权限访问此目录')
|
||||
|
||||
|
||||
return ResourceDirectorySchema(
|
||||
path=display_path, # 返回HTTP URL路径而不是文件系统路径
|
||||
name=os.path.basename(safe_path),
|
||||
@@ -279,85 +276,82 @@ class ResourceService:
|
||||
total_dirs=total_dirs,
|
||||
total_size=total_size
|
||||
).model_dump()
|
||||
|
||||
|
||||
except CustomException:
|
||||
raise
|
||||
except Exception as e:
|
||||
log.error(f'获取目录列表失败: {str(e)}')
|
||||
raise CustomException(msg=f'获取目录列表失败: {str(e)}')
|
||||
log.error(f'获取目录列表失败: {e!s}')
|
||||
raise CustomException(msg=f'获取目录列表失败: {e!s}')
|
||||
|
||||
@classmethod
|
||||
async def get_resources_list_service(cls, search: ResourceSearchQueryParam | None = None, order_by: str | None = None, base_url: str | None = None) -> list[dict]:
|
||||
"""
|
||||
搜索资源列表(用于分页和导出)
|
||||
|
||||
|
||||
参数:
|
||||
- search (ResourceSearchQueryParam | None): 查询参数模型。
|
||||
- order_by (str | None): 排序参数。
|
||||
- base_url (str | None): 基础URL,用于生成完整URL。
|
||||
|
||||
|
||||
返回:
|
||||
- list[dict]: 资源详情字典列表。
|
||||
"""
|
||||
try:
|
||||
# 确定搜索路径
|
||||
if search and hasattr(search, 'path') and search.path:
|
||||
resource_root = cls._get_safe_path(search.path)
|
||||
else:
|
||||
resource_root = cls._get_resource_root()
|
||||
|
||||
resource_root = cls._get_safe_path(search.path) if search and hasattr(search, 'path') and search.path else cls._get_resource_root()
|
||||
|
||||
# 检查路径是否存在
|
||||
if not os.path.exists(resource_root):
|
||||
raise CustomException(msg='目录不存在')
|
||||
|
||||
|
||||
if not os.path.isdir(resource_root):
|
||||
raise CustomException(msg='路径不是目录')
|
||||
|
||||
|
||||
# 收集资源
|
||||
all_resources = []
|
||||
|
||||
|
||||
try:
|
||||
for item_name in os.listdir(resource_root):
|
||||
# 跳过隐藏文件
|
||||
if item_name.startswith('.'):
|
||||
continue
|
||||
|
||||
|
||||
item_path = os.path.join(resource_root, item_name)
|
||||
file_info = cls._get_file_info(item_path, base_url)
|
||||
|
||||
|
||||
if file_info:
|
||||
# 应用名称过滤
|
||||
if search and hasattr(search, 'name') and search.name and search.name[1]:
|
||||
search_keyword = search.name[1].lower()
|
||||
if search_keyword not in file_info.get('name', '').lower():
|
||||
continue
|
||||
|
||||
|
||||
all_resources.append(file_info)
|
||||
|
||||
|
||||
except PermissionError:
|
||||
raise CustomException(msg='没有权限访问此目录')
|
||||
|
||||
|
||||
# 应用排序
|
||||
sorted_resources = cls._sort_results(all_resources, order_by)
|
||||
|
||||
|
||||
# 限制最大结果数
|
||||
if len(sorted_resources) > cls.MAX_SEARCH_RESULTS:
|
||||
sorted_resources = sorted_resources[:cls.MAX_SEARCH_RESULTS]
|
||||
|
||||
|
||||
return sorted_resources
|
||||
|
||||
|
||||
except Exception as e:
|
||||
log.error(f'搜索资源失败: {str(e)}')
|
||||
raise CustomException(msg=f'搜索资源失败: {str(e)}')
|
||||
log.error(f'搜索资源失败: {e!s}')
|
||||
raise CustomException(msg=f'搜索资源失败: {e!s}')
|
||||
|
||||
@classmethod
|
||||
async def export_resource_service(cls, data_list: list[dict]) -> bytes:
|
||||
"""
|
||||
导出资源列表
|
||||
|
||||
|
||||
参数:
|
||||
- data_list (list[dict]): 资源详情字典列表。
|
||||
|
||||
|
||||
返回:
|
||||
- bytes: Excel文件的二进制数据。
|
||||
"""
|
||||
@@ -372,7 +366,7 @@ class ResourceService:
|
||||
|
||||
# 复制数据并转换状态
|
||||
export_data = data_list.copy()
|
||||
|
||||
|
||||
# 格式化文件大小
|
||||
for item in export_data:
|
||||
if item.get('size'):
|
||||
@@ -384,47 +378,47 @@ class ResourceService:
|
||||
async def _get_directory_stats(cls, path: str, include_hidden: bool = False) -> dict[str, int]:
|
||||
"""
|
||||
递归获取目录统计信息
|
||||
|
||||
|
||||
参数:
|
||||
- path (str): 目录路径。
|
||||
- include_hidden (bool): 是否包含隐藏文件。
|
||||
|
||||
|
||||
返回:
|
||||
- dict[str, int]: 包含文件数、目录数和总大小的字典。
|
||||
"""
|
||||
stats = {'files': 0, 'dirs': 0, 'size': 0}
|
||||
|
||||
|
||||
try:
|
||||
for root, dirs, files in os.walk(path):
|
||||
# 过滤隐藏目录
|
||||
if not include_hidden:
|
||||
dirs[:] = [d for d in dirs if not d.startswith('.')]
|
||||
files = [f for f in files if not f.startswith('.')]
|
||||
|
||||
|
||||
stats['dirs'] += len(dirs)
|
||||
stats['files'] += len(files)
|
||||
|
||||
|
||||
for file in files:
|
||||
file_path = os.path.join(root, file)
|
||||
try:
|
||||
stats['size'] += os.path.getsize(file_path)
|
||||
except (OSError, IOError):
|
||||
except OSError:
|
||||
continue
|
||||
|
||||
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
return stats
|
||||
|
||||
|
||||
@classmethod
|
||||
def _sort_results(cls, results: list[dict], order_by: str | None = None) -> list[dict]:
|
||||
"""
|
||||
排序搜索结果
|
||||
|
||||
|
||||
参数:
|
||||
- results (list[dict]): 资源详情字典列表。
|
||||
- order_by (str | None): 排序参数。
|
||||
|
||||
|
||||
返回:
|
||||
- list[dict]: 排序后的资源详情字典列表。
|
||||
"""
|
||||
@@ -432,7 +426,7 @@ class ResourceService:
|
||||
# 默认按名称升序排序
|
||||
if not order_by:
|
||||
return sorted(results, key=lambda x: x.get('name', ''), reverse=False)
|
||||
|
||||
|
||||
# 解析order_by参数,格式: [{'field':'asc/desc'}]
|
||||
try:
|
||||
sort_conditions = eval(order_by)
|
||||
@@ -442,7 +436,7 @@ class ResourceService:
|
||||
keys = []
|
||||
for cond in sort_conditions:
|
||||
field = cond.get('field', 'name')
|
||||
direction = cond.get('direction', 'asc')
|
||||
cond.get('direction', 'asc')
|
||||
# 获取字段值,默认为空字符串
|
||||
value = item.get(field, '')
|
||||
# 如果是日期字段,转换为可比较的格式
|
||||
@@ -450,61 +444,58 @@ class ResourceService:
|
||||
value = datetime.fromisoformat(value)
|
||||
keys.append(value)
|
||||
return keys
|
||||
|
||||
|
||||
# 确定排序方向(这里只支持单一方向,多个条件时使用第一个条件的方向)
|
||||
reverse = False
|
||||
if sort_conditions and isinstance(sort_conditions[0], dict):
|
||||
direction = sort_conditions[0].get('direction', '').lower()
|
||||
reverse = direction == 'desc'
|
||||
|
||||
|
||||
return sorted(results, key=sort_key, reverse=reverse)
|
||||
except:
|
||||
# 如果解析失败,使用默认排序
|
||||
pass
|
||||
|
||||
|
||||
return sorted(results, key=lambda x: x.get('name', ''), reverse=False)
|
||||
except:
|
||||
return results
|
||||
|
||||
@classmethod
|
||||
async def upload_file_service(cls, file: UploadFile, target_path: str | None = None, base_url: str | None = None) -> dict:
|
||||
async def upload_file_service(cls, file: UploadFile, target_path: str | None = None, base_url: str | None = None) -> dict:
|
||||
"""
|
||||
上传文件到指定目录
|
||||
|
||||
|
||||
参数:
|
||||
- file (UploadFile): 上传的文件对象。
|
||||
- target_path (str | None): 目标目录路径。
|
||||
- base_url (str | None): 基础URL,用于生成完整URL。
|
||||
|
||||
|
||||
返回:
|
||||
- dict: 包含文件信息的字典。
|
||||
"""
|
||||
if not file or not file.filename:
|
||||
raise CustomException(msg="请选择要上传的文件")
|
||||
|
||||
|
||||
# 文件名安全检查
|
||||
if '..' in file.filename or '/' in file.filename or '\\' in file.filename:
|
||||
raise CustomException(msg="文件名包含不安全字符")
|
||||
|
||||
|
||||
try:
|
||||
# 检查文件大小
|
||||
content = await file.read()
|
||||
if len(content) > cls.MAX_UPLOAD_SIZE:
|
||||
raise CustomException(msg=f"文件太大,最大支持{cls.MAX_UPLOAD_SIZE // (1024*1024)}MB")
|
||||
|
||||
raise CustomException(msg=f"文件太大,最大支持{cls.MAX_UPLOAD_SIZE // (1024 * 1024)}MB")
|
||||
|
||||
# 确定上传目录,如果没有指定目标路径,使用静态文件根目录
|
||||
if target_path is None:
|
||||
safe_dir = cls._get_resource_root()
|
||||
else:
|
||||
safe_dir = cls._get_safe_path(target_path)
|
||||
|
||||
safe_dir = cls._get_resource_root() if target_path is None else cls._get_safe_path(target_path)
|
||||
|
||||
# 创建目录(如果不存在)
|
||||
os.makedirs(safe_dir, exist_ok=True)
|
||||
|
||||
|
||||
# 生成文件路径
|
||||
filename = file.filename
|
||||
file_path = os.path.join(safe_dir, filename)
|
||||
|
||||
|
||||
# 检查文件是否已存在
|
||||
if os.path.exists(file_path):
|
||||
# 生成唯一文件名
|
||||
@@ -515,119 +506,118 @@ class ResourceService:
|
||||
file_path = os.path.join(safe_dir, new_filename)
|
||||
counter += 1
|
||||
filename = os.path.basename(file_path)
|
||||
|
||||
|
||||
# 保存文件(使用已读取的内容)
|
||||
with open(file_path, 'wb') as f:
|
||||
f.write(content)
|
||||
|
||||
Path(file_path).write_bytes(content)
|
||||
|
||||
# 获取文件信息
|
||||
file_info = cls._get_file_info(file_path, base_url)
|
||||
|
||||
|
||||
# 生成文件URL
|
||||
file_url = cls._generate_http_url(file_path, base_url)
|
||||
|
||||
|
||||
log.info(f"文件上传成功: {filename}")
|
||||
|
||||
|
||||
return ResourceUploadSchema(
|
||||
filename=filename,
|
||||
file_url=file_url,
|
||||
file_size=file_info.get('size', 0),
|
||||
upload_time=datetime.now()
|
||||
).model_dump(mode='json')
|
||||
|
||||
|
||||
except Exception as e:
|
||||
log.error(f"文件上传失败: {str(e)}")
|
||||
raise CustomException(msg=f"文件上传失败: {str(e)}")
|
||||
log.error(f"文件上传失败: {e!s}")
|
||||
raise CustomException(msg=f"文件上传失败: {e!s}")
|
||||
|
||||
@classmethod
|
||||
async def download_file_service(cls, file_path: str, base_url: str | None = None) -> str:
|
||||
"""
|
||||
下载文件(返回本地文件系统路径)
|
||||
|
||||
|
||||
参数:
|
||||
- file_path (str): 文件路径(可为相对路径、绝对路径或完整URL)。
|
||||
- base_url (str | None): 基础URL,用于生成完整URL(不再直接返回URL)。
|
||||
|
||||
|
||||
返回:
|
||||
- str: 本地文件系统路径。
|
||||
"""
|
||||
try:
|
||||
safe_path = cls._get_safe_path(file_path)
|
||||
|
||||
|
||||
if not os.path.exists(safe_path):
|
||||
raise CustomException(msg='文件不存在')
|
||||
|
||||
|
||||
if not os.path.isfile(safe_path):
|
||||
raise CustomException(msg='路径不是文件')
|
||||
|
||||
|
||||
# 返回本地文件路径给 FileResponse 使用
|
||||
log.info(f"定位文件路径: {safe_path}")
|
||||
return safe_path
|
||||
|
||||
|
||||
except CustomException:
|
||||
raise
|
||||
except Exception as e:
|
||||
log.error(f"下载文件失败: {str(e)}")
|
||||
raise CustomException(msg=f"下载文件失败: {str(e)}")
|
||||
log.error(f"下载文件失败: {e!s}")
|
||||
raise CustomException(msg=f"下载文件失败: {e!s}")
|
||||
|
||||
@classmethod
|
||||
async def delete_file_service(cls, paths: list[str]) -> None:
|
||||
"""
|
||||
删除文件或目录
|
||||
|
||||
|
||||
参数:
|
||||
- paths (list[str]): 文件或目录路径列表。
|
||||
|
||||
|
||||
返回:
|
||||
- None
|
||||
"""
|
||||
if not paths:
|
||||
raise CustomException(msg='删除失败,删除路径不能为空')
|
||||
|
||||
|
||||
for path in paths:
|
||||
try:
|
||||
safe_path = cls._get_safe_path(path)
|
||||
|
||||
|
||||
if not os.path.exists(safe_path):
|
||||
log.error(f"路径不存在,跳过: {path}")
|
||||
continue
|
||||
|
||||
|
||||
if os.path.isfile(safe_path):
|
||||
os.remove(safe_path)
|
||||
log.info(f"删除文件成功: {safe_path}")
|
||||
elif os.path.isdir(safe_path):
|
||||
shutil.rmtree(safe_path)
|
||||
log.info(f"删除目录成功: {safe_path}")
|
||||
|
||||
|
||||
except Exception as e:
|
||||
log.error(f"删除失败 {path}: {str(e)}")
|
||||
raise CustomException(msg=f"删除失败 {path}: {str(e)}")
|
||||
log.error(f"删除失败 {path}: {e!s}")
|
||||
raise CustomException(msg=f"删除失败 {path}: {e!s}")
|
||||
|
||||
@classmethod
|
||||
async def batch_delete_service(cls, paths: list[str]) -> dict[str, list[str]]:
|
||||
"""
|
||||
批量删除文件或目录
|
||||
|
||||
|
||||
参数:
|
||||
- paths (List[str]): 文件或目录路径列表。
|
||||
|
||||
|
||||
返回:
|
||||
- Dict[str, List[str]]: 包含成功删除路径和失败删除路径的字典。
|
||||
"""
|
||||
if not paths:
|
||||
raise CustomException(msg='删除失败,删除路径不能为空')
|
||||
|
||||
|
||||
success_paths = []
|
||||
failed_paths = []
|
||||
|
||||
|
||||
for path in paths:
|
||||
try:
|
||||
safe_path = cls._get_safe_path(path)
|
||||
|
||||
|
||||
if not os.path.exists(safe_path):
|
||||
failed_paths.append(path)
|
||||
continue
|
||||
|
||||
|
||||
if os.path.isfile(safe_path):
|
||||
os.remove(safe_path)
|
||||
success_paths.append(path)
|
||||
@@ -636,11 +626,11 @@ class ResourceService:
|
||||
shutil.rmtree(safe_path)
|
||||
success_paths.append(path)
|
||||
log.info(f"删除目录成功: {safe_path}")
|
||||
|
||||
|
||||
except Exception as e:
|
||||
log.error(f"删除失败 {path}: {str(e)}")
|
||||
log.error(f"删除失败 {path}: {e!s}")
|
||||
failed_paths.append(path)
|
||||
|
||||
|
||||
return {
|
||||
"success": success_paths,
|
||||
"failed": failed_paths
|
||||
@@ -650,177 +640,176 @@ class ResourceService:
|
||||
async def move_file_service(cls, data: ResourceMoveSchema) -> None:
|
||||
"""
|
||||
移动文件或目录
|
||||
|
||||
|
||||
参数:
|
||||
- data (ResourceMoveSchema): 包含源路径和目标路径的模型。
|
||||
|
||||
|
||||
返回:
|
||||
- None
|
||||
"""
|
||||
try:
|
||||
source_path = cls._get_safe_path(data.source_path)
|
||||
target_path = cls._get_safe_path(data.target_path)
|
||||
|
||||
|
||||
if not os.path.exists(source_path):
|
||||
raise CustomException(msg='源路径不存在')
|
||||
|
||||
|
||||
# 检查目标路径是否已存在
|
||||
if os.path.exists(target_path):
|
||||
if not data.overwrite:
|
||||
raise CustomException(msg='目标路径已存在')
|
||||
# 删除目标路径
|
||||
if os.path.isfile(target_path):
|
||||
os.remove(target_path)
|
||||
else:
|
||||
# 删除目标路径
|
||||
if os.path.isfile(target_path):
|
||||
os.remove(target_path)
|
||||
else:
|
||||
shutil.rmtree(target_path)
|
||||
|
||||
shutil.rmtree(target_path)
|
||||
|
||||
# 确保目标目录存在
|
||||
target_dir = os.path.dirname(target_path)
|
||||
os.makedirs(target_dir, exist_ok=True)
|
||||
|
||||
|
||||
# 移动文件
|
||||
shutil.move(source_path, target_path)
|
||||
log.info(f"移动成功: {source_path} -> {target_path}")
|
||||
|
||||
|
||||
except CustomException:
|
||||
raise
|
||||
except Exception as e:
|
||||
log.error(f"移动失败: {str(e)}")
|
||||
raise CustomException(msg=f"移动失败: {str(e)}")
|
||||
log.error(f"移动失败: {e!s}")
|
||||
raise CustomException(msg=f"移动失败: {e!s}")
|
||||
|
||||
@classmethod
|
||||
async def copy_file_service(cls, data: ResourceCopySchema) -> None:
|
||||
"""
|
||||
复制文件或目录
|
||||
|
||||
|
||||
参数:
|
||||
- data (ResourceCopySchema): 包含源路径和目标路径的模型。
|
||||
|
||||
|
||||
返回:
|
||||
- None
|
||||
"""
|
||||
try:
|
||||
source_path = cls._get_safe_path(data.source_path)
|
||||
target_path = cls._get_safe_path(data.target_path)
|
||||
|
||||
|
||||
if not os.path.exists(source_path):
|
||||
raise CustomException(msg='源路径不存在')
|
||||
|
||||
|
||||
# 检查目标路径是否已存在
|
||||
if os.path.exists(target_path) and not data.overwrite:
|
||||
raise CustomException(msg='目标路径已存在')
|
||||
|
||||
|
||||
# 确保目标目录存在
|
||||
target_dir = os.path.dirname(target_path)
|
||||
os.makedirs(target_dir, exist_ok=True)
|
||||
|
||||
|
||||
# 复制文件或目录
|
||||
if os.path.isfile(source_path):
|
||||
shutil.copy2(source_path, target_path)
|
||||
else:
|
||||
shutil.copytree(source_path, target_path, dirs_exist_ok=data.overwrite)
|
||||
|
||||
|
||||
log.info(f"复制成功: {source_path} -> {target_path}")
|
||||
|
||||
|
||||
except CustomException:
|
||||
raise
|
||||
except Exception as e:
|
||||
log.error(f"复制失败: {str(e)}")
|
||||
raise CustomException(msg=f"复制失败: {str(e)}")
|
||||
log.error(f"复制失败: {e!s}")
|
||||
raise CustomException(msg=f"复制失败: {e!s}")
|
||||
|
||||
@classmethod
|
||||
async def rename_file_service(cls, data: ResourceRenameSchema) -> None:
|
||||
"""
|
||||
重命名文件或目录
|
||||
|
||||
|
||||
参数:
|
||||
- data (ResourceRenameSchema): 包含旧路径和新名称的模型。
|
||||
|
||||
|
||||
返回:
|
||||
- None
|
||||
"""
|
||||
try:
|
||||
old_path = cls._get_safe_path(data.old_path)
|
||||
|
||||
|
||||
if not os.path.exists(old_path):
|
||||
raise CustomException(msg='文件或目录不存在')
|
||||
|
||||
|
||||
# 生成新路径
|
||||
parent_dir = os.path.dirname(old_path)
|
||||
new_path = os.path.join(parent_dir, data.new_name)
|
||||
|
||||
|
||||
if os.path.exists(new_path):
|
||||
raise CustomException(msg='目标名称已存在')
|
||||
|
||||
|
||||
# 重命名
|
||||
os.rename(old_path, new_path)
|
||||
log.info(f"重命名成功: {old_path} -> {new_path}")
|
||||
|
||||
|
||||
except CustomException:
|
||||
raise
|
||||
except Exception as e:
|
||||
log.error(f"重命名失败: {str(e)}")
|
||||
raise CustomException(msg=f"重命名失败: {str(e)}")
|
||||
log.error(f"重命名失败: {e!s}")
|
||||
raise CustomException(msg=f"重命名失败: {e!s}")
|
||||
|
||||
@classmethod
|
||||
async def create_directory_service(cls, data: ResourceCreateDirSchema) -> None:
|
||||
"""
|
||||
创建目录
|
||||
|
||||
|
||||
参数:
|
||||
- data (ResourceCreateDirSchema): 包含父目录路径和目录名称的模型。
|
||||
|
||||
|
||||
返回:
|
||||
- None
|
||||
"""
|
||||
try:
|
||||
parent_path = cls._get_safe_path(data.parent_path)
|
||||
|
||||
|
||||
if not os.path.exists(parent_path):
|
||||
raise CustomException(msg='父目录不存在')
|
||||
|
||||
|
||||
if not os.path.isdir(parent_path):
|
||||
raise CustomException(msg='父路径不是目录')
|
||||
|
||||
|
||||
# 生成新目录路径
|
||||
new_dir_path = os.path.join(parent_path, data.dir_name)
|
||||
|
||||
|
||||
# 安全检查:确保新目录名称不包含路径遍历字符
|
||||
if '..' in data.dir_name or '/' in data.dir_name or '\\' in data.dir_name:
|
||||
raise CustomException(msg='目录名称包含不安全字符')
|
||||
|
||||
|
||||
if os.path.exists(new_dir_path):
|
||||
raise CustomException(msg='目录已存在')
|
||||
|
||||
|
||||
# 创建目录
|
||||
os.makedirs(new_dir_path)
|
||||
log.info(f"创建目录成功: {new_dir_path}")
|
||||
|
||||
|
||||
except CustomException:
|
||||
raise
|
||||
except Exception as e:
|
||||
log.error(f"创建目录失败: {str(e)}")
|
||||
raise CustomException(msg=f"创建目录失败: {str(e)}")
|
||||
log.error(f"创建目录失败: {e!s}")
|
||||
raise CustomException(msg=f"创建目录失败: {e!s}")
|
||||
|
||||
@classmethod
|
||||
def _format_file_size(cls, size_bytes: int) -> str:
|
||||
"""
|
||||
格式化文件大小
|
||||
|
||||
|
||||
参数:
|
||||
- size_bytes (int): 文件大小(字节)
|
||||
|
||||
|
||||
返回:
|
||||
- str: 格式化后的文件大小字符串(例如:"123.45MB")
|
||||
"""
|
||||
if size_bytes == 0:
|
||||
return "0B"
|
||||
|
||||
|
||||
size_names = ["B", "KB", "MB", "GB", "TB"]
|
||||
i = 0
|
||||
while size_bytes >= 1024 and i < len(size_names) - 1:
|
||||
size_bytes = int(size_bytes / 1024)
|
||||
i += 1
|
||||
|
||||
|
||||
return f"{size_bytes:.2f}{size_names[i]}"
|
||||
|
||||
@@ -1,2 +1 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
@@ -11,9 +10,9 @@ from app.core.router_class import OperationLogRoute
|
||||
|
||||
from .service import ServerService
|
||||
|
||||
|
||||
ServerRouter = APIRouter(route_class=OperationLogRoute, prefix="/server", tags=["服务器监控"])
|
||||
|
||||
|
||||
@ServerRouter.get(
|
||||
'/info',
|
||||
summary="查询服务器监控信息",
|
||||
@@ -23,7 +22,7 @@ ServerRouter = APIRouter(route_class=OperationLogRoute, prefix="/server", tags=[
|
||||
async def get_monitor_server_info_controller() -> JSONResponse:
|
||||
"""
|
||||
查询服务器监控信息
|
||||
|
||||
|
||||
返回:
|
||||
- JSONResponse: 包含服务器监控信息的JSON响应。
|
||||
"""
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
|
||||
|
||||
|
||||
@@ -1,21 +1,14 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import platform
|
||||
import psutil
|
||||
import socket
|
||||
import time
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import psutil
|
||||
|
||||
from app.utils.common_util import bytes2human
|
||||
|
||||
from .schema import (
|
||||
CpuInfoSchema,
|
||||
MemoryInfoSchema,
|
||||
PyInfoSchema,
|
||||
ServerMonitorSchema,
|
||||
DiskInfoSchema,
|
||||
SysInfoSchema
|
||||
)
|
||||
from .schema import CpuInfoSchema, DiskInfoSchema, MemoryInfoSchema, PyInfoSchema, ServerMonitorSchema, SysInfoSchema
|
||||
|
||||
|
||||
class ServerService:
|
||||
@@ -25,7 +18,7 @@ class ServerService:
|
||||
async def get_server_monitor_info_service(cls) -> dict:
|
||||
"""
|
||||
获取服务器监控信息
|
||||
|
||||
|
||||
返回:
|
||||
- Dict: 包含服务器监控信息的字典。
|
||||
"""
|
||||
@@ -41,12 +34,12 @@ class ServerService:
|
||||
def _get_cpu_info(cls) -> CpuInfoSchema:
|
||||
"""
|
||||
获取CPU信息
|
||||
|
||||
|
||||
返回:
|
||||
- CpuInfoSchema: CPU信息模型。
|
||||
"""
|
||||
cpu_times = psutil.cpu_times_percent()
|
||||
cpu_num=psutil.cpu_count(logical=True)
|
||||
cpu_num = psutil.cpu_count(logical=True)
|
||||
if not cpu_num:
|
||||
cpu_num = 1
|
||||
return CpuInfoSchema(
|
||||
@@ -60,7 +53,7 @@ class ServerService:
|
||||
def _get_memory_info(cls) -> MemoryInfoSchema:
|
||||
"""
|
||||
获取内存信息
|
||||
|
||||
|
||||
返回:
|
||||
- MemoryInfoSchema: 内存信息模型。
|
||||
"""
|
||||
@@ -76,7 +69,7 @@ class ServerService:
|
||||
def _get_system_info(cls) -> SysInfoSchema:
|
||||
"""
|
||||
获取系统信息
|
||||
|
||||
|
||||
返回:
|
||||
- SysInfoSchema: 系统信息模型。
|
||||
"""
|
||||
@@ -93,17 +86,17 @@ class ServerService:
|
||||
def _get_python_info(cls) -> PyInfoSchema:
|
||||
"""
|
||||
获取Python解释器信息
|
||||
|
||||
|
||||
返回:
|
||||
- PyInfoSchema: Python解释器信息模型。
|
||||
"""
|
||||
current_process = psutil.Process()
|
||||
memory = psutil.virtual_memory()
|
||||
process_memory = current_process.memory_info()
|
||||
|
||||
|
||||
start_time = current_process.create_time()
|
||||
run_time = ServerService._calculate_run_time(start_time)
|
||||
|
||||
|
||||
return PyInfoSchema(
|
||||
name=current_process.name(),
|
||||
version=platform.python_version(),
|
||||
@@ -120,7 +113,7 @@ class ServerService:
|
||||
def _get_disk_info(cls) -> list[DiskInfoSchema]:
|
||||
"""
|
||||
获取磁盘信息
|
||||
|
||||
|
||||
返回:
|
||||
- list[DiskInfoSchema]: 磁盘信息模型列表。
|
||||
"""
|
||||
@@ -147,13 +140,13 @@ class ServerService:
|
||||
return disk_info
|
||||
|
||||
@classmethod
|
||||
def _calculate_run_time(cls,start_time: float) -> str:
|
||||
def _calculate_run_time(cls, start_time: float) -> str:
|
||||
"""
|
||||
计算运行时间
|
||||
|
||||
|
||||
参数:
|
||||
- start_time (float): 进程启动时间(时间戳)
|
||||
|
||||
|
||||
返回:
|
||||
- str: 格式化后的运行时间字符串(例如:"1天2小时3分钟")
|
||||
"""
|
||||
|
||||
Reference in New Issue
Block a user