init: 初始化 dpb 桃育种系统代码库

前后端 + 后端 FastAPI 全量源码、部署脚本与文档。
This commit is contained in:
34047007@qq.com
2026-08-06 00:17:49 +08:00
commit b95053c52c
1469 changed files with 322298 additions and 0 deletions
@@ -0,0 +1,123 @@
from typing import Annotated
from fastapi import APIRouter, Body, Depends, File, Form, Query, Request, Security, UploadFile, status
from fastapi.responses import FileResponse, JSONResponse, StreamingResponse
from app.api.v1.module_common.file.service import FileService
from app.common.request import PaginationService
from app.common.response import ResponseSchema, StreamResponse, SuccessResponse, UploadFileResponse
from app.core.base_schema import PaginationQueryParam, UploadResponseSchema
from app.core.dependencies import AuthPermission
from app.core.router_class import OperationLogRoute
from app.utils.common_util import bytes2file_response
from .schema import ResourceCopySchema, ResourceCreateDirSchema, ResourceItemSchema, ResourceMoveSchema, ResourceRenameSchema, ResourceSearchQueryParam
from .service import ResourceService
ResourceRouter = APIRouter(route_class=OperationLogRoute, prefix="/resource", tags=["资源管理"])
@ResourceRouter.get("/list", summary="获取目录列表", response_model=ResponseSchema[list[ResourceItemSchema]], dependencies=[Security(AuthPermission(["module_monitor:resource:query"]))])
async def get_directory_list_controller(
request: Request,
page: Annotated[PaginationQueryParam, Depends()],
search: Annotated[ResourceSearchQueryParam, Query()],
) -> JSONResponse:
result_dict_list = await ResourceService.get_resources_list(search=search, base_url=str(request.base_url))
result_dict = await PaginationService.paginate(
data_list=result_dict_list,
page_no=page.page_no,
page_size=page.page_size,
)
return SuccessResponse(data=result_dict, msg="获取目录列表成功")
@ResourceRouter.post("/upload", summary="上传文件", response_model=ResponseSchema[UploadResponseSchema], dependencies=[Security(AuthPermission(["module_monitor:resource:upload"]))])
async def upload_file_controller(
request: Request,
file: Annotated[UploadFile, File(description="上传文件")],
target_path: Annotated[str | None, Form(description="目标目录路径")] = None,
) -> JSONResponse:
result = await FileService.upload_service(
base_url=str(request.base_url),
file=file,
upload_type="resource",
target_path=target_path,
)
return SuccessResponse(data=result, msg="上传文件成功")
@ResourceRouter.get(
"/download",
summary="下载文件",
dependencies=[Security(AuthPermission(["module_monitor:resource:download"]))],
)
async def download_file_controller(
path: Annotated[str, Query(description="文件路径")],
) -> FileResponse:
file_path = await ResourceService.download_file(file_path=path)
import os
filename = os.path.basename(file_path)
return UploadFileResponse(
file_path=file_path,
filename=filename,
media_type="application/octet-stream",
)
@ResourceRouter.delete("/delete", summary="删除文件", response_model=ResponseSchema[None], dependencies=[Security(AuthPermission(["module_monitor:resource:delete"]))])
async def delete_files_controller(
paths: Annotated[list[str], Body(description="文件路径列表")],
) -> JSONResponse:
await ResourceService.delete_file(paths=paths)
return SuccessResponse(msg="删除文件成功")
@ResourceRouter.post("/move", summary="移动文件", response_model=ResponseSchema[None], dependencies=[Security(AuthPermission(["module_monitor:resource:move"]))])
async def move_file_controller(
data: Annotated[ResourceMoveSchema, Body(description="移动文件参数")],
) -> JSONResponse:
await ResourceService.move_file(data=data)
return SuccessResponse(msg="移动文件成功")
@ResourceRouter.post("/copy", summary="复制文件", response_model=ResponseSchema[None], dependencies=[Security(AuthPermission(["module_monitor:resource:copy"]))])
async def copy_file_controller(
data: Annotated[ResourceCopySchema, Body(description="复制文件参数")],
) -> JSONResponse:
await ResourceService.copy_file(data=data)
return SuccessResponse(msg="复制文件成功")
@ResourceRouter.post("/rename", summary="重命名文件", response_model=ResponseSchema[None], dependencies=[Security(AuthPermission(["module_monitor:resource:rename"]))])
async def rename_file_controller(
data: Annotated[ResourceRenameSchema, Body(description="重命名文件参数")],
) -> JSONResponse:
await ResourceService.rename_file(data=data)
return SuccessResponse(msg="重命名文件成功")
@ResourceRouter.post("/mkdir", status_code=status.HTTP_201_CREATED, summary="创建目录", response_model=ResponseSchema[None], dependencies=[Security(AuthPermission(["module_monitor:resource:mkdir"]))])
async def create_directory_controller(
data: Annotated[ResourceCreateDirSchema, Body(description="创建目录参数")],
) -> JSONResponse:
await ResourceService.create_directory(data=data)
return SuccessResponse(msg="创建目录成功")
@ResourceRouter.post("/export", summary="导出资源列表", dependencies=[Security(AuthPermission(["module_monitor:resource:export"]))])
async def export_resource_list_controller(
request: Request,
search: Annotated[ResourceSearchQueryParam, Query()],
) -> StreamingResponse:
result_dict_list = await ResourceService.get_resources_list(search=search, base_url=str(request.base_url))
export_result = await ResourceService.export_resource(data_list=result_dict_list)
return StreamResponse(
data=bytes2file_response(export_result),
media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
headers={"Content-Disposition": "attachment; filename=resource_list.xlsx"},
)
@@ -0,0 +1,191 @@
from datetime import datetime
from urllib.parse import urlparse
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="相对路径")
is_file: bool = Field(..., description="是否为文件")
is_dir: bool = Field(..., description="是否为目录")
size: int | None = Field(None, description="文件大小(字节)")
created_time: datetime | None = Field(None, description="创建时间")
modified_time: datetime | None = Field(None, description="修改时间")
is_hidden: bool = Field(False, description="是否为隐藏文件")
@field_validator("file_url")
@classmethod
def _validate_file_url(cls, v: str) -> str:
v = v.strip()
parsed = urlparse(v)
# 允许相对路径(以 / 开头)和完整的 http/https URL
if parsed.scheme and parsed.scheme not in ("http", "https"):
raise ValueError("文件URL必须为 http/https 或相对路径")
return v
@field_validator("relative_path")
@classmethod
def _validate_relative_path(cls, v: str) -> str:
v = v.strip()
if ".." in v or v.startswith("\\"):
raise ValueError("相对路径包含不安全字符")
return v
@model_validator(mode="after")
def _validate_flags(self):
if self.is_file and self.is_dir:
raise ValueError("不能同时为文件和目录")
if not self.is_file and not self.is_dir:
raise ValueError("必须是文件或目录之一")
# 根据名称自动修正隐藏标记
self.is_hidden = self.name.startswith(".")
return self
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="目录项")
total_files: int = Field(0, description="文件总数")
total_dirs: int = Field(0, description="目录总数")
total_size: int = Field(0, description="总大小")
class ResourceUploadSchema(BaseModel):
"""资源上传响应模型"""
model_config = ConfigDict(from_attributes=True)
filename: str = Field(..., description="文件名")
file_url: str = Field(..., description="访问URL")
file_size: int = Field(..., description="文件大小")
upload_time: datetime = Field(..., description="上传时间")
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):
"""校验移动/复制涉及的源路径与目标路径非空并去首尾空格。
参数:
- value (str): 路径字段当前值。
返回:
- str: 去空格后的路径。
异常:
- ValueError: 路径为空时抛出。
"""
if not value or len(value.strip()) == 0:
raise ValueError("路径不能为空")
return value.strip()
class ResourceCopySchema(ResourceMoveSchema):
"""资源复制模型"""
class ResourceRenameSchema(BaseModel):
"""资源重命名模型"""
model_config = ConfigDict(from_attributes=True)
old_path: str = Field(..., description="原路径")
new_name: str = Field(..., max_length=255, description="新名称")
@field_validator("old_path", "new_name")
@classmethod
def validate_inputs(cls, value: str):
"""校验重命名所需的原路径与新名称非空并去首尾空格。
参数:
- value (str): 字段当前值。
返回:
- str: 去空格后的值。
异常:
- ValueError: 值为空时抛出。
"""
if not value or len(value.strip()) == 0:
raise ValueError("参数不能为空")
return value.strip()
@field_validator("new_name")
@classmethod
def _validate_new_name(cls, v: str) -> str:
v = v.strip()
if ".." in v or "/" in v or "\\" in v:
raise ValueError("新名称包含不安全字符")
return v
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):
"""校验创建目录的父路径与目录名,防止路径遍历等不安全输入。
参数:
- value (str): 当前字段值。
- info: Pydantic 校验上下文(含 `field_name`)。
返回:
- str: 规范化后的字段值。
异常:
- ValueError: 含不安全字符或目录名为空时抛出。
"""
# 对于parent_path允许为空字符串(表示根目录)或 '/',其他情况必须非空
if info.field_name == "parent_path":
# 对于parent_path仍然严格检查路径遍历
if ".." in value or value.startswith("\\"):
raise ValueError("参数包含不安全字符")
else: # 对于dir_name仍然严格检查
if not value or len(value.strip()) == 0:
raise ValueError("参数不能为空")
if ".." in value or value.startswith(("/", "\\")):
raise ValueError("参数包含不安全字符")
return value.strip()
class ResourceSearchQueryParam(BaseModel):
"""资源搜索查询参数"""
name: str | None = Field(None, description="搜索关键词")
path: str | None = Field(None, description="目录路径")
include_hidden: bool = Field(False, description="是否包含隐藏文件")
@@ -0,0 +1,471 @@
import ast
import os
import re
import shutil
import urllib.parse
from datetime import datetime
from pathlib import Path
from urllib.parse import urlparse
from app.config.path_conf import STATIC_DIR
from app.config.setting import settings
from app.core.exceptions import CustomException
from app.core.logger import logger
from app.utils.excel_util import ExcelUtil
from .schema import (
ResourceCopySchema,
ResourceCreateDirSchema,
ResourceItemSchema,
ResourceMoveSchema,
ResourceRenameSchema,
ResourceSearchQueryParam,
)
class ResourceService:
"""资源管理模块服务层 - 管理系统静态文件目录(仅管理 upload 目录)"""
MAX_UPLOAD_SIZE = 100 * 1024 * 1024 # 100MB
MAX_SEARCH_RESULTS = 1000
MAX_PATH_DEPTH = 20
@staticmethod
def _get_resource_root() -> str:
resource_root = str(settings.UPLOAD_FILE_PATH)
os.makedirs(resource_root, exist_ok=True)
return resource_root
@staticmethod
def _get_safe_path(path: str | None = None) -> str:
resource_root = ResourceService._get_resource_root()
if not path or not isinstance(path, str):
return resource_root
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://", "https://")):
parsed = urlparse(path)
url_path = parsed.path or ""
path = strip_prefix(url_path)
else:
path = strip_prefix(path)
path = path.strip().replace("//", "/").replace("\\\\\\\\", "/").replace("\\\\", "/")
path = path.removeprefix("/")
path = path.removeprefix("upload/")
if ".." in path or "\x00" in path:
logger.error(f"检测到路径遍历攻击尝试: {path}")
raise CustomException(msg="非法的路径格式")
decoded_path = urllib.parse.unquote(path)
if ".." in decoded_path:
logger.error(f"检测到编码后的路径遍历攻击: {path}")
raise CustomException(msg="非法的路径格式")
safe_path = 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 + os.sep) and safe_path_abs != resource_root_abs:
logger.error(f"路径遍历攻击被阻止: 尝试访问 {safe_path_abs}, 但根目录是 {resource_root_abs}")
raise CustomException(msg="访问路径不在允许范围内")
try:
relative_path = os.path.relpath(safe_path_abs, resource_root_abs)
if relative_path.count(os.sep) > ResourceService.MAX_PATH_DEPTH:
raise CustomException(msg="路径深度超过限制")
except ValueError:
raise CustomException(msg="无效的路径")
return safe_path_abs
@staticmethod
def _path_exists(path: str) -> bool:
try:
safe_path = ResourceService._get_safe_path(path)
return os.path.exists(safe_path)
except Exception as e:
raise CustomException(msg=f"检查路径是否存在失败: {e!s}")
@staticmethod
def _sanitize_filename(filename: str) -> str:
if not filename:
return f"file_{datetime.now().strftime('%Y%m%d%H%M%S')}"
dangerous_patterns = [
r"\.\.",
r"[\/]",
r"\x00",
r"%2e%2e",
r"%252e%252e",
]
for pattern in dangerous_patterns:
if re.search(pattern, filename, re.IGNORECASE):
logger.error(f"检测到文件名路径遍历攻击: {filename}")
return f"file_{datetime.now().strftime('%Y%m%d%H%M%S')}"
decoded = urllib.parse.unquote(filename)
decoded_twice = urllib.parse.unquote(decoded)
for check in [decoded, decoded_twice]:
if ".." in check or "/" in check or "\\" in check:
logger.error(f"检测到编码后的文件名攻击: {filename}")
return f"file_{datetime.now().strftime('%Y%m%d%H%M%S')}"
filename = os.path.basename(filename)
filename = re.sub(r'[<>:"|?*\x00-\x1f]', "", filename)
filename = re.sub(r"\.{2,}", ".", filename)
filename = filename.strip(". ")
if not filename:
filename = f"file_{datetime.now().strftime('%Y%m%d%H%M%S')}"
return filename
@staticmethod
def _detect_file_type(content: bytes) -> str | None:
if content.startswith(b"\xff\xd8\xff"):
return "image/jpeg"
if content.startswith(b"\x89PNG\r\n\x1a\n"):
return "image/png"
if content.startswith(b"GIF87a") or content.startswith(b"GIF89a"):
return "image/gif"
if content.startswith(b"PK\x03\x04"):
if b"[Content_Types].xml" in content[:1000]:
return "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
return "application/zip"
if content.startswith(b"%PDF"):
return "application/pdf"
if content.startswith(b"\xd0\xcf\x11\xe0\xa1\xb1\x1a\xe1"):
return "application/msword"
return None
@staticmethod
def _generate_http_url(file_path: str, base_url: str | None = None) -> str:
static_root = str(STATIC_DIR)
try:
relative_path = os.path.relpath(file_path, static_root)
url_path = relative_path.replace(os.sep, "/")
except ValueError:
url_path = os.path.basename(file_path)
if base_url:
base_part = base_url.rstrip("/")
static_part = settings.STATIC_URL.lstrip("/")
file_part = url_path.lstrip("/")
http_url = f"{base_part}/{static_part}/{file_part}".replace("//", "/").replace(":/", "://")
else:
http_url = f"{settings.STATIC_URL}/{url_path}".replace("//", "/")
return http_url
@staticmethod
def _get_file_info(file_path: str, base_url: str | None = None) -> ResourceItemSchema | None:
try:
safe_path = file_path
if not os.path.exists(safe_path):
return None
stat = os.stat(safe_path)
path_obj = Path(safe_path)
resource_root = ResourceService._get_resource_root()
try:
relative_path = os.path.relpath(safe_path, resource_root)
except ValueError:
relative_path = os.path.basename(safe_path)
http_url = ResourceService._generate_http_url(safe_path, base_url)
is_hidden = path_obj.name.startswith(".")
return ResourceItemSchema(
name=path_obj.name,
file_url=http_url,
relative_path=relative_path,
is_file=os.path.isfile(safe_path),
is_dir=os.path.isdir(safe_path),
size=stat.st_size if os.path.isfile(safe_path) else None,
created_time=datetime.fromtimestamp(stat.st_ctime),
modified_time=datetime.fromtimestamp(stat.st_mtime),
is_hidden=is_hidden,
)
except Exception as e:
logger.error(f"获取文件信息失败: {e!s}")
return None
@staticmethod
async def get_resources_list(
search: ResourceSearchQueryParam | None = None,
order_by: str | None = None,
base_url: str | None = None,
) -> list[ResourceItemSchema]:
try:
if search and hasattr(search, "path") and search.path and isinstance(search.path, str):
resource_root = ResourceService._get_safe_path(search.path)
else:
resource_root = ResourceService._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:
include_hidden = search.include_hidden if search and hasattr(search, "include_hidden") else False
for item_name in os.listdir(resource_root):
if item_name.startswith(".") and not include_hidden:
continue
item_path = os.path.join(resource_root, item_name)
file_info = ResourceService._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.name.lower():
continue
all_resources.append(file_info)
except PermissionError:
raise CustomException(msg="没有权限访问此目录")
sorted_resources = ResourceService._sort_results(all_resources, order_by)
if len(sorted_resources) > ResourceService.MAX_SEARCH_RESULTS:
sorted_resources = sorted_resources[: ResourceService.MAX_SEARCH_RESULTS]
return sorted_resources
except Exception as e:
logger.error(f"搜索资源失败: {e!s}")
raise CustomException(msg=f"搜索资源失败: {e!s}")
@staticmethod
async def export_resource(data_list: list[ResourceItemSchema]) -> bytes:
mapping_dict = {
"name": "文件名",
"path": "文件路径",
"size": "文件大小",
"created_time": "创建时间",
"modified_time": "修改时间",
"parent_path": "父目录",
}
export_data = [item.model_dump() for item in data_list]
for item in export_data:
if item.get("size"):
item["size"] = ResourceService._format_file_size(item["size"])
return ExcelUtil.export_list2excel(list_data=export_data, mapping_dict=mapping_dict)
@staticmethod
async def download_file(file_path: str) -> str:
safe_path = ResourceService._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="路径不是文件")
return safe_path
@staticmethod
async def delete_file(paths: list[str]) -> None:
for path_item in paths:
safe_path = ResourceService._get_safe_path(path_item)
if not os.path.exists(safe_path):
raise CustomException(msg=f"文件不存在: {path_item}")
try:
if os.path.isfile(safe_path):
os.remove(safe_path)
elif os.path.isdir(safe_path):
shutil.rmtree(safe_path)
else:
raise CustomException(msg=f"无法识别的文件类型: {path_item}")
except PermissionError:
raise CustomException(msg=f"没有权限删除: {path_item}")
except OSError as e:
raise CustomException(msg=f"删除失败: {path_item} - {e!s}")
logger.info(f"成功删除: {path_item}")
@staticmethod
async def move_file(data: ResourceMoveSchema) -> None:
source_safe = ResourceService._get_safe_path(data.source_path)
target_dir_safe = ResourceService._get_safe_path(data.target_path)
if not os.path.exists(source_safe):
raise CustomException(msg=f"源文件不存在: {data.source_path}")
if not os.path.isdir(target_dir_safe):
raise CustomException(msg=f"目标目录不存在: {data.target_path}")
filename = os.path.basename(source_safe)
target_path = os.path.join(target_dir_safe, filename)
if os.path.exists(target_path):
raise CustomException(msg=f"目标位置已存在同名文件: {filename}")
try:
shutil.move(source_safe, target_path)
except PermissionError:
raise CustomException(msg=f"没有权限移动文件: {data.source_path}")
except OSError as e:
raise CustomException(msg=f"移动文件失败: {e!s}")
logger.info(f"成功移动文件: {data.source_path} -> {data.target_path}")
@staticmethod
async def copy_file(data: ResourceCopySchema) -> None:
source_safe = ResourceService._get_safe_path(data.source_path)
target_dir_safe = ResourceService._get_safe_path(data.target_path)
if not os.path.exists(source_safe):
raise CustomException(msg=f"源文件不存在: {data.source_path}")
if not os.path.isdir(target_dir_safe):
raise CustomException(msg=f"目标目录不存在: {data.target_path}")
filename = os.path.basename(source_safe)
target_path = os.path.join(target_dir_safe, filename)
if os.path.exists(target_path):
raise CustomException(msg=f"目标位置已存在同名文件: {filename}")
try:
if os.path.isdir(source_safe):
shutil.copytree(source_safe, target_path)
else:
shutil.copy2(source_safe, target_path)
except PermissionError:
raise CustomException(msg=f"没有权限复制文件: {data.source_path}")
except OSError as e:
raise CustomException(msg=f"复制文件失败: {e!s}")
logger.info(f"成功复制文件: {data.source_path} -> {data.target_path}")
@staticmethod
async def rename_file(data: ResourceRenameSchema) -> None:
safe_path = ResourceService._get_safe_path(data.old_path)
parent_dir = os.path.dirname(safe_path)
safe_name = ResourceService._sanitize_filename(data.new_name)
new_path = os.path.join(parent_dir, safe_name)
if os.path.exists(new_path):
raise CustomException(msg=f"目标文件名已存在: {safe_name}")
try:
os.rename(safe_path, new_path)
except PermissionError:
raise CustomException(msg=f"没有权限重命名: {data.old_path}")
except OSError as e:
raise CustomException(msg=f"重命名失败: {e!s}")
logger.info(f"成功重命名: {data.old_path} -> {safe_name}")
@staticmethod
async def create_directory(data: ResourceCreateDirSchema) -> None:
parent_dir = ResourceService._get_safe_path(data.parent_path)
if not os.path.isdir(parent_dir):
raise CustomException(msg=f"父目录不存在: {data.parent_path}")
safe_name = ResourceService._sanitize_filename(data.dir_name)
new_dir = os.path.join(parent_dir, safe_name)
if os.path.exists(new_dir):
raise CustomException(msg=f"目录已存在: {data.dir_name}")
try:
os.makedirs(new_dir, exist_ok=False)
except PermissionError:
raise CustomException(msg=f"没有权限创建目录: {data.dir_name}")
except OSError as e:
raise CustomException(msg=f"创建目录失败: {e!s}")
logger.info(f"成功创建目录: {data.parent_path}/{safe_name}")
@staticmethod
async def _get_directory_stats(path: str, include_hidden: bool = False) -> 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:
continue
except Exception:
pass
return stats
@staticmethod
def _sort_results(results: list[ResourceItemSchema], order_by: str | None = None) -> list[ResourceItemSchema]:
try:
if not order_by:
return sorted(results, key=lambda x: x.name, reverse=False)
sort_conditions = ast.literal_eval(order_by)
if isinstance(sort_conditions, list):
def sort_key(item):
keys = []
for cond in sort_conditions:
field = cond.get("field", "name")
value = getattr(item, field, "")
if field in ["created_time", "modified_time", "accessed_time"] and value:
if isinstance(value, str):
value = datetime.fromisoformat(value)
keys.append(value)
return keys
reverse = False
if sort_conditions and isinstance(sort_conditions[0], dict):
order = sort_conditions[0].get("order", "asc")
reverse = order.lower() == "desc"
return sorted(results, key=sort_key, reverse=reverse)
return sorted(results, key=lambda x: x.name, reverse=False)
except (ValueError, SyntaxError):
return sorted(results, key=lambda x: x.name, reverse=False)
@staticmethod
def _format_file_size(size_bytes: int) -> str:
size = float(size_bytes)
for unit in ["B", "KB", "MB", "GB"]:
if size < 1024:
return f"{size:.2f} {unit}"
size /= 1024
return f"{size:.2f} TB"