dingdanquanliucheng/backend/app/services/storage_service.py

119 lines
5.0 KiB
Python

from __future__ import annotations
import base64
import hashlib
import hmac
from datetime import datetime, timedelta
from pathlib import Path
from urllib.parse import quote
from backend.app.core.config import get_settings
from backend.app.core.error_codes import ErrorCode
from backend.app.core.exceptions import AppException
class StorageService:
def __init__(self) -> None:
self.settings = get_settings()
self.export_publish_root = Path("D:/tmp/order-flow-oss")
self.export_publish_root.mkdir(parents=True, exist_ok=True)
def create_upload_token(self, biz_type: str, biz_id: int, file_name: str, file_size: int) -> dict:
object_key = self.build_object_key(biz_type, biz_id, file_name)
expire_at = datetime.now() + timedelta(seconds=self.settings.oss_signed_url_expire_seconds)
content_type = self._guess_content_type(file_name)
signed_meta = self._build_signed_upload_url(object_key, expire_at, content_type)
return {
"upload_url": signed_meta["upload_url"],
"file_url": self.build_public_file_url(object_key),
"headers": signed_meta["headers"],
"method": "PUT",
"object_key": object_key,
"bucket_name": self.settings.aliyun_oss_bucket,
"expire_at": expire_at.strftime("%Y-%m-%d %H:%M:%S"),
"max_file_size": file_size,
"content_type": content_type,
}
def publish_local_file(self, local_file_path: str, object_key: str) -> dict:
source = Path(local_file_path)
if not source.exists() or not source.is_file():
raise AppException(code=ErrorCode.FILE_UPLOAD_FAILED, message="待发布文件不存在", status_code=400)
target = self.export_publish_root / Path(object_key)
target.parent.mkdir(parents=True, exist_ok=True)
target.write_bytes(source.read_bytes())
return {
"file_path": str(target),
"file_url": self.build_public_file_url(object_key),
"object_key": object_key,
"bucket_name": self.settings.aliyun_oss_bucket,
"storage_provider": "aliyun_oss",
"published": True,
}
def build_object_key(self, biz_type: str, biz_id: int, file_name: str) -> str:
safe_name = Path(file_name).name
return f"{biz_type}/{biz_id}/{datetime.now().strftime('%Y%m%d%H%M%S')}_{safe_name}"
def build_public_file_url(self, object_key: str) -> str:
encoded_key = quote(object_key.replace("\\", "/"), safe="/-_.*()")
return f"{self.settings.aliyun_oss_public_base_url.rstrip('/')}/{encoded_key}"
def extract_object_key(self, file_url: str) -> str:
prefix = f"{self.settings.aliyun_oss_public_base_url.rstrip('/')}/"
if file_url.startswith(prefix):
return file_url[len(prefix) :]
return file_url.split("://", 1)[-1].split("/", 1)[-1] if "/" in file_url.split("://", 1)[-1] else file_url
def _build_signed_upload_url(self, object_key: str, expire_at: datetime, content_type: str) -> dict:
# 当前按 OSS V1 预签名 URL 规则生成上传地址,便于前端直接联调真实 PUT 上传口径。
expires = str(int(expire_at.timestamp()))
encoded_key = quote(object_key.replace("\\", "/"), safe="/-_.*()")
canonical_headers = "x-oss-object-acl:private\n"
canonical_resource = f"/{self.settings.aliyun_oss_bucket}/{object_key}"
string_to_sign = f"PUT\n\n{content_type}\n{expires}\n{canonical_headers}{canonical_resource}"
signature = base64.b64encode(
hmac.new(
self.settings.aliyun_oss_access_key_secret.encode("utf-8"),
string_to_sign.encode("utf-8"),
hashlib.sha1,
).digest()
).decode("utf-8")
query_signature = quote(signature, safe="")
upload_url = (
f"https://{self.settings.aliyun_oss_bucket}.{self.settings.aliyun_oss_endpoint}/{encoded_key}"
f"?OSSAccessKeyId={quote(self.settings.aliyun_oss_access_key_id, safe='')}"
f"&Expires={expires}"
f"&Signature={query_signature}"
)
return {
"upload_url": upload_url,
"headers": {
"Content-Type": content_type,
"x-oss-object-acl": "private",
},
}
def _guess_content_type(self, file_name: str) -> str:
suffix = Path(file_name).suffix.lower()
mapping = {
".jpg": "image/jpeg",
".jpeg": "image/jpeg",
".png": "image/png",
".gif": "image/gif",
".webp": "image/webp",
".mp4": "video/mp4",
".mov": "video/quicktime",
".avi": "video/x-msvideo",
".csv": "text/csv",
".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
".xls": "application/vnd.ms-excel",
".pdf": "application/pdf",
}
return mapping.get(suffix, "application/octet-stream")
storage_service = StorageService()