Skip to content

Django 基于 S3 兼容性接入 Minio 的坑点

发布于  at 09:15 PM更新于  at 12:54 AM

之前我在 Django 使用 Cloudflare R2 作为存储服务 中使用 boto3 接入 Cloudflare R2。如果是使用 Minio,本身上是兼容 S3,这使得我们可以使用现有的 S3 客户端库来与 MinIO 进行交互,实际使用上会遇到一些兼容性问题需要特殊处理。

使用 boto3 接入 Minio

基础配置

Django 使用 boto3 接入 Minio,需要在 Access Keys 中创建 Access Key 和 Secret Key。配置参数与 S3 基本相同,但有一些细微差异:

# settings.py
AWS_ACCESS_KEY_ID = 'pfIxzxxxxxxxxxx'
AWS_SECRET_ACCESS_KEY = 'xxxxxxxxxxxxxxxxxxxxxxxxxxxx'
AWS_S3_ENDPOINT_URL = 'https://your-minio-server.com'  # MinIO 服务器地址
AWS_STORAGE_BUCKET_NAME = 'your-bucket-name'
AWS_S3_REGION_NAME = 'us-east-1'  # MinIO 一般使用 us-east-1 或留空

# MinIO 特定配置
AWS_S3_USE_SSL = True  # 是否使用 HTTPS
AWS_S3_VERIFY_SSL = True  # 是否验证 SSL 证书
AWS_S3_ADDRESSING_STYLE = 'path'  # 推荐使用 path 样式
AWS_S3_SIGNATURE_VERSION = 's3v4'  # 使用 v4 签名

核心问题:403 兼容性错误

问题描述

通常而言,我们自然是不希望用户上传同名文件时把原有文件替换掉。因此一般来说,AWS_S3_FILE_OVERWRITE 都是设置为 False。这本身没什么问题,然而 MinIO 会返回 403 导致上传失败。

问题根因分析

问题的根本原因在于 MinIO 与标准 S3 API 在错误处理上的差异:

  1. 标准 S3 行为: 当检查文件是否存在时,如果文件不存在,返回 404 Not Found
  2. MinIO 行为: 当检查文件是否存在时,如果文件不存在,返回 403 Forbidden

这种非标准的行为导致 Django 的 django-storages 在处理文件覆盖检查时出现异常,进而阻止了文件的正常上传。

常见错误信息

当遇到这个问题时,你可能会看到类似的错误:

botocore.exceptions.ClientError: An error occurred (AccessDenied) when calling the HeadObject operation: Access Denied

或者:

ClientError: An error occurred (403) when calling the HeadObject operation: Forbidden

可能的解决方案

方案一:允许文件覆盖(不推荐)

第一种解决方法当然是允许覆盖,把 AWS_S3_FILE_OVERWRITE 设置为 True。只不过我们不能接受这个方案,如果用户上传同名文件直接覆盖原有文件,比如头像被替换掉,用户体验会很糟糕。

方案二:自定义存储类(推荐)

我们可以通过继承 S3Boto3Storage 类来修复这个兼容性问题,在捕获到 403 错误时将其视为文件不存在:

"""
自定义存储类,解决 MinIO 兼容性问题
"""

from storages.backends.s3boto3 import S3Boto3Storage
from botocore.exceptions import ClientError


class MinIOStorage(S3Boto3Storage):
    """
    MinIO 兼容的 S3 存储类
    解决 MinIO 对不存在文件返回 403 而不是 404 的问题
    """

    def exists(self, name):
        """
        检查文件是否存在,处理 MinIO 的 403 错误
        """
        try:
            return super().exists(name)
        except ClientError as e:
            # MinIO 对不存在的文件返回 403 而不是 404
            if e.response["Error"]["Code"] in ["403", "Forbidden"]:
                return False
            raise

    def get_available_name(self, name, max_length=None):
        """
        获取可用的文件名,在检查文件存在性时处理 403 错误
        """
        try:
            return super().get_available_name(name, max_length)
        except ClientError as e:
            # 如果遇到 403 错误,假设文件不存在,直接返回原名称
            if e.response["Error"]["Code"] in ["403", "Forbidden"]:
                return name
            raise

在设置中将 BACKEND 换成对应的类 config.storage.MinIOStorage 就可以了:

STORAGES = {
    "default": {
        "BACKEND": "config.storage.MinIOStorage", # 这一行!
        "OPTIONS": {
            "access_key": AWS_ACCESS_KEY_ID,
            "secret_key": AWS_SECRET_ACCESS_KEY,
            "bucket_name": AWS_STORAGE_BUCKET_NAME,
            "endpoint_url": AWS_S3_ENDPOINT_URL,
            "region_name": AWS_S3_REGION_NAME,
            "file_overwrite": AWS_S3_FILE_OVERWRITE,
            "default_acl": AWS_DEFAULT_ACL,
            "location": "media",
            "use_ssl": AWS_S3_USE_SSL,
            "verify": AWS_S3_VERIFY,
            "addressing_style": AWS_S3_ADDRESSING_STYLE,
            "signature_version": AWS_S3_SIGNATURE_VERSION,
        },
    },
    "staticfiles": {
        "BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage",
    },
}

结语

顺便吐槽一下,新版本的开源 Minio 已经砍掉 Web Admin UI 中的大部分功能,可能 Minio 之后也不是一个很理想的开源选择了(当然,如果是 Enterprise License 就没有这个问题)。不过,还是可以使用命令行(MinIO Client, mc)进行管理的。

所以 Docker 的 image 建议使用的是:image: minio/minio:RELEASE.2025-04-22T22-12-26Z,如果使用 image: minio/minio,会发现整个管理界面功能少了很多。如果之后有非常值得使用的更新,可能最终还是不得不接受更新的。

本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自小谷的随笔

下一篇
64 位汇编语言环境设置