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.

206 lines
5.9 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.

"""
通用文件上传模块
支持:
1. 小文件直接上传
2. 大文件分片上传(自定义协议)
3. 秒传(基于文件哈希)
4. 支持多存储类型local/oss/minio
"""
from __future__ import annotations
import hashlib
from io import BytesIO
from apiflask import APIBlueprint
from flask import request
from flask_jwt_extended import jwt_required, get_jwt_identity
from iti.applications.common.utils import success
from iti.applications.common.exceptions.biz_exp import BizException
from iti.applications.models import SysFileSchema
from iti.applications.service.sys.sys_file import SysFileService
from .schemas.upload import (
UploadIn,
ChunkUploadInitIn,
ChunkUploadIn,
ChunkUploadMergeIn,
)
bp = APIBlueprint("common_upload", __name__, url_prefix="/upload", tag="通用.上传")
# ---------------------------------------------------------------------------
# 小文件直接上传
# ---------------------------------------------------------------------------
@bp.post("")
@jwt_required()
@bp.doc(security="JWT")
@bp.input(UploadIn, location="form_and_files")
def upload_file(form_and_files_data):
"""文件直接上传"""
file = form_and_files_data["file"]
directory_id = form_and_files_data.get("directoryId")
storage_type = form_and_files_data.get("storageType")
# 从 UploadIn Schema 中获取所有已定义的字段名
schema_fields = set(UploadIn().fields.keys())
# 收集所有非 Schema 定义的额外字段作为 metadata
metadata = {}
for key, value in form_and_files_data.items():
if key not in schema_fields:
metadata[key] = value
result = SysFileService.upload_file(
file,
directory_id=directory_id,
metadata=metadata,
storage_type=storage_type,
)
# result 是一个字典,包含 file_record 和 instantUpload
file_record = result["file"]
instant_upload = result["instantUpload"]
# 使用 Schema 序列化文件记录
from iti.applications.models import SysFileSchema
schema = SysFileSchema()
file_data = schema.dump(file_record)
# 添加秒传标识
file_data["instantUpload"] = instant_upload
return success(file_data)
# ---------------------------------------------------------------------------
# 分片上传
# ---------------------------------------------------------------------------
@bp.post("/chunk/init")
@jwt_required()
@bp.doc(security="JWT")
@bp.input(ChunkUploadInitIn)
def chunk_upload_init(json_data):
"""初始化分片上传"""
result = SysFileService.init_chunk_upload(
filename=json_data["filename"],
file_size=json_data["file_size"],
file_hash=json_data.get("file_hash"),
chunk_size=json_data.get("chunk_size", 2 * 1024 * 1024),
total_chunks=json_data.get("total_chunks"),
directory_id=json_data.get("directory_id"),
storage_type=json_data.get("storage_type"),
metadata=json_data.get("metadata", {}),
)
# 如果是秒传,需要序列化文件对象
if result.get("instantUpload"):
from iti.applications.models import SysFileSchema
schema = SysFileSchema()
file_data = schema.dump(result["file"])
file_data["instantUpload"] = True
return success(file_data)
# 非秒传返回上传ID和已上传分片列表已经是驼峰命名
return success(result)
@bp.post("/chunk/upload")
@jwt_required()
@bp.doc(security="JWT")
@bp.input(ChunkUploadIn, location="form_and_files")
def chunk_upload(form_and_files_data):
"""上传分片"""
upload_id = form_and_files_data["upload_id"]
chunk_index = form_and_files_data["chunk_index"]
chunk_hash = form_and_files_data.get("chunk_hash")
chunk_file = form_and_files_data["file"]
chunk_data = chunk_file.read()
if chunk_hash:
actual_hash = hashlib.md5(chunk_data).hexdigest()
if actual_hash != chunk_hash:
raise BizException("分片数据校验失败", code=400)
result = SysFileService.upload_chunk(
upload_id=upload_id,
chunk_index=chunk_index,
chunk_data=chunk_data,
)
return success(result)
@bp.post("/chunk/merge")
@jwt_required()
@bp.doc(security="JWT")
@bp.input(ChunkUploadMergeIn)
def chunk_upload_merge(json_data):
"""合并分片"""
upload_id = json_data["upload_id"]
file_hash = json_data.get("file_hash")
file_record = SysFileService.merge_chunks(
upload_id=upload_id,
file_hash=file_hash,
)
# 使用 Schema 序列化文件记录
from iti.applications.models import SysFileSchema
schema = SysFileSchema()
file_data = schema.dump(file_record)
# 分片上传合并后不是秒传
file_data["instantUpload"] = False
return success(file_data)
@bp.delete("/chunk/<string:upload_id>")
@jwt_required()
@bp.doc(security="JWT")
def chunk_upload_abort(upload_id: str):
"""取消分片上传"""
SysFileService.abort_chunk_upload(upload_id)
return success(message="上传已取消")
@bp.get("/chunk/<string:upload_id>/progress")
@jwt_required()
@bp.doc(security="JWT")
def chunk_upload_progress(upload_id: str):
"""查询上传进度"""
progress = SysFileService.get_chunk_upload_progress(upload_id)
return success(progress)
@bp.post("/chunk/cleanup")
@jwt_required()
@bp.doc(security="JWT")
def cleanup_expired_chunks():
"""清理过期的分片上传临时文件(管理员接口)"""
from flask_jwt_extended import get_jwt
# 检查是否有管理员权限(可选,根据你的权限系统调整)
# claims = get_jwt()
# if not claims.get("is_admin"):
# raise BizException("需要管理员权限", code=403)
# 默认清理7天前的文件
days = request.args.get("days", 7, type=int)
result = SysFileService.cleanup_expired_chunk_uploads(days)
return success(
result,
message=f"清理完成,删除 {result['cleaned_dirs']} 个目录,释放 {result['cleaned_size']} 字节空间",
)