""" 通用文件上传模块 支持: 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_system.models import SysFileSchema from iti_system.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_system.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_system.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_system.models import SysFileSchema schema = SysFileSchema() file_data = schema.dump(file_record) # 分片上传合并后不是秒传 file_data["instantUpload"] = False return success(file_data) @bp.delete("/chunk/") @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//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']} 字节空间", )