How to update a file location in a FileField?
Asked Answered
M

4

12

I have a FileField in a model. For each instance of the model, I would like that the filename on the disk stays updated with the value of another field (let's call it label) of the model.

At the moment, I use a custom upload_to() function that generates the right filename when a new file is first uploaded. But if I change the value of label, the filename is not updated when saving the model.

In the save() function of the model I could (a) calculate the new filename from label (also checking that the new name would not correspond to another existing file on the disk), (b) rename the file on the disk and (c) set the new file location in the FileField. But is there no simpler way to do that?

Misanthrope answered 26/8, 2014 at 21:18 Comment(4)
There is no easier way. In fact, there is not even a clean way to rename files – unless you are willing to break support for non-file-system backends, you will have to delete the old file and create a new one. Have you considered to instead create a wrapper that redirects/links the labeled filename to the original file?Anthropomorphosis
There is, check django documentation.Froude
@Phillip. It's not true, there is an easy way to do it (see my answer). There is a clean way to rename files using django as well. You are right though, it will work only in file system backend.Marinna
@Alexey Kuleshevich If whether a solution works depends on a configuration variable having a particular value said solution cannot be clean. Also, it is arguable if replacing a manual os.rename call in the model's save() method with a large third-party dependency counts as being an easier solution. (That being said, django-smartfields still looks very promising if your goal is to replace code with dependency declarations.)Anthropomorphosis
P
6

All solutions posted here, and all of the solutions I've seen on the web involves the use of third-party apps or the solution you already have.

I agree with @Phillip, there is no easier way to do what you want, even with the use of third-party apps it would require some work in order to adapt it to your purposes.

If you have many models that needs this behaviour, just implement a pre_save signal and write that code only once.

I recommend you to read Django Signals too, I'm sure you'll find it very interesting.

Very simple example:

from django.db.models.signals import pre_save
from django.dispatch import receiver

@receiver(pre_save, sender=Product)
def my_signal_handler(sender, instance, **kwargs):
    """
    Sender here would be the model, Product in your case.
    Instance is the product instance being saved.
    """
    # Write your solution here.
Piscator answered 15/4, 2015 at 15:44 Comment(1)
Basically the same answer as mine (posted at the same time as well) but more complete so, if you have to upvote one of the two, upvote this one ;-)Preoccupied
M
2

Here is an app that can take care of it for you django-smartfields. I added a special processor just for that purpose, just because it seems like a useful functionality.

How this works:

  • changes the name of the file as specified in upload_to, and
  • FileDependency will take care of the filename whenever label field is changed.

Worth noting is that file will be renamed using file_move_safe but only in case of FileSystemStorage, as @Phillip mentioned you don't want to do it with Cloud file storage, since usually those backends don't support file renaming.

Also couple notes. You don't have to use UploadTo class, regular function will do. If you don't specify keep_orphans, file will be removed whenever model instance is removed.

from django.db import models
from smartfields import fields, processors
from smartfields.dependencies import FileDependency
from smartfields.utils import UploadTo

def name_getter(name, instance):
    return instance.label

class TestModel(models.Model):
    label = fields.CharField(max_length=32, dependencies=[
        FileDependency(attname='dynamic_file', keep_orphans=True,
                       processor=processors.RenameFileProcessor())
    ])
    dynamic_file = models.FileField(
        upload_to=UploadTo(name=name_getter, add_pk=False))
Marinna answered 9/4, 2015 at 23:1 Comment(0)
P
2

I think your approach with the save() method is the right and "simple" one except I would do it using the pre_save signal instead of overriding the save method (which is often a bad idea).

If this is a behavior you would like to repeat on other models, using a method connected to the pre_save signal also allows you to simply re-use the method.

For more information on pre_save: https://docs.djangoproject.com/en/1.8/ref/signals/#pre-save

Preoccupied answered 15/4, 2015 at 15:39 Comment(0)
F
1

Ok, it cannot be a lambda, since lambdas are not serializable for some reason but here's the easy answer.

def pic_loc(instance, filename):
"""
:param instance: Product Instance
:param filename: name of image being uploaded
:return: image location for the upload
"""
return '/'.join([str(instance.pk), str(instance.slug), filename])


Class Product(models.Model):
    image = models.ImageField(upload_to=pic_loc)
    slug = models.SlugField()
    user = models.ForeignKey(User, related_name="products")

Then to find say pk = 1 with:

slug = 'new-thing' would be //myexample.com/MEDIA_ROOT/1/new-thing/mything.png

<img src="{{ obj.image.url }}">

This is assuming you have MEDIA_ROOT set since uploads go to media and media url. Serve up like you did static files if in production, name it after MEDIA_URL.

upload_to passes both the object instance and the filename to a your function, from there you can manipulate it.

To change the actual filename you need to do some extra work in the save() method.

from django.core.files import File

class Product(models.Model):
label = CharField(max_length=255)
...
def save(self, **kwargs):
    # here we use os.rename then change the name of the file
    # add condition to do this, I suggest requerying the model
    # and checking if label is different
    if self.pk:  # Need this to mitigate error using self.pk
        if Product.objects.get(pk=self.pk).label != self.label: 
            path = self.image.path
            rename = '/'.join(path.split('/')[:-1]) + '/' + self.label
            os.rename(path, rename)
            file = File(open(rename))
            self.image.save(self.label, file)
    return super(Product, self).save(**kwargs)

If the file extension is important which it very well may be, either add it to label when creating label or we take the old file extension as part of the string:

filename, file_extention = os.splitext(path)
rename += file_extension  # before renaming and add to label too

os.rename(path, rename)
self.image.save(self.label + file_extension, file)

I would actually suggest writing a rename function as part of your app_label.utils

To check if the file exists is simply

if os.path.isfile(rename):
    # you can also do this before renaming, 
    # maybe raise an error if the file already exists
Froude answered 9/4, 2015 at 19:35 Comment(5)
With this code, I think that changing the slug value of an existing Product instance would not update the filename.Misanthrope
Ill add to the save method give me a couple of minutes, this is a good option on instantiating the object but to continually change it I'll finish the answerFroude
Well, that's exactly the solution that I described in the question, and I was wondering if there was an easier solution that that.Misanthrope
Wow I gotta learn how to read lol. Could always make it a method, could use a form, but the amount of code remains the same, I've been looking through django docs and nothing with regards to it. Ill add to answer a bit.Froude
@danihp Thanks for the link. I now catched up with the latest developments in the doc. I will look at all the answers on SO in a couple of days, but the new solution from the doc (docs.djangoproject.com/en/1.8/topics/files/…) may be the best!Misanthrope

© 2022 - 2024 — McMap. All rights reserved.