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.
iTi-Flask/iti/applications/common/storage/qiniu_kodo.py

123 lines
4.3 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.

from __future__ import annotations
import os
from io import BytesIO
from typing import BinaryIO, Dict, Optional
from .interface import StorageInterface
class QiniuKodoStorage(StorageInterface):
"""七牛云Kodo存储适配器"""
storage_type = "qiniu_kodo"
def __init__(self, config: dict):
try:
from qiniu import Auth, BucketManager, put_data
except ImportError:
raise ImportError("请先安装 qiniu: pip install qiniu")
self.access_key = config.get("access_key")
self.secret_key = config.get("secret_key")
self.bucket_name = config.get("bucket")
self.domain = config.get("domain") # CDN域名
if not all([self.access_key, self.secret_key, self.bucket_name]):
raise ValueError("七牛云Kodo配置不完整需要: access_key, secret_key, bucket")
self.auth = Auth(self.access_key, self.secret_key)
self.bucket_manager = BucketManager(self.auth)
self.put_data = put_data
def upload(self, file_stream: BinaryIO, key: str, mime_type: Optional[str] = None) -> Dict:
"""上传文件到七牛云Kodo优化大文件上传"""
token = self.auth.upload_token(self.bucket_name, key)
# 对于大文件,使用分块读取来减少内存压力
# 七牛 SDK 的 put_data 需要完整数据,所以我们分块读取后组合
BUFFER_SIZE = 8 * 1024 * 1024 # 8MB
chunks = []
total_size = 0
while True:
chunk = file_stream.read(BUFFER_SIZE)
if not chunk:
break
chunks.append(chunk)
total_size += len(chunk)
data = b''.join(chunks)
ret, info = self.put_data(token, key, data, mime_type=mime_type)
if info.status_code != 200:
raise Exception(f"七牛云上传失败: {info}")
return {
"hash": ret.get("hash"),
"key": ret.get("key"),
}
def append_chunk(self, key: str, chunk_stream: BinaryIO, offset: int) -> None:
"""
追加写入数据块
注意七牛云不支持原生append采用读-改-写策略
"""
chunk_data = chunk_stream.read()
# 如果文件已存在,先下载
existing_data = b""
if self.exists(key):
try:
file_stream = self.download(key)
existing_data = file_stream.read()
except Exception:
pass
# 合并数据
if offset == 0:
new_data = chunk_data
else:
new_data = existing_data[:offset] + chunk_data
# 重新上传
token = self.auth.upload_token(self.bucket_name, key)
self.put_data(token, key, new_data)
def download(self, key: str) -> BinaryIO:
"""从七牛云Kodo下载文件"""
# 生成私有下载链接
base_url = f"http://{self.domain}/{key}" if self.domain else f"http://{self.bucket_name}.kodo.com/{key}"
private_url = self.auth.private_download_url(base_url, expires=3600)
# 下载文件
import requests
response = requests.get(private_url)
if response.status_code != 200:
raise FileNotFoundError(f"文件不存在: {key}")
return BytesIO(response.content)
def delete(self, key: str) -> None:
"""删除Kodo文件"""
ret, info = self.bucket_manager.delete(self.bucket_name, key)
if info.status_code not in [200, 612]: # 612表示文件不存在
raise Exception(f"删除文件失败: {info}")
def exists(self, key: str) -> bool:
"""检查Kodo文件是否存在"""
ret, info = self.bucket_manager.stat(self.bucket_name, key)
return info.status_code == 200
def get_url(self, key: str, expires: int = 3600) -> str:
"""获取Kodo文件访问URL"""
base_url = f"http://{self.domain}/{key}" if self.domain else f"http://{self.bucket_name}.kodo.com/{key}"
if expires == 0:
# 永久URL适用于公共读bucket
return base_url
# 生成带签名的临时URL
return self.auth.private_download_url(base_url, expires=expires)