Files

207 lines
6.9 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""通用文件上传路由 - 腾讯云COS"""
import uuid
import os
from datetime import datetime
from fastapi import APIRouter, UploadFile, File, Depends, HTTPException
from sqlalchemy.orm import Session
from routers.auth import get_current_user
from models.user import User
from models.system_config import SystemConfig
from database import get_db
from config import MAX_UPLOAD_SIZE, MAX_ATTACHMENT_SIZE, UPLOAD_DIR
from models.attachment import Attachment
router = APIRouter()
ALLOWED_IMAGE_TYPES = ["image/jpeg", "image/png", "image/gif", "image/webp"]
ALLOWED_ATTACHMENT_TYPES = [
"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",
"application/zip",
"application/x-zip-compressed",
"application/x-rar-compressed",
"application/vnd.rar",
]
ALLOWED_ATTACHMENT_EXTS = {
"pdf", "doc", "docx", "xls", "xlsx", "ppt", "pptx", "zip", "rar",
}
def _get_cos_config(db: Session) -> dict:
"""从数据库读取COS配置"""
keys = ["cos_secret_id", "cos_secret_key", "cos_bucket", "cos_region", "cos_custom_domain"]
config = {}
for k in keys:
row = db.query(SystemConfig).filter(SystemConfig.key == k).first()
config[k] = row.value if row else ""
return config
def _get_cos_client(db: Session):
"""获取COS客户端实例未配置则返回None"""
config = _get_cos_config(db)
secret_id = config.get("cos_secret_id", "")
secret_key = config.get("cos_secret_key", "")
bucket = config.get("cos_bucket", "")
region = config.get("cos_region", "")
if not all([secret_id, secret_key, bucket, region]):
return None, config
try:
from qcloud_cos import CosConfig, CosS3Client
cos_config = CosConfig(Region=region, SecretId=secret_id, SecretKey=secret_key)
client = CosS3Client(cos_config)
return client, config
except ImportError:
return None, config
def _build_cos_url(config: dict, object_key: str) -> str:
"""构建COS访问URL"""
custom_domain = config.get("cos_custom_domain", "")
if custom_domain:
return f"https://{custom_domain}/{object_key}"
bucket = config.get("cos_bucket", "")
region = config.get("cos_region", "")
return f"https://{bucket}.cos.{region}.myqcloud.com/{object_key}"
@router.post("/image")
async def upload_image(
file: UploadFile = File(...),
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db),
):
"""上传图片优先OSS未配置则本地存储"""
# 验证文件类型
if file.content_type not in ALLOWED_IMAGE_TYPES:
raise HTTPException(status_code=400, detail="不支持的文件类型,仅支持 JPG/PNG/GIF/WEBP")
# 读取文件内容
content = await file.read()
# 验证文件大小
if len(content) > MAX_UPLOAD_SIZE:
raise HTTPException(status_code=400, detail=f"文件过大,最大支持 {MAX_UPLOAD_SIZE // (1024*1024)}MB")
# 生成唯一文件名
ext = file.filename.split(".")[-1].lower() if "." in file.filename else "png"
date_prefix = datetime.now().strftime("%Y/%m")
filename = f"{uuid.uuid4().hex}.{ext}"
object_key = f"images/{date_prefix}/{filename}"
# 尝试COS上传从数据库读取配置
client, config = _get_cos_client(db)
if client:
try:
client.put_object(
Bucket=config.get("cos_bucket", ""),
Body=content,
Key=object_key,
ContentType=file.content_type,
)
url = _build_cos_url(config, object_key)
return {"url": url, "storage": "cos"}
except Exception as e:
raise HTTPException(status_code=500, detail=f"COS上传失败: {str(e)}")
else:
# 降级到本地存储
os.makedirs(UPLOAD_DIR, exist_ok=True)
filepath = os.path.join(UPLOAD_DIR, filename)
with open(filepath, "wb") as f:
f.write(content)
return {"url": f"/uploads/{filename}", "storage": "local"}
@router.post("/attachment")
async def upload_attachment(
file: UploadFile = File(...),
post_id: int = 0,
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db),
):
"""上传附件到COS支持 PDF/Word/Excel/PPT/ZIP/RAR"""
# 验证文件扩展名
ext = file.filename.split(".")[-1].lower() if "." in file.filename else ""
if ext not in ALLOWED_ATTACHMENT_EXTS:
raise HTTPException(
status_code=400,
detail=f"不支持的文件类型,仅支持: {', '.join(sorted(ALLOWED_ATTACHMENT_EXTS))}",
)
# 读取文件内容
content = await file.read()
if len(content) > MAX_ATTACHMENT_SIZE:
raise HTTPException(
status_code=400,
detail=f"文件过大,最大支持 {MAX_ATTACHMENT_SIZE // (1024*1024)}MB",
)
# 生成唯一文件名
date_prefix = datetime.now().strftime("%Y/%m")
unique_name = f"{uuid.uuid4().hex}.{ext}"
object_key = f"attachments/{date_prefix}/{unique_name}"
# 上传到COS
client, config = _get_cos_client(db)
if not client:
raise HTTPException(status_code=500, detail="对象存储未配置,无法上传附件")
try:
client.put_object(
Bucket=config.get("cos_bucket", ""),
Body=content,
Key=object_key,
ContentType=file.content_type or "application/octet-stream",
)
except Exception as e:
raise HTTPException(status_code=500, detail=f"COS上传失败: {str(e)}")
url = _build_cos_url(config, object_key)
# 写入数据库
attachment = Attachment(
post_id=post_id if post_id else None,
user_id=current_user.id,
filename=file.filename,
storage_key=object_key,
url=url,
file_size=len(content),
file_type=file.content_type or "application/octet-stream",
)
db.add(attachment)
db.commit()
db.refresh(attachment)
return {
"id": attachment.id,
"filename": attachment.filename,
"url": attachment.url,
"file_size": attachment.file_size,
"file_type": attachment.file_type,
}
@router.put("/attachment/{attachment_id}/post")
async def update_attachment_post(
attachment_id: int,
post_id: int = 0,
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db),
):
"""将附件关联到帖子(新建帖子后回填 post_id"""
attachment = db.query(Attachment).filter(
Attachment.id == attachment_id,
Attachment.user_id == current_user.id,
).first()
if not attachment:
raise HTTPException(status_code=404, detail="附件不存在")
attachment.post_id = post_id
db.commit()
return {"message": "ok"}