Dynamic path for django FileSystemStorage

midhun picture midhun · Sep 3, 2015 · Viewed 12.4k times · Source

I was trying to save some files using Django FileSystemStorage. My model is as shown below

key_store = FileSystemStorage(
location='account/files/'+datetime.date.today().isoformat())


class Account(models.Model):
    name = models.CharField(max_length=100, null=True, blank=True)
    user = models.ForeignKey(User, related_name='auth_user_account_relation')
    subscription_id = models.CharField(max_length=100, null=True, blank=True)
    info_file = models.FileField(storage=key_store)

But when I save an object of this model only the file name is stored in the db.

So when I try to access the path it returns path appended with todays date and not as the uploaded date. ie. If I upload a file on say 09-21-2015 and I try to access the path on the next day it will return account/files/09-22-2015/<file_name> which will be an invalid path. So what tweak should be made to store absolute path in db. Or what am I doing wrong here?

Answer

dhke picture dhke · Sep 3, 2015

I'm pretty sure, your version does not do what you intend it to:

key_store = FileSystemStorage(
    location='account/files/'+datetime.date.today().isoformat()
)

is evaluated when the module is loaded (usually only when you start your application). After that the date stays fixed as long as the module is not re-loaded. Hence it points to a directory with the name of the date the application was started, which is probably not what you want.

Also, FileSystemStorage serializes with full path name, meaning that this also triggers a migration every other day (because the storage path changed).

You can solve this problem by using a combination of FileSystemStorage (to store files outside of the media directory) and upload_to with a callable:

key_store = FileSystemStorage(location='account/files/')

def key_store_upload_to(instance, path):
    # prepend date to path
    return os.path.join(
        datetime.date.today().isoformat(), 
        path
    )

class Account(models.Model):
    # [...]
    info_file = models.FileField(storage=key_store, upload_to=key_store_upload_to)

This uses a new FileSystemStorage for the base directory of uploaded files and upload_to to determine the name of an uploaded file relative to the storage's root.

Edit 1: Thanks for pointing out that upload_to takes a date format.

Since upload_to also takes strftime() format specifiers, a pure-date related path choice can also be implemented without the callable:

info_file = models.FileField(storage=key_store, upload_to='account/files/%Y-%m-%d')

As for explanation:

A storage is essentially an abstraction of a hierarchical file system in django. FileFields always store their files in a storage. By default the media storage is used, but that can be changed.

To determine the path a file is uploaded to, FileField roughly does the following

  1. Retrieve the request path, which is the path the file was uploaded with.
  2. Check if the upload_to option specified for the file field. If upload_to is a string, use this string as a base directory. upload_to is run through strftime() to process any date specifiers. Concatenate upload_to and the request path, which results in a target path relative to the storage.
  3. If upload_to is a callable, call it with (instance, request_path) and use the return value as the target path (relative to the storage).
  4. Check if the file already exists within the storage. If so, derive a new, unique path.
  5. Save file to the storage with the target path.
  6. Store the target path (still relative to storage) inside the database.

As one can see, the storage is meant to be more or less static. Changing the storage potentially invalidates all existing file paths in the database, because they are relative to the storage.