Django FileField with upload_to determined at runtime
Question:
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!
Answers:
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.
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]),
)
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.
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)
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 ]
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)
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!
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.
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]),
)
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.
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)
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 ]
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)