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

时间:2018-05-31 10:18:52

标签: django amazon-web-services amazon-s3 filefield

我有一个带文件字段的Django模型,以及使用Amazon S3存储桶的默认存储(通过优秀的django-storage)。

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

  • 文件已经在Amazon S3存储桶中,我不想下载 - 重新上传它们(更糟糕的是:我只有读取权限)。
  • 可以通过S3凭据访问文件,这些凭据可能因文件不同而不同(即,文件可以位于不同的存储桶中,并通过不同的凭据进行访问)。因此,我的FileField必须具有动态存储空间

有什么想法吗?

(Djabgo 1.11,Python 3)。

1 个答案:

答案 0 :(得分:0)

事实证明并不是那么困难。 但是下面的代码没有经过太多测试,我必须警告你不要复制粘贴而不检查!

我创建了一个自定义FileField子类:

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类(File提供的django-storages子类)。

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

神奇发生在FileDescriptor子类中:

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),根据当前S3Boto3StorageFile实例的内容自动创建file的有效实例(有时,它是一个文件,有时它很简单string,这是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的实例。因此,我可以使用该对象的任何属性来返回有效存储。在我的案例mediaset中,其中包含一个允许我检索包含S3凭据的对象的密钥,我可以使用该凭据构建S3Boto3Storage实例(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()

Etvoilà!