如何将 Django FileField 与动态 Amazon S3 存储桶一起使用?

How to use Django FileField with dynamic Amazon S3 bucket?

我有一个带有 Filefield 的 Django 模型,以及一个使用 Amazon S3 存储桶的默认存储(通过出色的 django-storage)。

我的问题是无法将文件上传到动态文件夹路径(正如我们在许多其他答案中看到的那样)。我的问题更深层次和双重:

有什么想法吗?

(Djabgo 1.11, Python 3).

原来并没有那么难。 但是下面的代码没有经过太多测试,我必须警告你不要在没有检查的情况下复制粘贴!

我创建了一个自定义 FileField subclass:

class DynamicS3BucketFileField(models.FileField):
    attr_class = S3Boto3StorageFile
    descriptor_class = DynamicS3BucketFileDescriptor

    def pre_save(self, model_instance, add):
        return getattr(model_instance, self.attname)

请注意,attr_class 专门使用 S3Boto3StorageFile class(django-storages 提供的 File subclass)。

pre_save 重载只有一个目标:避免尝试重新上传文件的内部 file.save 调用。

魔法发生在 FileDescriptor subclass:

class DynamicS3BucketFileDescriptor(FileDescriptor):
    def __get__(self, instance, cls=None):
        if instance is None:
            return self

        # Copied from FileDescriptor
        if self.field.name in instance.__dict__:
            file = instance.__dict__[self.field.name]
        else:
            instance.refresh_from_db(fields=[self.field.name])
            file = getattr(instance, self.field.name)

        # Make sure to transform storage to a Storage instance.
        if callable(self.field.storage):
            self.field.storage = self.field.storage(instance)

        # The file can be a string here (depending on when/how we access the field).
        if isinstance(file, six.string_types):
            # We instance file following S3Boto3StorageFile constructor.
            file = self.field.attr_class(file, 'rb', self.field.storage)
            # We follow here the way FileDescriptor work (see 'return' finish line).
            instance.__dict__[self.field.name] = file

        # Copied from FileDescriptor. The difference here is that these 3
        # properties are set systematically without conditions.
        file.instance = instance
        file.field = self.field
        file.storage = self.field.storage
        # Added a very handy property to file.
        file.url = self.field.storage.url(file.name)

        return instance.__dict__[self.field.name]

上面的代码采用了适合我的情况的 FileDescriptor 的一些内部代码。请注意下面解释的 if callable(self.field.storage):

关键行是:file = self.field.attr_class(file, 'rb', self.field.storage),根据当前file实例的内容自动创建S3Boto3StorageFile的有效实例(有时是文件,有时是一个简单的字符串,这是 FileDescriptor 业务的一部分)。

现在,动态部分非常简单。事实上,当声明一个FileField时,你可以提供给storage选项,一个函数。像这样:

class MyMedia(models.Model):
    class Meta:
        app_label = 'appname'

    mediaset = models.ForeignKey(Mediaset, on_delete=models.CASCADE, related_name='media_files')
    file = DynamicS3BucketFileField(null=True, blank=True, storage=get_fits_file_storage)

并且函数 get_fits_file_storage 将使用一个参数调用:MyMedia 的实例。因此,我可以使用该对象的任何 属性 到 return 有效存储。在我的例子中 mediaset,它包含一个密钥,允许我检索包含 S3 凭据的对象,我可以用它构建一个 S3Boto3Storage 实例(另一个 class 由 django-storages 提供) .

具体来说:

def get_fits_file_storage(instance):
    name = instance.mediaset.archive_storage_name
    return instance.mediaset.archive.bucket_keys.get(name= name).get_storage()

瞧瞧!