mirror of
https://github.com/fastapiadmin/FastapiAdmin.git
synced 2026-09-21 04:46:26 +00:00
feat: 新增权限指令并集成到多个页面组件
refactor(backend): 重构数据库模型和初始化逻辑 fix(frontend): 修复权限指令在多个组件中的使用问题 perf(backend): 优化数据库连接和初始化性能 docs: 更新低代码生成器的README文档 style: 清理无用代码和注释 chore: 移除不再需要的依赖项 test: 更新用户权限相关测试用例 ci: 更新CI配置以支持新的权限检查 build: 更新依赖项版本
This commit is contained in:
@@ -2,17 +2,20 @@
|
||||
|
||||
from fastapi import APIRouter, Body, Depends, Path, Query, Request, UploadFile, Form
|
||||
from fastapi.responses import JSONResponse, StreamingResponse, FileResponse
|
||||
import urllib.parse
|
||||
import os
|
||||
from typing import List, Optional
|
||||
|
||||
from app.common.response import StreamResponse, SuccessResponse
|
||||
from app.common.response import StreamResponse, SuccessResponse, ErrorResponse
|
||||
from app.common.request import PaginationService
|
||||
from app.utils.common_util import bytes2file_response
|
||||
from app.core.base_params import PaginationQueryParam
|
||||
from app.core.dependencies import AuthPermission
|
||||
from app.core.router_class import OperationLogRoute
|
||||
from app.core.logger import logger
|
||||
from ...module_system.auth.schema import AuthSchema
|
||||
from .param import ResourceQueryParam
|
||||
from .param import ResourceSearchQueryParam
|
||||
from .schema import (
|
||||
ResourceSearchSchema,
|
||||
ResourceMoveSchema,
|
||||
ResourceCopySchema,
|
||||
ResourceRenameSchema,
|
||||
@@ -20,44 +23,33 @@ from .schema import (
|
||||
)
|
||||
from .service import ResourceService
|
||||
|
||||
|
||||
ResourceRouter = APIRouter(route_class=OperationLogRoute, prefix="/resource", tags=["资源管理"])
|
||||
|
||||
|
||||
@ResourceRouter.get("/list", summary="获取目录列表", description="获取指定目录下的文件和子目录列表")
|
||||
async def get_directory_list_controller(
|
||||
request: Request,
|
||||
path: Optional[str] = Query(None, description="目录路径"),
|
||||
include_hidden: bool = Query(False, description="是否包含隐藏文件"),
|
||||
page: PaginationQueryParam = Depends(),
|
||||
search: ResourceSearchQueryParam = Depends(),
|
||||
auth: AuthSchema = Depends(AuthPermission(permissions=["monitor:resource:query"]))
|
||||
) -> JSONResponse:
|
||||
"""获取目录列表"""
|
||||
result_dict = await ResourceService.get_directory_list_service(
|
||||
auth=auth,
|
||||
path=path,
|
||||
include_hidden=include_hidden,
|
||||
# 获取资源列表(与案例模块保持一致的分页实现)
|
||||
result_dict_list = await ResourceService.get_resources_list_service(
|
||||
search=search,
|
||||
order_by=page.order_by,
|
||||
base_url=str(request.base_url)
|
||||
)
|
||||
logger.info(f"获取目录列表成功: {path or 'default'}")
|
||||
# 使用分页服务进行分页处理(与案例模块保持一致)
|
||||
result_dict = await PaginationService.paginate(
|
||||
data_list=result_dict_list,
|
||||
page_no=page.page_no,
|
||||
page_size=page.page_size
|
||||
)
|
||||
|
||||
logger.info(f"获取目录列表成功: {getattr(search, 'name', None) or ''}")
|
||||
return SuccessResponse(data=result_dict, msg="获取目录列表成功")
|
||||
|
||||
|
||||
@ResourceRouter.post("/search", summary="搜索资源", description="根据条件搜索资源")
|
||||
async def search_resources_controller(
|
||||
request: Request,
|
||||
search: ResourceSearchSchema,
|
||||
auth: AuthSchema = Depends(AuthPermission(permissions=["monitor:resource:search"]))
|
||||
) -> JSONResponse:
|
||||
"""搜索资源"""
|
||||
result_list = await ResourceService.search_resources_service(
|
||||
auth=auth,
|
||||
search=search,
|
||||
base_url=str(request.base_url)
|
||||
)
|
||||
logger.info(f"搜索资源成功,找到 {len(result_list)} 个结果")
|
||||
return SuccessResponse(data=result_list, msg=f"搜索成功,找到 {len(result_list)} 个结果")
|
||||
|
||||
|
||||
@ResourceRouter.post("/upload", summary="上传文件", description="上传文件到指定目录")
|
||||
async def upload_file_controller(
|
||||
file: UploadFile,
|
||||
@@ -137,7 +129,7 @@ async def copy_file_controller(
|
||||
@ResourceRouter.post("/rename", summary="重命名文件", description="重命名文件或目录")
|
||||
async def rename_file_controller(
|
||||
data: ResourceRenameSchema,
|
||||
auth: AuthSchema = Depends(AuthPermission(permissions=["rmonitor:resource:rename"]))
|
||||
auth: AuthSchema = Depends(AuthPermission(permissions=["monitor:resource:rename"]))
|
||||
) -> JSONResponse:
|
||||
"""重命名文件"""
|
||||
await ResourceService.rename_file_service(auth=auth, data=data)
|
||||
@@ -156,34 +148,19 @@ async def create_directory_controller(
|
||||
return SuccessResponse(msg="创建目录成功")
|
||||
|
||||
|
||||
@ResourceRouter.get("/stats", summary="获取资源统计", description="获取资源统计信息")
|
||||
async def get_resource_stats_controller(
|
||||
request: Request,
|
||||
auth: AuthSchema = Depends(AuthPermission(permissions=["monitor:resource:query"]))
|
||||
) -> JSONResponse:
|
||||
"""获取资源统计"""
|
||||
result_dict = await ResourceService.get_stats_service(
|
||||
auth=auth,
|
||||
base_url=str(request.base_url)
|
||||
)
|
||||
logger.info("获取资源统计成功")
|
||||
return SuccessResponse(data=result_dict, msg="获取资源统计成功")
|
||||
|
||||
|
||||
@ResourceRouter.post("/export", summary="导出资源列表", description="导出资源列表")
|
||||
async def export_resource_list_controller(
|
||||
request: Request,
|
||||
search: ResourceSearchSchema,
|
||||
search: ResourceSearchQueryParam = Depends(),
|
||||
auth: AuthSchema = Depends(AuthPermission(permissions=["monitor:resource:export"]))
|
||||
) -> StreamingResponse:
|
||||
"""导出资源列表"""
|
||||
# 获取搜索结果
|
||||
result_list = await ResourceService.search_resources_service(
|
||||
auth=auth,
|
||||
result_dict_list = await ResourceService.search_resources_service(
|
||||
search=search,
|
||||
base_url=str(request.base_url)
|
||||
)
|
||||
export_result = await ResourceService.export_resource_service(data_list=result_list)
|
||||
export_result = await ResourceService.export_resource_service(data_list=result_dict_list)
|
||||
|
||||
logger.info("导出资源列表成功")
|
||||
return StreamResponse(
|
||||
|
||||
@@ -1,33 +1,20 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from typing import Optional, List
|
||||
from pydantic import BaseModel, Field
|
||||
from enum import Enum
|
||||
from typing import Optional
|
||||
from fastapi import Query
|
||||
|
||||
class ResourceSearchQueryParam:
|
||||
"""资源搜索查询参数"""
|
||||
|
||||
class ResourceType(Enum):
|
||||
"""资源类型枚举"""
|
||||
IMAGE = "image" # 图片
|
||||
VIDEO = "video" # 视频
|
||||
AUDIO = "audio" # 音频
|
||||
DOCUMENT = "document" # 文档
|
||||
ARCHIVE = "archive" # 压缩包
|
||||
OTHER = "other" # 其他
|
||||
|
||||
|
||||
class ResourceQueryParam(BaseModel):
|
||||
"""资源查询参数模型"""
|
||||
path: Optional[str] = Field(None, description="文件路径")
|
||||
keyword: Optional[str] = Field(None, description="关键词搜索")
|
||||
resource_type: Optional[ResourceType] = Field(None, description="资源类型")
|
||||
file_extension: Optional[str] = Field(None, description="文件扩展名")
|
||||
min_size: Optional[int] = Field(None, ge=0, description="最小文件大小")
|
||||
max_size: Optional[int] = Field(None, ge=0, description="最大文件大小")
|
||||
include_hidden: bool = Field(False, description="包含隐藏文件")
|
||||
recursive: bool = Field(True, description="递归搜索")
|
||||
max_depth: int = Field(10, description="最大搜索深度")
|
||||
sort_by: Optional[str] = Field("name", description="排序字段(name/size/modified_time)")
|
||||
sort_order: Optional[str] = Field("asc", description="排序方式(asc/desc)")
|
||||
|
||||
class Config:
|
||||
use_enum_values = True
|
||||
def __init__(
|
||||
self,
|
||||
name: Optional[str] = Query(None, description="搜索关键词"),
|
||||
path: Optional[str] = Query(None, description="目录路径"),
|
||||
) -> None:
|
||||
super().__init__()
|
||||
|
||||
# 模糊查询字段
|
||||
self.name = ("like", name) if name else None
|
||||
|
||||
# 精确查询字段
|
||||
self.path = path
|
||||
@@ -4,42 +4,26 @@ from typing import Optional, List, Dict, Any
|
||||
from datetime import datetime
|
||||
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
||||
from pathlib import Path
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class ResourceType(Enum):
|
||||
"""资源类型枚举"""
|
||||
IMAGE = "image" # 图片
|
||||
VIDEO = "video" # 视频
|
||||
AUDIO = "audio" # 音频
|
||||
DOCUMENT = "document" # 文档
|
||||
ARCHIVE = "archive" # 压缩包
|
||||
OTHER = "other" # 其他
|
||||
|
||||
|
||||
class ResourceItemSchema(BaseModel):
|
||||
"""资源项目模型"""
|
||||
model_config = ConfigDict(from_attributes=True, use_enum_values=True)
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
name: str = Field(..., description="文件名")
|
||||
path: 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: Optional[int] = Field(None, description="文件大小(字节)")
|
||||
file_type: Optional[str] = Field(None, description="文件类型")
|
||||
file_extension: Optional[str] = Field(None, description="文件扩展名")
|
||||
resource_type: Optional[ResourceType] = Field(None, description="资源类型")
|
||||
created_time: Optional[datetime] = Field(None, description="创建时间")
|
||||
modified_time: Optional[datetime] = Field(None, description="修改时间")
|
||||
accessed_time: Optional[datetime] = Field(None, description="访问时间")
|
||||
parent_path: Optional[str] = Field(None, description="父目录路径")
|
||||
depth: int = Field(0, description="目录深度")
|
||||
|
||||
is_hidden: bool = Field(False, description="是否为隐藏文件")
|
||||
|
||||
|
||||
class ResourceDirectorySchema(BaseModel):
|
||||
"""资源目录模型"""
|
||||
model_config = ConfigDict(from_attributes=True, use_enum_values=True)
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
path: str = Field(..., description="目录路径")
|
||||
name: str = Field(..., description="目录名称")
|
||||
@@ -47,53 +31,22 @@ class ResourceDirectorySchema(BaseModel):
|
||||
total_files: int = Field(0, description="文件总数")
|
||||
total_dirs: int = Field(0, description="目录总数")
|
||||
total_size: int = Field(0, description="总大小")
|
||||
|
||||
|
||||
class ResourceStatsSchema(BaseModel):
|
||||
"""资源统计模型"""
|
||||
model_config = ConfigDict(from_attributes=True, use_enum_values=True)
|
||||
|
||||
mount_point: str = Field(..., description="挂载点")
|
||||
total_files: int = Field(0, description="文件总数")
|
||||
total_dirs: int = Field(0, description="目录总数")
|
||||
total_size: int = Field(0, description="总大小")
|
||||
free_space: int = Field(0, description="可用空间")
|
||||
used_space: int = Field(0, description="已用空间")
|
||||
total_space: int = Field(0, description="总空间")
|
||||
type_stats: Dict[str, int] = Field(default_factory=dict, description="类型统计")
|
||||
extension_stats: Dict[str, int] = Field(default_factory=dict, description="扩展名统计")
|
||||
|
||||
|
||||
class ResourceSearchSchema(BaseModel):
|
||||
"""资源搜索模型"""
|
||||
model_config = ConfigDict(from_attributes=True, use_enum_values=True)
|
||||
|
||||
keyword: Optional[str] = Field(None, description="关键词")
|
||||
file_type: Optional[str] = Field(None, description="文件类型")
|
||||
resource_type: Optional[ResourceType] = Field(None, description="资源类型")
|
||||
min_size: Optional[int] = Field(None, description="最小文件大小")
|
||||
max_size: Optional[int] = Field(None, description="最大文件大小")
|
||||
start_date: Optional[datetime] = Field(None, description="开始日期")
|
||||
end_date: Optional[datetime] = Field(None, description="结束日期")
|
||||
extensions: Optional[List[str]] = Field(None, description="文件扩展名列表")
|
||||
include_hidden: bool = Field(False, description="包含隐藏文件")
|
||||
max_depth: int = Field(10, description="最大搜索深度")
|
||||
|
||||
|
||||
class ResourceUploadSchema(BaseModel):
|
||||
"""资源上传响应模型"""
|
||||
model_config = ConfigDict(from_attributes=True, use_enum_values=True)
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
filename: str = Field(..., description="文件名")
|
||||
file_path: str = Field(..., description="文件路径")
|
||||
file_url: str = Field(..., description="访问URL")
|
||||
file_size: int = Field(..., description="文件大小")
|
||||
resource_type: ResourceType = 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="是否覆盖")
|
||||
@@ -113,6 +66,8 @@ class ResourceCopySchema(ResourceMoveSchema):
|
||||
|
||||
class ResourceRenameSchema(BaseModel):
|
||||
"""资源重命名模型"""
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
old_path: str = Field(..., description="原路径")
|
||||
new_name: str = Field(..., description="新名称")
|
||||
|
||||
@@ -126,12 +81,25 @@ class ResourceRenameSchema(BaseModel):
|
||||
|
||||
class ResourceCreateDirSchema(BaseModel):
|
||||
"""创建目录模型"""
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
parent_path: str = Field(..., description="父目录路径")
|
||||
dir_name: str = Field(..., description="目录名称")
|
||||
dir_name: str = Field(..., description="目录名称", max_length=255)
|
||||
|
||||
@field_validator('parent_path', 'dir_name')
|
||||
@classmethod
|
||||
def validate_inputs(cls, value: str):
|
||||
if not value or len(value.strip()) == 0:
|
||||
raise ValueError("参数不能为空")
|
||||
def validate_inputs(cls, value: str, info):
|
||||
# 对于parent_path允许为空字符串(表示根目录)或 '/',其他情况必须非空
|
||||
if info.field_name == 'parent_path':
|
||||
# 允许空字符串或 '/' 表示根目录
|
||||
if value is None:
|
||||
raise ValueError("参数不能为空")
|
||||
# 对于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('/') or value.startswith('\\'):
|
||||
raise ValueError("参数包含不安全字符")
|
||||
return value.strip()
|
||||
@@ -2,34 +2,25 @@
|
||||
|
||||
import os
|
||||
import shutil
|
||||
import hashlib
|
||||
import io
|
||||
import psutil
|
||||
from datetime import datetime
|
||||
from typing import List, Dict, Any, Optional
|
||||
from pathlib import Path
|
||||
from fastapi import UploadFile
|
||||
from PIL import Image
|
||||
import pylibmagic # 不要删除,否则import magic 导入启动报错ImportError: failed to find libmagic. Check your installation
|
||||
import magic
|
||||
|
||||
from app.core.exceptions import CustomException
|
||||
from app.core.logger import logger
|
||||
from app.utils.excel_util import ExcelUtil
|
||||
from app.config.setting import settings
|
||||
from ...module_system.auth.schema import AuthSchema
|
||||
from .param import ResourceQueryParam
|
||||
from .param import ResourceSearchQueryParam
|
||||
from .schema import (
|
||||
ResourceItemSchema,
|
||||
ResourceDirectorySchema,
|
||||
ResourceStatsSchema,
|
||||
ResourceSearchSchema,
|
||||
ResourceUploadSchema,
|
||||
ResourceMoveSchema,
|
||||
ResourceCopySchema,
|
||||
ResourceRenameSchema,
|
||||
ResourceCreateDirSchema,
|
||||
ResourceType
|
||||
ResourceCreateDirSchema
|
||||
)
|
||||
|
||||
|
||||
@@ -51,7 +42,7 @@ class ResourceService:
|
||||
return str(settings.STATIC_ROOT)
|
||||
|
||||
@classmethod
|
||||
def _get_safe_path(cls, path: str = None) -> str:
|
||||
def _get_safe_path(cls, path: Optional[str] = None) -> str:
|
||||
"""获取安全的文件路径"""
|
||||
resource_root = cls._get_resource_root()
|
||||
|
||||
@@ -89,6 +80,33 @@ class ResourceService:
|
||||
except:
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
def _generate_http_url(cls, file_path: str, base_url: Optional[str] = None) -> str:
|
||||
"""生成文件的HTTP URL"""
|
||||
resource_root = cls._get_resource_root()
|
||||
try:
|
||||
relative_path = os.path.relpath(file_path, resource_root)
|
||||
# 确保路径使用正斜杠(URL格式)
|
||||
url_path = relative_path.replace(os.sep, '/')
|
||||
except ValueError:
|
||||
# 如果无法计算相对路径,使用文件名
|
||||
url_path = os.path.basename(file_path)
|
||||
|
||||
# 如果提供了base_url,使用它生成完整URL,否则使用settings.STATIC_URL
|
||||
if base_url:
|
||||
from urllib.parse import urljoin
|
||||
# 修复URL生成逻辑
|
||||
base_part = base_url.rstrip('/')
|
||||
static_part = settings.STATIC_URL.lstrip('/')
|
||||
file_part = url_path.lstrip('/')
|
||||
if base_part.endswith(':') or (len(base_part) > 0 and base_part[-1] not in ['/', ':']):
|
||||
base_part += '/'
|
||||
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: Optional[str] = None) -> Dict[str, Any]:
|
||||
"""获取文件信息"""
|
||||
@@ -101,27 +119,6 @@ class ResourceService:
|
||||
path_obj = Path(safe_path)
|
||||
resource_root = cls._get_resource_root()
|
||||
|
||||
# 获取文件扩展名和类型
|
||||
file_extension = path_obj.suffix.lower() if path_obj.suffix else None
|
||||
|
||||
# 优先使用 magic 库检测 MIME 类型
|
||||
file_type = None
|
||||
if os.path.isfile(safe_path):
|
||||
try:
|
||||
file_type = magic.from_file(safe_path, mime=True)
|
||||
except Exception as e:
|
||||
logger.debug(f"magic 库检测文件类型失败: {e}")
|
||||
|
||||
# 如果 magic 检测失败或不可用,使用扩展名检测
|
||||
if not file_type and file_extension:
|
||||
file_type = cls._get_mime_type_from_extension(file_extension)
|
||||
|
||||
# 如果仍然没有类型,使用默认值
|
||||
if not file_type:
|
||||
file_type = 'application/octet-stream' if os.path.isfile(safe_path) else None
|
||||
|
||||
resource_type = cls._determine_resource_type(file_type, file_extension)
|
||||
|
||||
# 计算相对路径
|
||||
try:
|
||||
relative_path = os.path.relpath(safe_path, resource_root)
|
||||
@@ -135,33 +132,28 @@ class ResourceService:
|
||||
depth = 0
|
||||
|
||||
# 生成HTTP URL路径而不是文件系统路径
|
||||
if base_url:
|
||||
from urllib.parse import urljoin
|
||||
base_part = base_url.rstrip('/')
|
||||
static_part = settings.STATIC_URL.lstrip('/')
|
||||
relative_part = relative_path.lstrip('/')
|
||||
# 手动构建URL而不是使用urljoin,避免双斜杠问题
|
||||
if base_part.endswith(':') or (len(base_part) > 0 and base_part[-1] not in ['/', ':']):
|
||||
base_part += '/'
|
||||
http_url = f"{base_part}{static_part}/{relative_part}".replace('\\', '/').replace('//', '/').replace(':/', '://')
|
||||
else:
|
||||
http_url = f"{settings.STATIC_URL}/{relative_path}".replace('\\', '/').replace('//', '/')
|
||||
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,
|
||||
'path': http_url, # 返回HTTP URL而不是文件系统路径
|
||||
'file_url': http_url, # 统一使用file_url字段
|
||||
'relative_path': relative_path,
|
||||
'is_file': os.path.isfile(safe_path),
|
||||
'is_dir': os.path.isdir(safe_path),
|
||||
'is_dir': is_directory,
|
||||
'size': stat.st_size if os.path.isfile(safe_path) else None,
|
||||
'file_type': file_type,
|
||||
'file_extension': file_extension,
|
||||
'resource_type': resource_type,
|
||||
'created_time': datetime.fromtimestamp(stat.st_ctime),
|
||||
'modified_time': datetime.fromtimestamp(stat.st_mtime),
|
||||
'accessed_time': datetime.fromtimestamp(stat.st_atime),
|
||||
'parent_path': str(path_obj.parent),
|
||||
'depth': depth
|
||||
'created_time': created_time,
|
||||
'modified_time': modified_time,
|
||||
'is_hidden': is_hidden
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f'获取文件信息失败: {str(e)}')
|
||||
@@ -170,7 +162,6 @@ class ResourceService:
|
||||
@classmethod
|
||||
async def get_directory_list_service(
|
||||
cls,
|
||||
auth: AuthSchema,
|
||||
path: Optional[str] = None,
|
||||
include_hidden: bool = False,
|
||||
base_url: Optional[str] = None
|
||||
@@ -180,45 +171,10 @@ class ResourceService:
|
||||
# 如果没有指定路径,使用静态文件根目录
|
||||
if path is None:
|
||||
safe_path = cls._get_resource_root()
|
||||
# 对于根目录,返回静态URL路径
|
||||
if base_url:
|
||||
from urllib.parse import urljoin
|
||||
# 修复URL生成逻辑
|
||||
base_part = base_url.rstrip('/')
|
||||
static_part = settings.STATIC_URL.lstrip('/')
|
||||
if base_part.endswith(':') or (len(base_part) > 0 and base_part[-1] not in ['/', ':']):
|
||||
base_part += '/'
|
||||
display_path = f"{base_part}{static_part}".replace('//', '/').replace(':/', '://')
|
||||
else:
|
||||
display_path = settings.STATIC_URL
|
||||
display_path = cls._generate_http_url(safe_path, base_url)
|
||||
else:
|
||||
safe_path = cls._get_safe_path(path)
|
||||
# 对于子目录,生成相对于静态URL的路径
|
||||
resource_root = cls._get_resource_root()
|
||||
try:
|
||||
relative_path = os.path.relpath(safe_path, resource_root)
|
||||
if base_url:
|
||||
from urllib.parse import urljoin
|
||||
# 修复URL生成逻辑
|
||||
base_part = base_url.rstrip('/')
|
||||
static_part = settings.STATIC_URL.lstrip('/')
|
||||
relative_part = relative_path.lstrip('/')
|
||||
if base_part.endswith(':') or (len(base_part) > 0 and base_part[-1] not in ['/', ':']):
|
||||
base_part += '/'
|
||||
display_path = f"{base_part}{static_part}/{relative_part}".replace('\\', '/').replace('//', '/').replace(':/', '://')
|
||||
else:
|
||||
display_path = f"{settings.STATIC_URL}/{relative_path}".replace('\\', '/').replace('//', '/')
|
||||
except ValueError:
|
||||
if base_url:
|
||||
from urllib.parse import urljoin
|
||||
# 修复URL生成逻辑
|
||||
base_part = base_url.rstrip('/')
|
||||
static_part = settings.STATIC_URL.lstrip('/')
|
||||
if base_part.endswith(':') or (len(base_part) > 0 and base_part[-1] not in ['/', ':']):
|
||||
base_part += '/'
|
||||
display_path = f"{base_part}{static_part}".replace('//', '/').replace(':/', '://')
|
||||
else:
|
||||
display_path = settings.STATIC_URL
|
||||
display_path = cls._generate_http_url(safe_path, base_url)
|
||||
|
||||
if not os.path.exists(safe_path):
|
||||
raise CustomException(msg='目录不存在')
|
||||
@@ -259,14 +215,107 @@ class ResourceService:
|
||||
total_files=total_files,
|
||||
total_dirs=total_dirs,
|
||||
total_size=total_size
|
||||
).model_dump(mode='json')
|
||||
).model_dump()
|
||||
|
||||
except CustomException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f'获取目录列表失败: {str(e)}')
|
||||
raise CustomException(msg=f'获取目录列表失败: {str(e)}')
|
||||
|
||||
|
||||
@classmethod
|
||||
async def search_resources_service(
|
||||
cls,
|
||||
search: Optional[ResourceSearchQueryParam] = None,
|
||||
order_by: Optional[str] = None,
|
||||
base_url: Optional[str] = None
|
||||
) -> 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()
|
||||
|
||||
# 检查路径是否存在
|
||||
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 CustomException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f'搜索资源失败: {str(e)}')
|
||||
raise CustomException(msg=f'搜索资源失败: {str(e)}')
|
||||
|
||||
@classmethod
|
||||
async def get_resources_list_service(
|
||||
cls,
|
||||
search: Optional[ResourceSearchQueryParam] = None,
|
||||
order_by: Optional[str] = None,
|
||||
base_url: Optional[str] = None
|
||||
) -> List[Dict]:
|
||||
"""获取资源列表(用于分页查询)"""
|
||||
return await cls.search_resources_service(search=search, order_by=order_by, base_url=base_url)
|
||||
|
||||
@classmethod
|
||||
async def export_resource_service(cls, data_list: List[Dict[str, Any]]) -> bytes:
|
||||
"""导出资源列表"""
|
||||
mapping_dict = {
|
||||
'name': '文件名',
|
||||
'path': '文件路径',
|
||||
'size': '文件大小',
|
||||
'created_time': '创建时间',
|
||||
'modified_time': '修改时间',
|
||||
'parent_path': '父目录'
|
||||
}
|
||||
|
||||
# 复制数据并转换状态
|
||||
export_data = data_list.copy()
|
||||
|
||||
# 格式化文件大小
|
||||
for item in export_data:
|
||||
if item.get('size'):
|
||||
item['size'] = cls._format_file_size(item['size'])
|
||||
|
||||
return ExcelUtil.export_list2excel(list_data=export_data, mapping_dict=mapping_dict)
|
||||
|
||||
@classmethod
|
||||
async def _get_directory_stats(cls, path: str, include_hidden: bool = False) -> Dict[str, int]:
|
||||
"""递归获取目录统计信息"""
|
||||
@@ -294,126 +343,45 @@ class ResourceService:
|
||||
|
||||
return stats
|
||||
|
||||
@classmethod
|
||||
async def search_resources_service(
|
||||
cls,
|
||||
auth: AuthSchema,
|
||||
search: ResourceSearchSchema,
|
||||
base_url: Optional[str] = None
|
||||
) -> List[Dict]:
|
||||
"""搜索资源"""
|
||||
try:
|
||||
# 使用静态文件根目录作为搜索起点
|
||||
search_root = cls._get_resource_root()
|
||||
results = []
|
||||
|
||||
for root, dirs, files in os.walk(search_root):
|
||||
# 控制搜索深度
|
||||
try:
|
||||
depth = len(Path(root).relative_to(search_root).parts)
|
||||
except ValueError:
|
||||
depth = 0
|
||||
|
||||
if depth > search.max_depth:
|
||||
dirs.clear() # 阻止进一步深入
|
||||
continue
|
||||
|
||||
# 过滤隐藏文件夹(性能优化)
|
||||
if not search.include_hidden:
|
||||
dirs[:] = [d for d in dirs if not d.startswith('.')]
|
||||
files = [f for f in files if not f.startswith('.')]
|
||||
|
||||
# 优化:先过滤文件名,再进行详细检查
|
||||
if search.keyword:
|
||||
files = [f for f in files if search.keyword.lower() in f.lower()]
|
||||
|
||||
# 搜索文件
|
||||
for file in files:
|
||||
file_path = os.path.join(root, file)
|
||||
|
||||
# 优化:先进行快速检查
|
||||
if search.extensions:
|
||||
file_ext = os.path.splitext(file)[1].lower()
|
||||
if file_ext not in search.extensions:
|
||||
continue
|
||||
|
||||
file_info = cls._get_file_info(file_path, base_url)
|
||||
|
||||
if cls._match_search_criteria(file_info, search):
|
||||
results.append(file_info)
|
||||
|
||||
# 限制结果数量防止内存溢出
|
||||
if len(results) >= cls.MAX_SEARCH_RESULTS:
|
||||
logger.warning(f"搜索结果过多,已截断到前{cls.MAX_SEARCH_RESULTS}个")
|
||||
break
|
||||
|
||||
if len(results) >= cls.MAX_SEARCH_RESULTS:
|
||||
break
|
||||
|
||||
# 排序结果
|
||||
return cls._sort_results(results, search)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f'搜索资源失败: {str(e)}')
|
||||
raise CustomException(msg=f'搜索资源失败: {str(e)}')
|
||||
|
||||
@classmethod
|
||||
def _match_search_criteria(cls, file_info: Dict, search: ResourceSearchSchema) -> bool:
|
||||
"""检查文件是否匹配搜索条件"""
|
||||
if not file_info or not file_info.get('is_file'):
|
||||
return False
|
||||
|
||||
# 关键词搜索
|
||||
if search.keyword:
|
||||
if search.keyword.lower() not in file_info.get('name', '').lower():
|
||||
return False
|
||||
|
||||
# 文件类型搜索
|
||||
if search.file_type:
|
||||
if search.file_type.lower() != file_info.get('file_type', '').lower():
|
||||
return False
|
||||
|
||||
# 资源类型搜索
|
||||
if search.resource_type:
|
||||
if search.resource_type != file_info.get('resource_type'):
|
||||
return False
|
||||
|
||||
# 文件大小搜索
|
||||
file_size = file_info.get('size', 0) or 0
|
||||
if search.min_size and file_size < search.min_size:
|
||||
return False
|
||||
if search.max_size and file_size > search.max_size:
|
||||
return False
|
||||
|
||||
# 扩展名搜索
|
||||
if search.extensions:
|
||||
file_ext = file_info.get('file_extension', '')
|
||||
if file_ext not in search.extensions:
|
||||
return False
|
||||
|
||||
# 时间范围搜索
|
||||
modified_time = file_info.get('modified_time')
|
||||
if modified_time:
|
||||
if search.start_date and modified_time < search.start_date:
|
||||
return False
|
||||
if search.end_date and modified_time > search.end_date:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
@classmethod
|
||||
def _sort_results(cls, results: List[Dict], search: ResourceSearchSchema) -> List[Dict]:
|
||||
def _sort_results(cls, results: List[Dict], order_by: Optional[str] = None) -> List[Dict]:
|
||||
"""排序搜索结果"""
|
||||
sort_key = 'name'
|
||||
if hasattr(search, 'sort_by') and search.sort_by:
|
||||
sort_key = search.sort_by
|
||||
|
||||
reverse = False
|
||||
if hasattr(search, 'sort_order') and search.sort_order == 'desc':
|
||||
reverse = True
|
||||
|
||||
try:
|
||||
return sorted(results, key=lambda x: x.get(sort_key, ''), reverse=reverse)
|
||||
# 默认按名称升序排序
|
||||
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)
|
||||
if isinstance(sort_conditions, list):
|
||||
# 构建排序键函数
|
||||
def sort_key(item):
|
||||
keys = []
|
||||
for cond in sort_conditions:
|
||||
field = cond.get('field', 'name')
|
||||
direction = cond.get('direction', 'asc')
|
||||
# 获取字段值,默认为空字符串
|
||||
value = item.get(field, '')
|
||||
# 如果是日期字段,转换为可比较的格式
|
||||
if field in ['created_time', 'modified_time', 'accessed_time'] and value:
|
||||
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
|
||||
|
||||
@@ -470,38 +438,8 @@ class ResourceService:
|
||||
# 获取文件信息
|
||||
file_info = cls._get_file_info(file_path, base_url)
|
||||
|
||||
# 生成相对于资源根目录的URL路径
|
||||
resource_root = cls._get_resource_root()
|
||||
try:
|
||||
relative_path = os.path.relpath(file_path, resource_root)
|
||||
# 确保路径使用正斜杠(URL格式)
|
||||
file_url_path = relative_path.replace(os.sep, '/')
|
||||
# 如果提供了base_url,使用它生成完整URL,否则使用settings.STATIC_URL
|
||||
if base_url:
|
||||
from urllib.parse import urljoin
|
||||
# 修复URL生成逻辑
|
||||
base_part = base_url.rstrip('/')
|
||||
static_part = settings.STATIC_URL.lstrip('/')
|
||||
file_url_part = file_url_path.lstrip('/')
|
||||
if base_part.endswith(':') or (len(base_part) > 0 and base_part[-1] not in ['/', ':']):
|
||||
base_part += '/'
|
||||
file_url = f"{base_part}{static_part}/{file_url_part}".replace('//', '/').replace(':/', '://')
|
||||
else:
|
||||
file_url = f"{settings.STATIC_URL}/{file_url_path}".replace('//', '/')
|
||||
except ValueError:
|
||||
# 如果无法计算相对路径,使用文件名
|
||||
filename = os.path.basename(file_path)
|
||||
if base_url:
|
||||
from urllib.parse import urljoin
|
||||
# 修复URL生成逻辑
|
||||
base_part = base_url.rstrip('/')
|
||||
static_part = settings.STATIC_URL.lstrip('/')
|
||||
filename_part = filename.lstrip('/')
|
||||
if base_part.endswith(':') or (len(base_part) > 0 and base_part[-1] not in ['/', ':']):
|
||||
base_part += '/'
|
||||
file_url = f"{base_part}{static_part}/{filename_part}".replace('//', '/').replace(':/', '://')
|
||||
else:
|
||||
file_url = f"{settings.STATIC_URL}/{filename}"
|
||||
# 生成文件URL
|
||||
file_url = cls._generate_http_url(file_path, base_url)
|
||||
|
||||
logger.info(f"文件上传成功: {filename}")
|
||||
|
||||
@@ -510,7 +448,6 @@ class ResourceService:
|
||||
file_path=file_url, # 返回HTTP URL而不是文件系统路径
|
||||
file_url=file_url,
|
||||
file_size=file_info.get('size', 0),
|
||||
resource_type=file_info.get('resource_type', ResourceType.OTHER),
|
||||
upload_time=datetime.now()
|
||||
).model_dump(mode='json')
|
||||
|
||||
@@ -531,39 +468,9 @@ class ResourceService:
|
||||
raise CustomException(msg='路径不是文件')
|
||||
|
||||
# 生成HTTP URL路径而不是返回文件系统路径
|
||||
resource_root = cls._get_resource_root()
|
||||
try:
|
||||
relative_path = os.path.relpath(safe_path, resource_root)
|
||||
# 生成HTTP URL
|
||||
if base_url:
|
||||
from urllib.parse import urljoin
|
||||
# 修复URL生成逻辑
|
||||
base_part = base_url.rstrip('/')
|
||||
static_part = settings.STATIC_URL.lstrip('/')
|
||||
relative_part = relative_path.lstrip('/')
|
||||
if base_part.endswith(':') or (len(base_part) > 0 and base_part[-1] not in ['/', ':']):
|
||||
base_part += '/'
|
||||
http_url = f"{base_part}{static_part}/{relative_part}".replace('\\', '/').replace('//', '/').replace(':/', '://')
|
||||
else:
|
||||
http_url = f"{settings.STATIC_URL}/{relative_path}".replace('\\', '/').replace('//', '/')
|
||||
logger.info(f"生成文件访问URL: {http_url}")
|
||||
return http_url
|
||||
except ValueError:
|
||||
# 如果无法计算相对路径,使用文件名
|
||||
filename = os.path.basename(safe_path)
|
||||
if base_url:
|
||||
from urllib.parse import urljoin
|
||||
# 修复URL生成逻辑
|
||||
base_part = base_url.rstrip('/')
|
||||
static_part = settings.STATIC_URL.lstrip('/')
|
||||
filename_part = filename.lstrip('/')
|
||||
if base_part.endswith(':') or (len(base_part) > 0 and base_part[-1] not in ['/', ':']):
|
||||
base_part += '/'
|
||||
http_url = f"{base_part}{static_part}/{filename_part}".replace('//', '/').replace(':/', '://')
|
||||
else:
|
||||
http_url = f"{settings.STATIC_URL}/{filename}"
|
||||
logger.info(f"生成文件访问URL: {http_url}")
|
||||
return http_url
|
||||
http_url = cls._generate_http_url(safe_path, base_url)
|
||||
logger.info(f"生成文件访问URL: {http_url}")
|
||||
return http_url
|
||||
|
||||
except CustomException:
|
||||
raise
|
||||
@@ -596,6 +503,41 @@ class ResourceService:
|
||||
logger.error(f"删除失败 {path}: {str(e)}")
|
||||
raise CustomException(msg=f"删除失败 {path}: {str(e)}")
|
||||
|
||||
@classmethod
|
||||
async def batch_delete_service(cls, auth: AuthSchema, 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)
|
||||
logger.info(f"删除文件成功: {safe_path}")
|
||||
elif os.path.isdir(safe_path):
|
||||
shutil.rmtree(safe_path)
|
||||
success_paths.append(path)
|
||||
logger.info(f"删除目录成功: {safe_path}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"删除失败 {path}: {str(e)}")
|
||||
failed_paths.append(path)
|
||||
|
||||
return {
|
||||
"success": success_paths,
|
||||
"failed": failed_paths
|
||||
}
|
||||
|
||||
@classmethod
|
||||
async def move_file_service(cls, auth: AuthSchema, data: ResourceMoveSchema) -> None:
|
||||
"""移动文件或目录"""
|
||||
@@ -704,6 +646,10 @@ class ResourceService:
|
||||
# 生成新目录路径
|
||||
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='目录已存在')
|
||||
|
||||
@@ -717,118 +663,6 @@ class ResourceService:
|
||||
logger.error(f"创建目录失败: {str(e)}")
|
||||
raise CustomException(msg=f"创建目录失败: {str(e)}")
|
||||
|
||||
@classmethod
|
||||
async def get_stats_service(cls, auth: AuthSchema, base_url: Optional[str] = None) -> Dict:
|
||||
"""获取资源统计信息"""
|
||||
try:
|
||||
# 使用静态文件根目录
|
||||
stats_root = cls._get_resource_root()
|
||||
|
||||
# 获取磁盘空间信息
|
||||
disk_usage = psutil.disk_usage(stats_root)
|
||||
total_space = disk_usage.total
|
||||
free_space = disk_usage.free
|
||||
used_space = disk_usage.used
|
||||
|
||||
# 统计文件信息
|
||||
total_files = 0
|
||||
total_dirs = 0
|
||||
total_size = 0
|
||||
type_stats = {}
|
||||
extension_stats = {}
|
||||
|
||||
for root, dirs, files in os.walk(stats_root):
|
||||
total_dirs += len(dirs)
|
||||
|
||||
for file in files:
|
||||
file_path = os.path.join(root, file)
|
||||
try:
|
||||
file_info = cls._get_file_info(file_path, base_url)
|
||||
if file_info:
|
||||
total_files += 1
|
||||
total_size += file_info.get('size', 0) or 0
|
||||
|
||||
# 类型统计
|
||||
resource_type = file_info.get('resource_type')
|
||||
if resource_type:
|
||||
type_name = resource_type.value if hasattr(resource_type, 'value') else str(resource_type)
|
||||
type_stats[type_name] = type_stats.get(type_name, 0) + 1
|
||||
|
||||
# 扩展名统计
|
||||
extension = file_info.get('file_extension', '')
|
||||
if extension:
|
||||
extension_stats[extension] = extension_stats.get(extension, 0) + 1
|
||||
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
return ResourceStatsSchema(
|
||||
mount_point=stats_root,
|
||||
total_files=total_files,
|
||||
total_dirs=total_dirs,
|
||||
total_size=total_size,
|
||||
free_space=free_space,
|
||||
used_space=used_space,
|
||||
total_space=total_space,
|
||||
type_stats=type_stats,
|
||||
extension_stats=extension_stats
|
||||
).model_dump(mode='json')
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"获取统计信息失败: {str(e)}")
|
||||
raise CustomException(msg=f"获取统计信息失败: {str(e)}")
|
||||
|
||||
@classmethod
|
||||
async def export_resource_service(cls, data_list: List[Dict[str, Any]]) -> bytes:
|
||||
"""导出资源列表"""
|
||||
mapping_dict = {
|
||||
'name': '文件名',
|
||||
'path': '文件路径',
|
||||
'size': '文件大小',
|
||||
'file_type': 'MIME类型',
|
||||
'file_extension': '文件扩展名',
|
||||
'resource_type': '资源类型',
|
||||
'created_time': '创建时间',
|
||||
'modified_time': '修改时间',
|
||||
'parent_path': '父目录'
|
||||
}
|
||||
|
||||
# 复制数据并转换状态
|
||||
export_data = data_list.copy()
|
||||
for item in export_data:
|
||||
# 处理枚举值
|
||||
if 'resource_type' in item and hasattr(item['resource_type'], 'value'):
|
||||
item['resource_type'] = item['resource_type'].value
|
||||
|
||||
# 格式化文件大小
|
||||
if item.get('size'):
|
||||
item['size'] = cls._format_file_size(item['size'])
|
||||
|
||||
return ExcelUtil.export_list2excel(list_data=export_data, mapping_dict=mapping_dict)
|
||||
|
||||
@classmethod
|
||||
def _determine_resource_type(cls, file_type: str, file_extension: str) -> ResourceType:
|
||||
"""根据MIME类型和文件扩展名确定资源类型"""
|
||||
if not file_type:
|
||||
return ResourceType.OTHER
|
||||
|
||||
if file_type.startswith('image/'):
|
||||
return ResourceType.IMAGE
|
||||
elif file_type.startswith('video/'):
|
||||
return ResourceType.VIDEO
|
||||
elif file_type.startswith('audio/'):
|
||||
return ResourceType.AUDIO
|
||||
elif file_type in ['application/pdf', 'application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||
'application/vnd.ms-excel', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
'application/vnd.ms-powerpoint', 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
||||
'text/plain', 'text/csv']:
|
||||
return ResourceType.DOCUMENT
|
||||
elif file_type in ['application/zip', 'application/x-rar-compressed', 'application/x-7z-compressed',
|
||||
'application/gzip', 'application/x-tar']:
|
||||
return ResourceType.ARCHIVE
|
||||
else:
|
||||
return ResourceType.OTHER
|
||||
|
||||
@classmethod
|
||||
def _format_file_size(cls, size_bytes: int) -> str:
|
||||
"""格式化文件大小"""
|
||||
@@ -838,50 +672,7 @@ class ResourceService:
|
||||
size_names = ["B", "KB", "MB", "GB", "TB"]
|
||||
i = 0
|
||||
while size_bytes >= 1024 and i < len(size_names) - 1:
|
||||
size_bytes /= 1024.0
|
||||
size_bytes = int(size_bytes / 1024)
|
||||
i += 1
|
||||
|
||||
return f"{size_bytes:.2f}{size_names[i]}"
|
||||
|
||||
@classmethod
|
||||
def _get_mime_type_from_extension(cls, file_extension: str) -> str:
|
||||
"""根据文件扩展名获取MIME类型"""
|
||||
if not file_extension:
|
||||
return 'application/octet-stream'
|
||||
|
||||
# 扩展更全面的MIME类型映射
|
||||
mime_types = {
|
||||
# 图片类型
|
||||
'.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.png': 'image/png',
|
||||
'.gif': 'image/gif', '.bmp': 'image/bmp', '.webp': 'image/webp',
|
||||
'.svg': 'image/svg+xml', '.ico': 'image/x-icon', '.tiff': 'image/tiff',
|
||||
|
||||
# 视频类型
|
||||
'.mp4': 'video/mp4', '.avi': 'video/x-msvideo', '.mov': 'video/quicktime',
|
||||
'.wmv': 'video/x-ms-wmv', '.flv': 'video/x-flv', '.webm': 'video/webm',
|
||||
'.mkv': 'video/x-matroska', '.m4v': 'video/x-m4v',
|
||||
|
||||
# 音频类型
|
||||
'.mp3': 'audio/mpeg', '.wav': 'audio/wav', '.aac': 'audio/aac',
|
||||
'.ogg': 'audio/ogg', '.flac': 'audio/flac', '.m4a': 'audio/mp4',
|
||||
|
||||
# 文档类型
|
||||
'.pdf': 'application/pdf', '.doc': 'application/msword',
|
||||
'.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||
'.xls': 'application/vnd.ms-excel',
|
||||
'.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
'.ppt': 'application/vnd.ms-powerpoint',
|
||||
'.pptx': 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
||||
'.txt': 'text/plain', '.csv': 'text/csv', '.rtf': 'application/rtf',
|
||||
|
||||
# 代码文件
|
||||
'.html': 'text/html', '.css': 'text/css', '.js': 'application/javascript',
|
||||
'.json': 'application/json', '.xml': 'application/xml',
|
||||
'.py': 'text/x-python', '.java': 'text/x-java-source',
|
||||
|
||||
# 压缩文件
|
||||
'.zip': 'application/zip', '.rar': 'application/x-rar-compressed',
|
||||
'.7z': 'application/x-7z-compressed', '.tar': 'application/x-tar',
|
||||
'.gz': 'application/gzip', '.bz2': 'application/x-bzip2'
|
||||
}
|
||||
return mime_types.get(file_extension.lower(), 'application/octet-stream')
|
||||
Reference in New Issue
Block a user