You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

215 lines
7.8 KiB
Python

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

import os
from pathlib import Path
from fastapi import UploadFile
from app.config.setting import settings
from app.core.base_schema import AuthSchema, DownloadFileSchema
from app.core.exceptions import CustomException
from app.core.logger import logger
from app.utils.minio_util import MinioUtil
from app.utils.upload_util import UploadUtil
from .crud import FileCRUD
from .schema import FileCreateSchema, FileOutSchema, FileQueryParam
class FileService:
"""
文件管理服务层
"""
@classmethod
async def upload_service(
cls,
auth: AuthSchema,
base_url: str,
file: UploadFile,
upload_type: str = "file",
target_path: str | None = None,
business_type: str | None = None,
business_id: int | None = None,
) -> FileOutSchema:
"""上传文件"""
original_name = file.filename or ""
content_type = file.content_type
file_size = file.size or 0
filename, filepath, file_url = await UploadUtil.upload_file(
file=file,
base_url=base_url,
upload_type=upload_type,
target_path=target_path,
)
storage_type = "local"
bucket_name = None
object_name = None
final_file_url = f"{file_url}"
final_file_path = f"{filepath}"
if settings.MINIO_ENABLE:
object_name = cls._build_object_name(upload_type=upload_type, filename=filename, filepath=filepath)
try:
final_file_url = MinioUtil.upload_file(
object_name=object_name,
file_path=filepath,
content_type=content_type,
)
except Exception:
UploadUtil.delete_file(filepath)
raise
bucket_name = settings.MINIO_BUCKET_NAME
storage_type = "minio"
final_file_path = object_name
UploadUtil.delete_file(filepath)
try:
file_record = await FileCRUD(auth).create(
data=FileCreateSchema(
storage_type=storage_type,
bucket_name=bucket_name,
object_name=object_name,
original_name=original_name,
file_name=filename,
file_ext=UploadUtil.get_extension_from_filename(filename),
content_type=content_type,
file_size=file_size,
file_path=final_file_path,
file_url=final_file_url,
upload_type=upload_type,
business_type=business_type,
business_id=business_id,
)
)
except Exception:
cls._delete_uploaded_object(storage_type=storage_type, bucket_name=bucket_name, object_name=object_name, file_path=final_file_path)
raise
return cls._to_out_schema(file_record)
@classmethod
async def page(
cls,
auth: AuthSchema,
page_no: int,
page_size: int,
search: FileQueryParam | None = None,
order_by: list[dict[str, str]] | None = None,
) -> dict:
offset = (page_no - 1) * page_size
page_result = await FileCRUD(auth).page(
offset=offset,
limit=page_size,
order_by=order_by or [{"id": "desc"}],
search=vars(search) if search else None,
out_schema=FileOutSchema,
)
for item in page_result.items:
item["origin_name"] = item.get("original_name")
return page_result
@classmethod
async def detail_service(cls, auth: AuthSchema, id: int) -> FileOutSchema:
file_record = await FileCRUD(auth).get_or_404(id=id, msg="文件记录不存在")
return cls._to_out_schema(file_record)
@classmethod
async def delete_service(cls, auth: AuthSchema, ids: list[int], delete_object: bool = False) -> None:
if len(ids) < 1:
raise CustomException(msg="删除失败,删除对象不能为空")
records = await FileCRUD(auth).get_list(search={"id": ("in", ids)})
record_map = {record.id: record for record in records}
for file_id in ids:
if file_id not in record_map:
raise CustomException(msg="删除失败,文件记录不存在")
if delete_object:
for record in records:
cls._delete_storage_object(record)
await FileCRUD(auth).delete(ids=ids)
@classmethod
def _to_out_schema(cls, file_record) -> FileOutSchema:
result = FileOutSchema.model_validate(file_record)
result.origin_name = result.original_name
return result
@classmethod
def _build_object_name(cls, upload_type: str, filename: str, filepath: Path) -> str:
try:
relative_path = filepath.resolve().relative_to(settings.UPLOAD_FILE_PATH.resolve())
return relative_path.as_posix()
except ValueError:
return f"{upload_type}/{filename}"
@classmethod
def _delete_storage_object(cls, file_record) -> None:
if file_record.storage_type == "minio" and file_record.object_name:
MinioUtil.remove_object(object_name=file_record.object_name, bucket_name=file_record.bucket_name)
return
if file_record.file_path:
UploadUtil.delete_file(Path(file_record.file_path))
@classmethod
def _delete_uploaded_object(cls, storage_type: str, bucket_name: str | None, object_name: str | None, file_path: str | None) -> None:
try:
if storage_type == "minio" and object_name:
MinioUtil.remove_object(object_name=object_name, bucket_name=bucket_name)
return
if file_path:
UploadUtil.delete_file(Path(file_path))
except Exception as e:
logger.error(f"清理上传文件失败: {e}")
@classmethod
def to_upload_response(cls, file_record: FileOutSchema):
from app.core.base_schema import UploadResponseSchema
return UploadResponseSchema(
file_path=file_record.file_path,
file_name=file_record.file_name,
origin_name=file_record.original_name,
file_url=file_record.file_url,
)
@classmethod
async def presigned_url_service(cls, auth: AuthSchema, id: int) -> str:
file_record = await FileCRUD(auth).get_or_404(id=id, msg="文件记录不存在")
if file_record.storage_type != "minio" or not file_record.object_name:
if file_record.file_url:
return file_record.file_url
raise CustomException(msg="该文件不是MinIO文件无法生成预签名地址")
return MinioUtil.presigned_get_url(object_name=file_record.object_name, bucket_name=file_record.bucket_name)
@classmethod
async def download_service(cls, file_path: str) -> DownloadFileSchema:
"""下载文件"""
if not file_path:
raise CustomException(msg="请选择要下载的文件")
dangerous_patterns = ["../", "..\\", "\0"]
for pattern in dangerous_patterns:
if pattern in file_path:
logger.error(f"检测到路径穿越攻击: {file_path}")
raise CustomException(msg="非法的文件路径")
upload_root = settings.UPLOAD_FILE_PATH.resolve()
abs_path = os.path.normpath(os.path.abspath(file_path))
if not abs_path.startswith(str(upload_root)):
logger.error(f"路径不在上传目录内: {file_path}")
raise CustomException(msg="非法的文件路径")
if not UploadUtil.check_file_exists(abs_path):
raise CustomException(msg="文件不存在")
file_name = UploadUtil.download_file(abs_path)
return DownloadFileSchema(
file_path=abs_path,
file_name=str(file_name),
)