Django uploads: Discard uploaded duplicates, use existing file (md5 based check)

phoibos picture phoibos · Apr 8, 2013 · Viewed 13.5k times · Source

I have a model with a FileField, which holds user uploaded files. Since I want to save space, I would like to avoid duplicates.

What I'd like to achieve:

  1. Calculate the uploaded files md5 checksum
  2. Store the file with the file name based on its md5sum
  3. If a file with that name is already there (the new file's a duplicate), discard the uploaded file and use the existing file instead

1 and 2 is already working, but how would I forget about an uploaded duplicate and use the existing file instead?

Note that I'd like to keep the existing file and not overwrite it (mainly to keep the modified time the same - better for backup).

Notes:

  • I'm using Django 1.5
  • The upload handler is django.core.files.uploadhandler.TemporaryFileUploadHandler

Code:

def media_file_name(instance, filename):
    h = instance.md5sum
    basename, ext = os.path.splitext(filename)
    return os.path.join('mediafiles', h[0:1], h[1:2], h + ext.lower())

class Media(models.Model):
    orig_file = models.FileField(upload_to=media_file_name)
    md5sum = models.CharField(max_length=36)
    ...

    def save(self, *args, **kwargs):
            if not self.pk:  # file is new
                md5 = hashlib.md5()
                for chunk in self.orig_file.chunks():
                    md5.update(chunk)
                self.md5sum = md5.hexdigest()
            super(Media, self).save(*args, **kwargs)

Any help is appreciated!

Answer

phoibos picture phoibos · Apr 9, 2013

Thanks to alTus answer, I was able to figure out that writing a custom storage class is the key, and it was easier than expected.

  • I just omit calling the superclasses _save method to write the file if it is already there and I just return the name.
  • I overwrite get_available_name, to avoid getting numbers appended to the file name if a file with the same name is already existing

I don't know if this is the proper way of doing it, but it works fine so far.

Hope this is useful!

Here's the complete sample code:

import hashlib
import os

from django.core.files.storage import FileSystemStorage
from django.db import models

class MediaFileSystemStorage(FileSystemStorage):
    def get_available_name(self, name, max_length=None):
        if max_length and len(name) > max_length:
            raise(Exception("name's length is greater than max_length"))
        return name

    def _save(self, name, content):
        if self.exists(name):
            # if the file exists, do not call the superclasses _save method
            return name
        # if the file is new, DO call it
        return super(MediaFileSystemStorage, self)._save(name, content)


def media_file_name(instance, filename):
    h = instance.md5sum
    basename, ext = os.path.splitext(filename)
    return os.path.join('mediafiles', h[0:1], h[1:2], h + ext.lower())


class Media(models.Model):
    # use the custom storage class fo the FileField
    orig_file = models.FileField(
        upload_to=media_file_name, storage=MediaFileSystemStorage())
    md5sum = models.CharField(max_length=36)
    # ...

    def save(self, *args, **kwargs):
        if not self.pk:  # file is new
            md5 = hashlib.md5()
            for chunk in self.orig_file.chunks():
                md5.update(chunk)
            self.md5sum = md5.hexdigest()
        super(Media, self).save(*args, **kwargs)