148

I'm trying to set up my uploads so that if user joe uploads a file it goes to MEDIA_ROOT/joe as opposed to having everyone's files go to MEDIA_ROOT. The problem is I don't know how to define this in the model. Here is how it currently looks:

class Content(models.Model):
    name = models.CharField(max_length=200)
    user = models.ForeignKey(User)
    file = models.FileField(upload_to='.')

So what I want is instead of '.' as the upload_to, have it be the user's name.

I understand that as of Django 1.0 you can define your own function to handle the upload_to but that function has no idea of who the user will be either so I'm a bit lost.

Thanks for the help!

Serjik
  • 10,543
  • 8
  • 61
  • 70
Teebes
  • 2,056
  • 2
  • 14
  • 11

6 Answers6

279

You've probably read the documentation, so here's an easy example to make it make sense:

def content_file_name(instance, filename):
    return '/'.join(['content', instance.user.username, filename])

class Content(models.Model):
    name = models.CharField(max_length=200)
    user = models.ForeignKey(User)
    file = models.FileField(upload_to=content_file_name)

As you can see, you don't even need to use the filename given - you could override that in your upload_to callable too if you liked.

SmileyChris
  • 10,578
  • 4
  • 40
  • 33
  • Yeah, it probably does belong in docs - it's a reasonably FAQ on IRC – SmileyChris Jul 28 '09 at 09:22
  • 2
    Does this work with ModelForm? I can see that instance has all the attributes of the class model, but there are no values (just a str of the field name). In the template, user is hidden. I may have to submit a question, I have been googling this for hours. – mgag Mar 07 '10 at 16:46
  • Yes it works, and yes you should ask a new question (or ask for help on #django irc) – SmileyChris Mar 07 '10 at 22:51
  • 3
    Oddly enough this is failing on me in basically this same setup. instance.user has no attributes on it. – Bob Spryn Aug 19 '12 at 07:26
  • In this callable function, can I access the `Content` class id, before it is saved? I would like to prefix or sufix the filename with the object id, so I can know to whom the file belongs. – Throoze Jun 24 '13 at 19:50
  • No, you can't. To quote the documentation, "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." – SmileyChris Jul 07 '13 at 06:07
  • 14
    You might want to use `os.path.join` instead of `'/'.join` to make sure it also works on not-Unix systems. They may be rare, but it's good practice ;) – Xudonax Feb 04 '14 at 14:26
  • In fact, it's better to not use `os.path.join`. You're dealing with a Django storage, not the operating system directly and it expects '/' on both environments. – SmileyChris Feb 12 '14 at 22:34
  • So just to be clear here, the file still gets uploaded to the MEDIA_ROOT defined in settings.py? – Caleb Bramwell Feb 04 '15 at 19:36
  • If you're using the default FileSystemStorage, then yes. The FileField only ever contains the relative name and path. – SmileyChris Feb 05 '15 at 23:17
  • 2
    Hi, I tried the same code, put them in models.py, but get error Content object has no attribute 'user'. – Harry Feb 07 '15 at 15:35
  • The function cannot be static method in model; `makemigrations` rightfully complains. – Wtower Mar 01 '15 at 11:54
  • Just a note - I tried changing "content" to something else but then my adblocker got invoked, so "content" works fine at least! – Jhnsbrst Jan 06 '22 at 14:31
13

This really helped. For a bit more brevity's sake, decided to use lambda in my case:

file = models.FileField(
    upload_to=lambda instance, filename: '/'.join(['mymodel', str(instance.pk), filename]),
)
gdakram
  • 441
  • 1
  • 6
  • 13
  • 6
    This didn't work for me in Django 1.7 using migrations. Ended up creating a function instead and the migration took. – aboutaaron Mar 18 '15 at 17:38
  • Even if you can't get lambda to work using the str(instance.pk) is a good idea if you have problems with files overwriting when you don't want them to. – Joseph Dattilo Mar 11 '16 at 19:49
  • 2
    instance does not have a `pk` before saving. It only works for updates not creations (inserts). – Mohammad Jafar Mashhadi Jul 24 '17 at 16:42
  • 3
    lambda doesn't work in `migrations` operations because it cant be serialized according to the [docs](https://docs.djangoproject.com/en/2.1/ref/models/fields/#s-default) – Ebrahim Karimi Aug 13 '18 at 21:30
5

A note on using the 'instance' object's pk value. According to the documentation:

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.

Therefore the validity of using pk depends on how your particular model is defined.

Max Dudzinski
  • 63
  • 3
  • 4
4

If you have problems with migrations you probably should be using @deconstructible decorator.

import datetime
import os
import unicodedata

from django.core.files.storage import default_storage
from django.utils.deconstruct import deconstructible
from django.utils.encoding import force_text, force_str


@deconstructible
class UploadToPath(object):
    def __init__(self, upload_to):
        self.upload_to = upload_to

    def __call__(self, instance, filename):
        return self.generate_filename(filename)

    def get_directory_name(self):
        return os.path.normpath(force_text(datetime.datetime.now().strftime(force_str(self.upload_to))))

    def get_filename(self, filename):
        filename = default_storage.get_valid_name(os.path.basename(filename))
        filename = force_text(filename)
        filename = unicodedata.normalize('NFKD', filename).encode('ascii', 'ignore').decode('ascii')
        return os.path.normpath(filename)

    def generate_filename(self, filename):
        return os.path.join(self.get_directory_name(), self.get_filename(filename))

Usage:

class MyModel(models.Model):
    file = models.FileField(upload_to=UploadToPath('files/%Y/%m/%d'), max_length=255)
michal-michalak
  • 827
  • 10
  • 6
1

I wanted to change the upload path in runtime, and none of the solutions were suitable for this need.

this is what I've done:

class Content(models.Model):
    name = models.CharField(max_length=200)
    user = models.ForeignKey(User)
    file = models.FileField(upload_to=DynamicUploadPath.get_file_path)


class ContentSerializer(serializers.ModelSerializer):
    class Meta:
        model = Content
        fields = '__all__'


class UploadDir(models.TextChoices):
    PRODUCT = 'PRD', _('Product')
    USER_PROFILE = 'UP', _('User Profile')


class DynamicUploadPath:
    dir: UploadDir = None

    @classmethod
    def get_file_path(cls, instance, filename):
        return str(cls.dir.name.lower() + '/' + filename)


def set_DynamicUploadPath(dir: UploadDir):
    DynamicUploadPath.dir = dir


class UploadFile(APIView):
    parser_classes = (MultiPartParser, FormParser)

    def post(self, request):
        # file save path: MEDIA_ROOT/product/filename
        set_DynamicUploadPath(UploadDir.PRODUCT)

        # file save path: MEDIA_ROOT/user_profile/filename
        # set_DynamicUploadPath(UploadDir.USER_PROFILE)

        serializer = ContentSerializer(data=request.data)
        serializer.is_valid(raise_exception=True)
        serializer.save()

        return Response(serializer.data, status=status.HTTP_200_OK)
0

If you have a user instance, let there be a quick setup to generate

<model-slug>/<username>-<first_name>-<last_name>/filename-random.png

eg: /medias/content/ft0004-john-doe/filename-lkl9237.png


def upload_directory_name(instance, filename):

    user = getattr(instance, 'user', None)
    if user:
        name = f"{user.username}-{user.get_full_name().replace(' ', '-')}"
    else:
        name=str(instance)
    model_name = instance._meta.verbose_name.replace(' ', '-')
    return str(os.path.pathsep).join([model_name, name, filename])


class Content(models.Model):
    name = models.CharField(max_length=200)
    user = models.ForeignKey(User)
    file = models.FileField(upload_to=upload_directory_name)


[A Modified Version of @SmileyChris ]

jerinisready
  • 936
  • 10
  • 24