|
|
|
@ -7,7 +7,7 @@ from io import BytesIO
|
|
|
|
from typing import Dict, Optional
|
|
|
|
from typing import Dict, Optional
|
|
|
|
|
|
|
|
|
|
|
|
from flask import current_app, url_for
|
|
|
|
from flask import current_app, url_for
|
|
|
|
from sqlalchemy import select
|
|
|
|
from sqlalchemy import select, exists
|
|
|
|
|
|
|
|
|
|
|
|
from iti.applications.common.enums import StatusEnum
|
|
|
|
from iti.applications.common.enums import StatusEnum
|
|
|
|
from iti.applications.common.exceptions.biz_exp import BizException
|
|
|
|
from iti.applications.common.exceptions.biz_exp import BizException
|
|
|
|
@ -40,9 +40,12 @@ class SysFileService:
|
|
|
|
resolved_storage_type = cls._resolve_storage_type(storage_type, directory_id)
|
|
|
|
resolved_storage_type = cls._resolve_storage_type(storage_type, directory_id)
|
|
|
|
|
|
|
|
|
|
|
|
existing = db.session.scalar(
|
|
|
|
existing = db.session.scalar(
|
|
|
|
select(SysFile)
|
|
|
|
select(
|
|
|
|
.filter_by(file_hash=file_hash, status=StatusEnum.ENABLED.value)
|
|
|
|
exists(SysFile).where(
|
|
|
|
.limit(1)
|
|
|
|
SysFile.file_hash == file_hash,
|
|
|
|
|
|
|
|
SysFile.status == StatusEnum.ENABLED.value,
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
)
|
|
|
|
)
|
|
|
|
)
|
|
|
|
if existing and existing.storage_type == resolved_storage_type:
|
|
|
|
if existing and existing.storage_type == resolved_storage_type:
|
|
|
|
# 秒传:更新已有记录
|
|
|
|
# 秒传:更新已有记录
|
|
|
|
@ -56,7 +59,9 @@ class SysFileService:
|
|
|
|
ext = os.path.splitext(file.filename or "")[1]
|
|
|
|
ext = os.path.splitext(file.filename or "")[1]
|
|
|
|
# 为支持多存储类型,前缀加上存储类型,使用冒号分隔,避免唯一索引冲突
|
|
|
|
# 为支持多存储类型,前缀加上存储类型,使用冒号分隔,避免唯一索引冲突
|
|
|
|
file_key = f"{resolved_storage_type}:{datetime.now():%Y%m%d}/{file_hash}{ext}"
|
|
|
|
file_key = f"{resolved_storage_type}:{datetime.now():%Y%m%d}/{file_hash}{ext}"
|
|
|
|
upload_result = storage.upload(BytesIO(file_bytes), file_key, getattr(file, "mimetype", None))
|
|
|
|
upload_result = storage.upload(
|
|
|
|
|
|
|
|
BytesIO(file_bytes), file_key, getattr(file, "mimetype", None)
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
new_file = SysFile(
|
|
|
|
new_file = SysFile(
|
|
|
|
filename=file.filename,
|
|
|
|
filename=file.filename,
|
|
|
|
@ -175,7 +180,9 @@ class SysFileService:
|
|
|
|
file_size=file_size,
|
|
|
|
file_size=file_size,
|
|
|
|
extension=os.path.splitext(upload_data.get("filename") or "")[1],
|
|
|
|
extension=os.path.splitext(upload_data.get("filename") or "")[1],
|
|
|
|
storage_type=storage_type,
|
|
|
|
storage_type=storage_type,
|
|
|
|
storage_info=upload_data.get("storage_info") if storage_type != "local" else None,
|
|
|
|
storage_info=upload_data.get("storage_info")
|
|
|
|
|
|
|
|
if storage_type != "local"
|
|
|
|
|
|
|
|
else None,
|
|
|
|
directory_id=upload_data.get("directory_id"),
|
|
|
|
directory_id=upload_data.get("directory_id"),
|
|
|
|
metadata_=metadata if metadata else None,
|
|
|
|
metadata_=metadata if metadata else None,
|
|
|
|
status=StatusEnum.ENABLED.value,
|
|
|
|
status=StatusEnum.ENABLED.value,
|
|
|
|
@ -250,7 +257,9 @@ class SysFileService:
|
|
|
|
return storage.get_preview_url(file_obj.file_key, expires=3600)
|
|
|
|
return storage.get_preview_url(file_obj.file_key, expires=3600)
|
|
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
@classmethod
|
|
|
|
def get_thumbnail_url(cls, file_id: str, width: int = 200, height: int = 200, mode: str = "fit") -> Optional[str]:
|
|
|
|
def get_thumbnail_url(
|
|
|
|
|
|
|
|
cls, file_id: str, width: int = 200, height: int = 200, mode: str = "fit"
|
|
|
|
|
|
|
|
) -> Optional[str]:
|
|
|
|
"""获取缩略图URL
|
|
|
|
"""获取缩略图URL
|
|
|
|
|
|
|
|
|
|
|
|
- local: 返回后端缩略图路由
|
|
|
|
- local: 返回后端缩略图路由
|
|
|
|
@ -278,31 +287,33 @@ class SysFileService:
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
@classmethod
|
|
|
|
def get_thumbnail(cls, file_id: str, width: int = 200, height: int = 200, mode: str = "fit") -> BytesIO:
|
|
|
|
def get_thumbnail(
|
|
|
|
|
|
|
|
cls, file_id: str, width: int = 200, height: int = 200, mode: str = "fit"
|
|
|
|
|
|
|
|
) -> BytesIO:
|
|
|
|
"""生成缩略图"""
|
|
|
|
"""生成缩略图"""
|
|
|
|
file_obj = cls.get_file_by_id(file_id)
|
|
|
|
file_obj = cls.get_file_by_id(file_id)
|
|
|
|
|
|
|
|
|
|
|
|
# 非图片类型不支持缩略图
|
|
|
|
# 非图片类型不支持缩略图
|
|
|
|
if not file_obj.mime_type or not file_obj.mime_type.startswith("image/"):
|
|
|
|
if not file_obj.mime_type or not file_obj.mime_type.startswith("image/"):
|
|
|
|
raise BizException("该文件类型不支持缩略图", code=400)
|
|
|
|
raise BizException("该文件类型不支持缩略图", code=400)
|
|
|
|
|
|
|
|
|
|
|
|
# OSS 存储:使用 OSS 图片处理
|
|
|
|
# OSS 存储:使用 OSS 图片处理
|
|
|
|
if file_obj.storage_type != "local":
|
|
|
|
if file_obj.storage_type != "local":
|
|
|
|
# 对于 OSS,直接重定向到带图片处理参数的URL
|
|
|
|
# 对于 OSS,直接重定向到带图片处理参数的URL
|
|
|
|
# TODO: 各OSS需要实现 get_thumbnail_url 方法
|
|
|
|
# TODO: 各OSS需要实现 get_thumbnail_url 方法
|
|
|
|
return cls.download_file(file_id)
|
|
|
|
return cls.download_file(file_id)
|
|
|
|
|
|
|
|
|
|
|
|
# 本地存储:使用 Pillow 生成缩略图
|
|
|
|
# 本地存储:使用 Pillow 生成缩略图
|
|
|
|
try:
|
|
|
|
try:
|
|
|
|
from PIL import Image
|
|
|
|
from PIL import Image
|
|
|
|
from io import BytesIO as PILBytesIO
|
|
|
|
from io import BytesIO as PILBytesIO
|
|
|
|
except ImportError:
|
|
|
|
except ImportError:
|
|
|
|
raise BizException("需要安装 Pillow: pip install Pillow", code=500)
|
|
|
|
raise BizException("需要安装 Pillow: pip install Pillow", code=500)
|
|
|
|
|
|
|
|
|
|
|
|
# 下载原图
|
|
|
|
# 下载原图
|
|
|
|
file_stream = cls.download_file(file_id)
|
|
|
|
file_stream = cls.download_file(file_id)
|
|
|
|
img = Image.open(file_stream)
|
|
|
|
img = Image.open(file_stream)
|
|
|
|
|
|
|
|
|
|
|
|
# 转换为 RGB(处理 PNG 透明通道等)
|
|
|
|
# 转换为 RGB(处理 PNG 透明通道等)
|
|
|
|
if img.mode in ("RGBA", "LA", "P"):
|
|
|
|
if img.mode in ("RGBA", "LA", "P"):
|
|
|
|
background = Image.new("RGB", img.size, (255, 255, 255))
|
|
|
|
background = Image.new("RGB", img.size, (255, 255, 255))
|
|
|
|
@ -312,7 +323,7 @@ class SysFileService:
|
|
|
|
img = background
|
|
|
|
img = background
|
|
|
|
elif img.mode != "RGB":
|
|
|
|
elif img.mode != "RGB":
|
|
|
|
img = img.convert("RGB")
|
|
|
|
img = img.convert("RGB")
|
|
|
|
|
|
|
|
|
|
|
|
# 根据模式调整大小
|
|
|
|
# 根据模式调整大小
|
|
|
|
if mode == "fill":
|
|
|
|
if mode == "fill":
|
|
|
|
# 填充模式:裁剪居中
|
|
|
|
# 填充模式:裁剪居中
|
|
|
|
@ -330,12 +341,12 @@ class SysFileService:
|
|
|
|
else: # fit (默认)
|
|
|
|
else: # fit (默认)
|
|
|
|
# 适应模式:保持比例
|
|
|
|
# 适应模式:保持比例
|
|
|
|
img.thumbnail((width, height), Image.Resampling.LANCZOS)
|
|
|
|
img.thumbnail((width, height), Image.Resampling.LANCZOS)
|
|
|
|
|
|
|
|
|
|
|
|
# 保存为 JPEG
|
|
|
|
# 保存为 JPEG
|
|
|
|
output = PILBytesIO()
|
|
|
|
output = PILBytesIO()
|
|
|
|
img.save(output, format="JPEG", quality=85, optimize=True)
|
|
|
|
img.save(output, format="JPEG", quality=85, optimize=True)
|
|
|
|
output.seek(0)
|
|
|
|
output.seek(0)
|
|
|
|
|
|
|
|
|
|
|
|
return output
|
|
|
|
return output
|
|
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
@classmethod
|
|
|
|
|