From c337707e05906815698aef59603ed3aa3e897cad Mon Sep 17 00:00:00 2001 From: lewis_yan Date: Fri, 3 Apr 2026 16:41:04 +0800 Subject: [PATCH] feat(storage): add MinIO storage utility (boto3-based) --- backend/dvadmin/utils/minio_storage.py | 118 +++++++++++++++++++++++++ 1 file changed, 118 insertions(+) create mode 100644 backend/dvadmin/utils/minio_storage.py diff --git a/backend/dvadmin/utils/minio_storage.py b/backend/dvadmin/utils/minio_storage.py new file mode 100644 index 0000000..67d672a --- /dev/null +++ b/backend/dvadmin/utils/minio_storage.py @@ -0,0 +1,118 @@ +# -*- coding: utf-8 -*- +""" +MinIO (S3-compatible) 文件存储工具 +""" +import uuid +from typing import Optional + +import boto3 +from botocore.config import Config +from botocore.exceptions import ClientError +from django.conf import settings + + +def get_minio_client(): + """ + 创建 MinIO S3 客户端 + """ + client = boto3.client( + 's3', + endpoint_url=f"{'https' if settings.MINIO_SECURE else 'http'}://{settings.MINIO_ENDPOINT}", + aws_access_key_id=settings.MINIO_ACCESS_KEY, + aws_secret_access_key=settings.MINIO_SECRET_KEY, + region_name=settings.MINIO_REGION or 'us-east-1', + config=Config(signature_version='s3v4'), + ) + return client + + +def minio_upload_file(file_obj, file_name: str) -> Optional[str]: + """ + 上传文件到 MinIO + + Args: + file_obj: 文件对象 (InMemoryUploadedFile 或 UploadedFile) + file_name: 原始文件名 + + Returns: + 文件访问URL,失败返回 None + """ + try: + # 生成唯一文件名 (uuid前缀避免冲突) + ext = file_name.split('.')[-1] if '.' in file_name else '' + unique_name = f"{uuid.uuid4().hex}.{ext}" if ext else uuid.uuid4().hex + + # 路径前缀处理 + path_prefix = getattr(settings, 'MINIO_PATH_PREFIX', '') + if path_prefix: + path_prefix = path_prefix.strip('/') + '/' + object_name = f"{path_prefix}{unique_name}" + + # 获取文件内容 + file_obj.seek(0) + file_content = file_obj.read() + + # 上传 + client = get_minio_client() + client.put_object( + Bucket=settings.MINIO_BUCKET, + Key=object_name, + Body=file_content, + ContentType=getattr(file_obj, 'content_type', 'application/octet-stream'), + ) + + # 生成访问URL + file_url = minio_get_file_url(object_name) + return file_url + + except ClientError as e: + print(f"MinIO upload error: {e}") + return None + except Exception as e: + print(f"MinIO upload unexpected error: {e}") + return None + + +def minio_delete_file(file_url: str) -> bool: + """ + 从 MinIO 删除文件 + + Args: + file_url: 文件完整URL或 object_key + + Returns: + 删除成功返回 True,否则 False + """ + try: + # 从URL中提取 object_key + object_key = file_url + if settings.MINIO_ENDPOINT in file_url: + # 提取 /bucket/key 部分 + parts = file_url.split(settings.MINIO_BUCKET + '/') + if len(parts) > 1: + object_key = parts[-1] + else: + return True # URL格式不对,当作已删除 + + client = get_minio_client() + client.delete_object(Bucket=settings.MINIO_BUCKET, Key=object_key) + return True + except ClientError as e: + print(f"MinIO delete error: {e}") + return False + + +def minio_get_file_url(object_key: str) -> str: + """ + 获取文件访问URL + + Args: + object_key: 对象存储的key + + Returns: + 完整的文件访问URL + """ + scheme = 'https' if settings.MINIO_SECURE else 'http' + endpoint = settings.MINIO_ENDPOINT + bucket = settings.MINIO_BUCKET + return f"{scheme}://{endpoint}/{bucket}/{object_key}"