| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335 |
- """文件存储工具模块
- 支持多种存储方式:
- 1. 本地存储 (local) - 默认
- 2. 七牛云存储 (qiniu) - 推荐生产环境使用
- 3. 阿里云OSS (oss) - 可选
- 使用方式:
- from app.core.storage import storage
- # 上传文件
- url = storage.upload_file(file_content, "test.jpg", "images")
- # 上传 Base64 图片
- url = storage.upload_base64(base64_data, "avatar.png", "avatar")
- # 删除文件
- storage.delete_file("images/test.jpg")
- """
- import os
- import uuid
- import base64
- from io import BytesIO
- from datetime import datetime
- from typing import Optional, BinaryIO
- from pathlib import Path
- from app.core.config import settings
- # HEIC 格式的 MIME 类型
- HEIC_TYPES = {"image/heic", "image/heif"}
- def convert_heic_to_jpeg(content: bytes) -> bytes:
- """将 HEIC/HEIF 格式转换为 JPEG 格式"""
- try:
- import pillow_heif
- from PIL import Image
- heif_file = pillow_heif.open(BytesIO(content))
- image = heif_file.to_pillow()
- output = BytesIO()
- image.save(output, format="JPEG", quality=85)
- return output.getvalue()
- except ImportError:
- raise Exception("请安装 pillow-heif: pip install pillow-heif")
- def process_image_content(content: bytes, filename: str) -> tuple[bytes, str]:
- """处理图片内容,如果是 HEIC 格式则转换为 JPEG"""
- import imghdr
- img_type = imghdr.what(None, h=content[:32])
- if img_type in ("heic", "heif"):
- return convert_heic_to_jpeg(content), filename.rsplit(".", 1)[0] + ".jpg"
- return content, filename
- class BaseStorage:
- """存储基类"""
- def upload_file(self, file_content: bytes, filename: str, folder: str = "") -> str:
- """上传文件"""
- raise NotImplementedError
- def upload_base64(self, base64_data: str, filename: str, folder: str = "") -> str:
- """上传 Base64 编码的图片"""
- raise NotImplementedError
- def delete_file(self, file_path: str) -> bool:
- """删除文件"""
- raise NotImplementedError
- def get_url(self, file_path: str) -> str:
- """获取文件访问URL"""
- raise NotImplementedError
- class LocalStorage(BaseStorage):
- """本地存储"""
- def __init__(self):
- self.base_url = settings.LOCAL_STORAGE_URL.rstrip("/")
- self.upload_dir = settings.LOCAL_UPLOAD_DIR
- def _ensure_dir(self, folder: str) -> Path:
- """确保目录存在"""
- path = Path(self.upload_dir) / folder / datetime.now().strftime("%Y%m")
- path.mkdir(parents=True, exist_ok=True)
- return path
- def _generate_filename(self, filename: str) -> str:
- """生成唯一文件名"""
- ext = os.path.splitext(filename)[1] or ""
- unique_name = f"{uuid.uuid4().hex}{ext}"
- return unique_name
- def upload_file(self, file_content: bytes, filename: str, folder: str = "") -> str:
- """上传文件到本地"""
- # 处理 HEIC 格式图片
- file_content, filename = process_image_content(file_content, filename)
- folder_path = self._ensure_dir(folder)
- unique_filename = self._generate_filename(filename)
- file_path = folder_path / unique_filename
- with open(file_path, "wb") as f:
- f.write(file_content)
- relative_path = f"{folder}/{datetime.now().strftime('%Y%m')}/{unique_filename}" if folder else f"{datetime.now().strftime('%Y%m')}/{unique_filename}"
- return self.get_url(relative_path)
- def upload_base64(self, base64_data: str, filename: str, folder: str = "") -> str:
- """上传 Base64 图片"""
- # 去除 data:image/png;base64, 等前缀
- if "," in base64_data:
- base64_data = base64_data.split(",")[1]
- file_content = base64.b64decode(base64_data)
- return self.upload_file(file_content, filename, folder)
- def delete_file(self, file_path: str) -> bool:
- """删除本地文件"""
- try:
- # 移除 URL 前缀获取相对路径
- if file_path.startswith(self.base_url):
- relative_path = file_path.replace(self.base_url, "").lstrip("/")
- else:
- relative_path = file_path.lstrip("/")
- file_full_path = Path(self.upload_dir) / relative_path
- if file_full_path.exists():
- file_full_path.unlink()
- return True
- return False
- except Exception:
- return False
- def get_url(self, file_path: str) -> str:
- """获取文件访问URL"""
- return f"{self.base_url}/{file_path}"
- class QiniuStorage(BaseStorage):
- """七牛云存储"""
- def __init__(self):
- try:
- from qiniu import Auth, put_data, delete
- self.qiniu = Auth(settings.QINIU_ACCESS_KEY, settings.QINIU_SECRET_KEY)
- self.bucket = settings.QINIU_BUCKET
- self.base_url = settings.QINIU_DOMAIN.rstrip("/")
- except ImportError:
- raise ImportError("请安装七牛云 SDK: pip install qiniu")
- def _generate_filename(self, filename: str) -> str:
- """生成唯一文件名"""
- ext = os.path.splitext(filename)[1] or ""
- unique_name = f"{datetime.now().strftime('%Y%m/%d')}/{uuid.uuid4().hex}{ext}"
- return unique_name
- def upload_file(self, file_content: bytes, filename: str, folder: str = "") -> str:
- """上传文件到七牛云"""
- from qiniu import put_data
- # 处理 HEIC 格式图片
- file_content, filename = process_image_content(file_content, filename)
- key = self._generate_filename(filename)
- token = self.qiniu.upload_token(self.bucket, key, 3600)
- ret, info = put_data(token, key, file_content)
- if ret is not None:
- return self.get_url(ret["key"])
- raise Exception(f"七牛云上传失败: {info}")
- def upload_base64(self, base64_data: str, filename: str, folder: str = "") -> str:
- """上传 Base64 图片到七牛云"""
- if "," in base64_data:
- base64_data = base64_data.split(",")[1]
- file_content = base64.b64decode(base64_data)
- return self.upload_file(file_content, filename, folder)
- def delete_file(self, file_path: str) -> bool:
- """删除七牛云文件"""
- from qiniu import delete
- try:
- key = file_path.replace(f"{self.base_url}/", "")
- ret, info = delete(self.bucket, key)
- return ret is not None
- except Exception:
- return False
- def get_url(self, file_path: str) -> str:
- """获取文件访问URL"""
- return f"{self.base_url}/{file_path}"
- class OSSStorage(BaseStorage):
- """阿里云OSS存储"""
- def __init__(self):
- try:
- import oss2
- self.auth = oss2.Auth(settings.OSS_ACCESS_KEY_ID, settings.OSS_ACCESS_KEY_SECRET)
- self.bucket_name = settings.OSS_BUCKET
- self.bucket = oss2.Bucket(self.auth, settings.OSS_ENDPOINT, self.bucket_name)
- self.base_url = settings.OSS_DOMAIN.rstrip("/")
- except ImportError:
- raise ImportError("请安装阿里云 OSS SDK: pip install oss2")
- def _generate_filename(self, filename: str) -> str:
- """生成唯一文件名"""
- ext = os.path.splitext(filename)[1] or ""
- unique_name = f"{datetime.now().strftime('%Y%m/%d')}/{uuid.uuid4().hex}{ext}"
- return unique_name
- def upload_file(self, file_content: bytes, filename: str, folder: str = "") -> str:
- """上传文件到OSS"""
- # 处理 HEIC 格式图片
- file_content, filename = process_image_content(file_content, filename)
- key = self._generate_filename(filename)
- result = self.bucket.put_object(key, file_content)
- if result.status == 200:
- return self.get_url(key)
- raise Exception(f"OSS上传失败: {result.status}")
- def upload_base64(self, base64_data: str, filename: str, folder: str = "") -> str:
- """上传 Base64 图片到OSS"""
- if "," in base64_data:
- base64_data = base64_data.split(",")[1]
- file_content = base64.b64decode(base64_data)
- return self.upload_file(file_content, filename, folder)
- def delete_file(self, file_path: str) -> bool:
- """删除OSS文件"""
- try:
- key = file_path.replace(f"{self.base_url}/", "")
- result = self.bucket.delete_object(key)
- return result.status == 204
- except Exception:
- return False
- def get_url(self, file_path: str) -> str:
- """获取文件访问URL"""
- return f"{self.base_url}/{file_path}"
- class MinIOStorage(BaseStorage):
- """MinIO存储"""
- def __init__(self):
- try:
- from minio import Minio
- self.client = Minio(
- settings.MINIO_ENDPOINT,
- access_key=settings.MINIO_ACCESS_KEY,
- secret_key=settings.MINIO_SECRET_KEY,
- secure=settings.MINIO_SECURE,
- )
- self.bucket_name = settings.MINIO_BUCKET
- if settings.MINIO_BASE_URL:
- self.base_url = settings.MINIO_BASE_URL.rstrip("/")
- else:
- scheme = "https" if settings.MINIO_SECURE else "http"
- self.base_url = f"{scheme}://{settings.MINIO_ENDPOINT}/{self.bucket_name}"
- except ImportError:
- raise ImportError("请安装 MinIO SDK: pip install minio")
- def _generate_filename(self, filename: str) -> str:
- """生成唯一文件名"""
- ext = os.path.splitext(filename)[1] or ""
- unique_name = f"{datetime.now().strftime('%Y%m/%d')}/{uuid.uuid4().hex}{ext}"
- return unique_name
- def upload_file(self, file_content: bytes, filename: str, folder: str = "") -> str:
- """上传文件到MinIO"""
- # 处理 HEIC 格式图片
- file_content, filename = process_image_content(file_content, filename)
- key = self._generate_filename(filename)
- if folder:
- key = f"{folder.strip('/')}/{key}"
- data = BytesIO(file_content)
- self.client.put_object(
- bucket_name=self.bucket_name,
- object_name=key,
- data=data,
- length=len(file_content),
- )
- return self.get_url(key)
- def upload_base64(self, base64_data: str, filename: str, folder: str = "") -> str:
- """上传 Base64 图片到MinIO"""
- if "," in base64_data:
- base64_data = base64_data.split(",")[1]
- file_content = base64.b64decode(base64_data)
- return self.upload_file(file_content, filename, folder)
- def delete_file(self, file_path: str) -> bool:
- """删除MinIO文件"""
- try:
- key = file_path.replace(f"{self.base_url}/", "")
- self.client.remove_object(self.bucket_name, key)
- return True
- except Exception:
- return False
- def get_url(self, file_path: str) -> str:
- """获取文件访问URL"""
- return f"{self.base_url}/{file_path}"
- def get_storage() -> BaseStorage:
- """获取存储实例"""
- storage_type = settings.STORAGE_TYPE.lower()
- if storage_type == "qiniu":
- return QiniuStorage()
- elif storage_type == "oss":
- return OSSStorage()
- elif storage_type == "minio":
- return MinIOStorage()
- else:
- return LocalStorage()
- # 全局存储实例
- storage = get_storage()
|