50

I'm trying to generate dynamic file paths in django. I want to make a file system like this:

 -- user_12
     --- photo_1
     --- photo_2
 --- user_ 13
     ---- photo_1

I found a related question : Django Custom image upload field with dynamic path

Here, they say we can change the upload_to path and leads to https://docs.djangoproject.com/en/stable/topics/files/ doc. In the documentation, there is an example :

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)

But, still this is not dynamic, I want to give Car id to the image name, and I cant assign the id before Car definition completed. So how can I create a path with car ID ??

MichielB
  • 4,181
  • 1
  • 30
  • 39
iva123
  • 3,395
  • 10
  • 47
  • 68
  • I thought I can't get `instance`'s attribute before it is saved. I was wrong, those Car attributes have been bounded to `instance`, yet haven't been saved in database – laike9m Jan 09 '14 at 08:38

12 Answers12

83

You can use a callable in the upload_to argument rather than using custom storage. See the docs, and note the warning there that the primary key may not yet be set when the function is called. This can happen because the upload may be handled before the object is saved to the database, so using ID might not be possible. You might want to consider using another field on the model such as slug. E.g:

import os
def get_upload_path(instance, filename):
    return os.path.join(
      "user_%d" % instance.owner.id, "car_%s" % instance.slug, filename)

then:

photo = models.ImageField(upload_to=get_upload_path)
martin-martin
  • 3,274
  • 1
  • 33
  • 60
DrMeers
  • 4,117
  • 2
  • 36
  • 38
  • I'm new to python and I might be missing something. But, I did not see you passing instance, filename in the following statement - photo = models.ImageField(upload_to=get_upload_path). So, my question is how does the method know what are the values? – Naz Mir May 23 '13 at 15:27
  • 2
    See the "docs" link in the answer. It sells you -- "The two arguments that will be passed are..." – DrMeers May 23 '13 at 21:16
  • @MatthieuRiegler Really? I'm not aware of any changes of that nature, it should still work. Can you provide details of the change to which you are referring? – DrMeers Jul 09 '13 at 01:08
  • 3
    @MatthieuRiegler You're wrong. It works in Django 1.5.5 which I've just tested. – laike9m Jan 09 '14 at 08:41
6

You can use lambda function as below, take note that if instance is new then it won't have the instance id, so use something else:

logo = models.ImageField(upload_to=lambda instance, filename: 'directory/images/{0}/{1}'.format(instance.owner.id, filename))
James Lin
  • 25,028
  • 36
  • 133
  • 233
6

https://docs.djangoproject.com/en/stable/ref/models/fields/#django.db.models.FileField.upload_to

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)

There is a warning in the docs, but it shouldn't affect you since we're after the User ID and not the Car ID.

In most cases, this object will not have been saved to the database yet, so if it uses the default AutoField, it might not yet have a value for its primary key field.

MichielB
  • 4,181
  • 1
  • 30
  • 39
Yuji 'Tomita' Tomita
  • 115,817
  • 29
  • 282
  • 245
  • 2
    One thing that may not be entirely obvious unless you read all of the docs on `FileField` is that the `upload_to` callable actually returns a filename `/path/to/base.ext` that gets appended to your storage's location. Because the storage abstracts the absolute path and file operations, the less obvious benefit is that you can move your files to a completely different storage/location and your models will continue to work as expected. – Filip Dupanović Feb 27 '11 at 20:38
  • Hi, I tried but got error: AttributeError: 'Car' object has no attribute 'user'. How can I pass request.user.id (id of logged in user) to instance.user.id? Thanks. – Harry Feb 07 '15 at 12:17
3

My solution is not elegant, but it works:

In the model, use a the standard function that will need the id/pk

def directory_path(instance, filename):
    return 'files/instance_id_{0}/{1}'.format(instance.pk, filename)

in views.py save the form like this:

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()

It worked for me. Note: My filefield in models and allowed for Null values. Null=True

2

Well very late to the party but this one works for me.

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)

Model like this only

class Album(models.Model):
    albumname = models.CharField(max_length=100)
    audiofile = models.FileField(upload_to=content_file_name)
arogachev
  • 33,150
  • 7
  • 114
  • 117
Raja Simon
  • 10,126
  • 5
  • 43
  • 74
  • This worked for me, but I have an issue, I need to generate two subtrees for one model id, I do it this way: upload_dir = os.path.join('equipos', instance.equipo_id, 'anexos', 'img') & upload_dir = os.path.join('equipos', instance.equipo_id, 'anexos', 'files') in different functions which I call in different models, is there any way to do this but with no breaking DRY paradigm? – Angie Alejo Dec 22 '16 at 17:30
1

There are two solutions on DjangoSnippets

  1. Two-stage save: https://djangosnippets.org/snippets/1129/
  2. Prefetch the ID (PostgreSQL only): https://djangosnippets.org/snippets/2731/
blueyed
  • 27,102
  • 4
  • 75
  • 71
0

As the primary key (id) may not be available if the model instance was not saved to the database yet, I wrote my FileField subclasses which move the file on model save, and a storage subclass which removes the old files.

Storage:

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

and my upload_to callables look like this:

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))
Kukosk
  • 2,892
  • 1
  • 27
  • 30
0
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)
Evgeni Shudzel
  • 231
  • 2
  • 5
0

This guy has a way to do dynamic path. The idea is to set your favourite storage and customise "upload_to()" parameter with a function.

Hope this helps.

Laur Ivan
  • 4,117
  • 3
  • 38
  • 62
0

I found out a different solution, which is dirty, but it works. You should create a new dummy model, which is self synchronized with the original one. I'm not proud of this, but didn't find another solution. In my case I want to upload files, and store each in a directory named after the model id (because I'll generate there more files).

the 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)

I'm very new in python and in django, but it seems like ok for me.

another solution:

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)
balazs
  • 5,698
  • 7
  • 37
  • 45
  • ok, so you get a random id, how do you then link the file to the saved object? – Mikhail Oct 14 '13 at 04:44
  • 1
    huh, it was long time ago, and I stoped working with django, so I don't remember. I looked up the codes, and supprisingly I'm using something else, here it is (edited my answer): – balazs Oct 14 '13 at 11:04
  • That's an interesting approach, hoping that primary key is in sync with total item count. I guess that's true as long as you don't delete anything. – Mikhail Oct 15 '13 at 18:01
  • A uuid field may help in this case :) – GrvTyagi Aug 26 '16 at 13:06
0

You can override model's save method:

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)
vigo
  • 298
  • 3
  • 8
0

The method will be

def user_directory_path(field_name):

    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

And in your models you can have as many ImageField as you like. example

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,)
rzlvmp
  • 7,512
  • 5
  • 16
  • 45
  • Is it desirable to have a nested function, where you have `def upload_path()` inside of `def user_directory_path()`? I'm totally new to this so I have no idea...this could be a mistake or completely necessary. – Martin Burch Jul 03 '20 at 13:36
  • I came from c, java and php and you are right this is new concept for me too. It completely worked for me for multiple pictures. I am able to makemigrations etc. Being short and concise I liked it too – Ftwi Grmay Jul 07 '20 at 08:43