Динамический путь к файлу в Django
Я пытаюсь генерировать динамические пути к файлам в Django. Я хочу сделать файловую систему такой:
-- user_12
--- photo_1
--- photo_2
--- user_ 13
---- photo_1
Я нашел связанный вопрос: Django Пользовательское поле загрузки изображения с динамическим путем
Здесь говорят, что мы можем изменить путь upload_to и приводит к https://docs.djangoproject.com/en/stable/topics/files/ doc. В документации есть пример:
from django.db import models
from django.core.files.storage import FileSystemStorage
fs = FileSystemStorage(location='/media/photos')
class Car(models.Model):
...
photo = models.ImageField(storage=fs)
Но, тем не менее, это не динамично, я хочу присвоить идентификатору автомобиля имя изображения, и я не могу присвоить идентификатор до завершения определения автомобиля. Так как я могу создать путь с идентификатором автомобиля?
10 ответов
Вы можете использовать вызываемый в upload_to
аргумент, а не использование пользовательского хранилища. См. Документы и обратите внимание на предупреждение, что первичный ключ еще не может быть установлен при вызове функции (потому что выгрузка может быть обработана до сохранения объекта в базе данных), поэтому используйте ID
может быть не возможно Возможно, вы захотите рассмотреть использование другого поля в модели, например, слаг. Например:
import os
def get_upload_path(instance, filename):
return os.path.join(
"user_%d" % instance.owner.id, "car_%s" % instance.slug, filename)
затем:
photo = models.ImageField(upload_to=get_upload_path)
https://docs.djangoproject.com/en/stable/ref/models/fields/
def upload_path_handler(instance, filename):
return "user_{id}/{file}".format(id=instance.user.id, file=filename)
class Car(models.Model):
...
photo = models.ImageField(upload_to=upload_path_handler, storage=fs)
В документах есть предупреждение, но оно не должно повлиять на вас, так как мы User
ID а не Car
Я БЫ.
В большинстве случаев этот объект еще не был сохранен в базе данных, поэтому, если он использует AutoField по умолчанию, он может еще не иметь значения для своего поля первичного ключа.
Вы можете использовать лямбда-функцию, как показано ниже, обратите внимание, что если экземпляр новый, то у него не будет идентификатора экземпляра, поэтому используйте что-то другое:
logo = models.ImageField(upload_to=lambda instance, filename: 'directory/images/{0}/{1}'.format(instance.owner.id, filename))
Мое решение не элегантно, но оно работает:
В модели используйте стандартную функцию, для которой потребуется id/pk
def directory_path(instance, filename):
return 'files/instance_id_{0}/{1}'.format(instance.pk, filename)
в views.py сохраните форму следующим образом:
f=form.save(commit=False)
ftemp1=f.filefield
f.filefield=None
f.save()
#And now that we have crated the record we can add it
f.filefield=ftemp1
f.save()
Это сработало для меня. Примечание: мое файловое поле в моделях и допускается для значений Null. Null=True
Ну, очень поздно на вечеринку, но этот работает для меня.
def content_file_name(instance, filename):
upload_dir = os.path.join('uploads',instance.albumname)
if not os.path.exists(upload_dir):
os.makedirs(upload_dir)
return os.path.join(upload_dir, filename)
Модель как эта только
class Album(models.Model):
albumname = models.CharField(max_length=100)
audiofile = models.FileField(upload_to=content_file_name)
На DjangoSnippets есть два решения
- Двухэтапное сохранение: https://djangosnippets.org/snippets/1129/
- Предварительная загрузка идентификатора (только для PostgreSQL): https://djangosnippets.org/snippets/2731/
Вы можете переопределить модель save
метод:
def save_image(instance, filename):
instance_id = f'{instance.id:03d}' # 001
return f'{instance_id}-{filename.lower()}' # 001-foo.jpg
class Resource(models.Model):
photo = models.ImageField(upload_to=save_image)
def save(self, *args, **kwargs):
if self.id is None:
photo = self.photo
self.photo = None
super().save(*args, **kwargs)
self.photo = photo
if 'force_insert' in kwargs:
kwargs.pop('force_insert')
super().save(*args, **kwargs)
Поскольку первичный ключ (id) может быть недоступен, если экземпляр модели еще не был сохранен в базе данных, я написал свои подклассы FileField, которые перемещают файл при сохранении модели, и подкласс хранения, который удаляет старые файлы.
Место хранения:
class OverwriteFileSystemStorage(FileSystemStorage):
def _save(self, name, content):
self.delete(name)
return super()._save(name, content)
def get_available_name(self, name):
return name
def delete(self, name):
super().delete(name)
last_dir = os.path.dirname(self.path(name))
while True:
try:
os.rmdir(last_dir)
except OSError as e:
if e.errno in {errno.ENOTEMPTY, errno.ENOENT}:
break
raise e
last_dir = os.path.dirname(last_dir)
FileField:
def tweak_field_save(cls, field):
field_defined_in_this_class = field.name in cls.__dict__ and field.name not in cls.__bases__[0].__dict__
if field_defined_in_this_class:
orig_save = cls.save
if orig_save and callable(orig_save):
assert isinstance(field.storage, OverwriteFileSystemStorage), "Using other storage than '{0}' may cause unexpected behavior.".format(OverwriteFileSystemStorage.__name__)
def save(self, *args, **kwargs):
if self.pk is None:
orig_save(self, *args, **kwargs)
field_file = getattr(self, field.name)
if field_file:
old_path = field_file.path
new_filename = field.generate_filename(self, os.path.basename(old_path))
new_path = field.storage.path(new_filename)
os.makedirs(os.path.dirname(new_path), exist_ok=True)
os.rename(old_path, new_path)
setattr(self, field.name, new_filename)
# for next save
if len(args) > 0:
args = tuple(v if k >= 2 else False for k, v in enumerate(args))
kwargs['force_insert'] = False
kwargs['force_update'] = False
orig_save(self, *args, **kwargs)
cls.save = save
def tweak_field_class(orig_cls):
orig_init = orig_cls.__init__
def __init__(self, *args, **kwargs):
if 'storage' not in kwargs:
kwargs['storage'] = OverwriteFileSystemStorage()
if orig_init and callable(orig_init):
orig_init(self, *args, **kwargs)
orig_cls.__init__ = __init__
orig_contribute_to_class = orig_cls.contribute_to_class
def contribute_to_class(self, cls, name):
if orig_contribute_to_class and callable(orig_contribute_to_class):
orig_contribute_to_class(self, cls, name)
tweak_field_save(cls, self)
orig_cls.contribute_to_class = contribute_to_class
return orig_cls
def tweak_file_class(orig_cls):
"""
Overriding FieldFile.save method to remove the old associated file.
I'm doing the same thing in OverwriteFileSystemStorage, but it works just when the names match.
I probably want to preserve both methods if anyone calls Storage.save.
"""
orig_save = orig_cls.save
def new_save(self, name, content, save=True):
self.delete(save=False)
if orig_save and callable(orig_save):
orig_save(self, name, content, save=save)
new_save.__name__ = 'save'
orig_cls.save = new_save
return orig_cls
@tweak_file_class
class OverwriteFieldFile(models.FileField.attr_class):
pass
@tweak_file_class
class OverwriteImageFieldFile(models.ImageField.attr_class):
pass
@tweak_field_class
class RenamedFileField(models.FileField):
attr_class = OverwriteFieldFile
@tweak_field_class
class RenamedImageField(models.ImageField):
attr_class = OverwriteImageFieldFile
и мои загружаемые файлы выглядят так:
def user_image_path(instance, filename):
name, ext = 'image', os.path.splitext(filename)[1]
if instance.pk is not None:
return os.path.join('users', os.path.join(str(instance.pk), name + ext))
return os.path.join('users', '{0}_{1}{2}'.format(uuid1(), name, ext))
MEDIA_ROOT/
/company_Company1/company.png
/shop_Shop1/shop.png
/bikes/bike.png
def photo_path_company(instance, filename):
# file will be uploaded to MEDIA_ROOT/company_<name>/
return 'company_{0}/{1}'.format(instance.name, filename)
class Company(models.Model):
name = models.CharField()
photo = models.ImageField(max_length=255, upload_to=photo_path_company)
def photo_path_shop(instance, filename):
# file will be uploaded to MEDIA_ROOT/company_<name>/shop_<name>/
parent_path = instance.company._meta.get_field('photo').upload_to(instance.company, '')
return parent_path + 'shop_{0}/{1}'.format(instance.name, filename)
class Shop(models.Model):
name = models.CharField()
photo = models.ImageField(max_length=255, upload_to=photo_path_shop)
def photo_path_bike(instance, filename):
# file will be uploaded to MEDIA_ROOT/company_<name>/shop_<name>/bikes/
parent_path = instance.shop._meta.get_field('photo').upload_to(instance.shop, '')
return parent_path + 'bikes/{0}'.format(filename)
class Bike(models.Model):
name = models.CharField()
photo = models.ImageField(max_length=255, upload_to=photo_path_bike)
У этого парня есть способ сделать динамический путь. Идея состоит в том, чтобы установить ваше любимое хранилище и настроить параметр upload_to() с помощью функции.
Надеюсь это поможет.
Я нашел другое решение, которое грязное, но оно работает. Вы должны создать новую фиктивную модель, которая будет синхронизирована с оригинальной. Я не горжусь этим, но не нашел другого решения. В моем случае я хочу загрузить файлы и сохранить каждый в каталоге, названном в честь идентификатора модели (потому что я сгенерирую там больше файлов).
model.py
class dummyexperiment(models.Model):
def __unicode__(self):
return str(self.id)
class experiment(models.Model):
def get_exfile_path(instance, filename):
if instance.id == None:
iid = instance.dummye.id
else:
iid = instance.id
return os.path.join('experiments', str(iid), filename)
exfile = models.FileField(upload_to=get_exfile_path)
def save(self, *args, **kwargs):
if self.id == None:
self.dummye = dummyexperiment()
self.dummye.save()
super(experiment, self).save(*args, **kwargs)
Я очень новичок в Python и в Django, но мне кажется, что все в порядке.
другое решение:
def get_theme_path(instance, filename):
id = instance.id
if id == None:
id = max(map(lambda a:a.id,Theme.objects.all())) + 1
return os.path.join('experiments', str(id), filename)
Метод будет
def user_directory_path(имя_поля):
def upload_path(instance, filename):
year = datetime.now().year
name, ext = instance.user, os.path.splitext(filename)[1]
return f'photos/{year}/{instance._meta.model_name}s/{instance.user}/{field_name}_{name}{ext}'
return upload_path
И в ваших моделях вы можете иметь столько ImageField, сколько захотите. пример
photo = models.ImageField(upload_to=user_directory_path('photo'), null=True, blank=True,)
Passport_photo = models.ImageField(upload_to=user_directory_path('Passport_photo'), null=True, blank=True,)