Your Name 8ab2d869fc feat: 阿里云 OSS 迁移至腾讯云 COS + 完善部署配置
COS 迁移:
- 后端签名服务改为 COS HMAC-SHA1 表单直传签名
- config.py: OSS_* 配置项替换为 COS_SECRET_ID/KEY/REGION/BUCKET_NAME/CDN_DOMAIN
- upload.py: UploadPolicyResponse 改为 COS 字段
- 前端 useOSSUpload hook: FormData 字段改为 COS 格式
- 前端 api.ts: UploadPolicyResponse 类型对齐

部署配置:
- docker-compose.yml: 新增 Nginx + 前端容器,数据卷宿主机持久化
- Nginx: HTTPS + HTTP/2 + SSE 长连接 + API/前端反向代理
- backup.sh: PostgreSQL 每日备份 → 本地 + COS
- .env.example: 更新为 COS 配置模板

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-10 10:28:13 +08:00

161 lines
4.3 KiB
Python
Raw 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 time
import hmac
import base64
import hashlib
import json
from typing import Optional
from datetime import datetime
from app.config import settings
def generate_upload_policy(
max_size_mb: int = 500,
expire_seconds: int = 3600,
upload_dir: Optional[str] = None,
) -> dict:
"""
生成前端直传 COS 所需的 Policy 和签名
COS 表单直传签名流程:
1. key_time = "{start_time};{end_time}"
2. sign_key = HMAC-SHA1(secret_key, key_time)
3. policy JSON → Base64 编码
4. string_to_sign = SHA1(policy_base64)
5. signature = HMAC-SHA1(sign_key, string_to_sign)
Returns:
{
"q_sign_algorithm": "sha1",
"q_ak": "SecretId",
"q_key_time": "{start};{end}",
"q_signature": "...",
"policy": "base64 encoded policy",
"host": "https://bucket.cos.region.myqcloud.com",
"dir": "uploads/2026/02/",
"expire": 1234567890,
}
"""
if not settings.COS_SECRET_ID or not settings.COS_SECRET_KEY:
raise ValueError("COS 配置未设置")
# 计算时间范围
start_time = int(time.time())
end_time = start_time + expire_seconds
# key_time: "{start};{end}"
key_time = f"{start_time};{end_time}"
# 默认上传目录uploads/年/月/
if upload_dir is None:
now = datetime.now()
upload_dir = f"uploads/{now.year}/{now.month:02d}/"
# 1. sign_key = HMAC-SHA1(secret_key, key_time)
sign_key = hmac.new(
settings.COS_SECRET_KEY.encode(),
key_time.encode(),
hashlib.sha1,
).hexdigest()
# 2. 构建 PolicyCOS 表单上传 Policy 格式)
policy_dict = {
"expiration": datetime.utcfromtimestamp(end_time).strftime(
"%Y-%m-%dT%H:%M:%S.000Z"
),
"conditions": [
{"bucket": settings.COS_BUCKET_NAME},
["starts-with", "$key", upload_dir],
{"q-sign-algorithm": "sha1"},
{"q-ak": settings.COS_SECRET_ID},
{"q-sign-time": key_time},
["content-length-range", 0, max_size_mb * 1024 * 1024],
],
}
# 3. Base64 编码 Policy
policy_json = json.dumps(policy_dict)
policy_base64 = base64.b64encode(policy_json.encode()).decode()
# 4. string_to_sign = SHA1(policy_base64)
string_to_sign = hashlib.sha1(policy_base64.encode()).hexdigest()
# 5. signature = HMAC-SHA1(sign_key, string_to_sign)
signature = hmac.new(
sign_key.encode(),
string_to_sign.encode(),
hashlib.sha1,
).hexdigest()
# 构建 Host
host = f"https://{settings.COS_BUCKET_NAME}.cos.{settings.COS_REGION}.myqcloud.com"
return {
"q_sign_algorithm": "sha1",
"q_ak": settings.COS_SECRET_ID,
"q_key_time": key_time,
"q_signature": signature,
"policy": policy_base64,
"host": host,
"dir": upload_dir,
"expire": end_time,
}
def get_file_url(file_key: str) -> str:
"""
获取文件的访问 URL
优先使用 CDN 域名,否则用 COS 源站域名。
Args:
file_key: 文件在 COS 中的 key"uploads/2026/02/video.mp4"
Returns:
完整的访问 URL
"""
if settings.COS_CDN_DOMAIN:
host = settings.COS_CDN_DOMAIN
else:
host = f"https://{settings.COS_BUCKET_NAME}.cos.{settings.COS_REGION}.myqcloud.com"
# 确保 host 以 https:// 开头
if not host.startswith("http"):
host = f"https://{host}"
# 确保 host 不以 / 结尾
host = host.rstrip("/")
# 确保 file_key 不以 / 开头
file_key = file_key.lstrip("/")
return f"{host}/{file_key}"
def parse_file_key_from_url(url: str) -> str:
"""
从完整 URL 解析出文件 key
Args:
url: 完整的 COS URL
Returns:
文件 key
"""
# 尝试移除 CDN 域名
if settings.COS_CDN_DOMAIN:
cdn = settings.COS_CDN_DOMAIN.rstrip("/")
if not cdn.startswith("http"):
cdn = f"https://{cdn}"
if url.startswith(cdn):
return url[len(cdn):].lstrip("/")
# 尝试移除 COS 源站域名
cos_host = f"https://{settings.COS_BUCKET_NAME}.cos.{settings.COS_REGION}.myqcloud.com"
if url.startswith(cos_host):
return url[len(cos_host):].lstrip("/")
return url