Оказывается, это не так сложно. Но приведенный ниже код не очень проверен , и я должен предупредить вас, чтобы не копировать-вставлять без проверки!
Я создал пользовательский 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
(иногда этофайл, иногда это простая строка, это часть бизнеса 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()
Et voilà!