Skip to content

Instantly share code, notes, and snippets.

@dokterbob
Created August 31, 2011 15:03
Show Gist options
  • Save dokterbob/1183767 to your computer and use it in GitHub Desktop.
Save dokterbob/1183767 to your computer and use it in GitHub Desktop.
Validator for files, checking the size, extension and mimetype.
from os.path import splitext
from django.core.exceptions import ValidationError
from django.utils.translation import ugettext_lazy as _
from django.template.defaultfilters import filesizeformat
class FileValidator(object):
"""
Validator for files, checking the size, extension and mimetype.
Initialization parameters:
allowed_extensions: iterable with allowed file extensions
ie. ('txt', 'doc')
allowd_mimetypes: iterable with allowed mimetypes
ie. ('image/png', )
min_size: minimum number of bytes allowed
ie. 100
max_size: maximum number of bytes allowed
ie. 24*1024*1024 for 24 MB
Usage example::
MyModel(models.Model):
myfile = FileField(validators=FileValidator(max_size=24*1024*1024), ...)
"""
extension_message = _("Extension '%(extension)s' not allowed. Allowed extensions are: '%(allowed_extensions)s.'")
mime_message = _("MIME type '%(mimetype)s' is not valid. Allowed types are: %(allowed_mimetypes)s.")
min_size_message = _('The current file %(size)s, which is too small. The minumum file size is %(allowed_size)s.')
max_size_message = _('The current file %(size)s, which is too large. The maximum file size is %(allowed_size)s.')
def __init__(self, *args, **kwargs):
self.allowed_extensions = kwargs.pop('allowed_extensions', None)
self.allowed_mimetypes = kwargs.pop('allowed_mimetypes', None)
self.min_size = kwargs.pop('min_size', 0)
self.max_size = kwargs.pop('max_size', None)
def __call__(self, value):
"""
Check the extension, content type and file size.
"""
# Check the extension
ext = splitext(value.name)[1][1:].lower()
if self.allowed_extensions and not ext in self.allowed_extensions:
message = self.extension_message % {
'extension' : ext,
'allowed_extensions': ', '.join(self.allowed_extensions)
}
raise ValidationError(message)
# Check the content type
mimetype = value.file.content_type
if self.allowed_mimetypes and not mimetype in self.allowed_mimetypes:
message = self.mime_message % {
'mimetype': mimetype,
'allowed_mimetypes': ', '.join(self.allowed_mimetypes)
}
raise ValidationError(message)
# Check the file size
filesize = value.file._size
if self.max_size and filesize > self.max_size:
message = self.max_size_message % {
'size': filesizeformat(filesize),
'allowed_size': filesizeformat(self.max_size)
}
raise ValidationError(message)
elif filesize < self.min_size:
message = self.min_size_message % {
'size': filesizeformat(filesize),
'allowed_size': filesizeformat(self.min_size)
}
raise ValidationError(message)
@eecp
Copy link

eecp commented Nov 28, 2015

In order to get the validator working with django migration,one may use a @deconstructible class decorator.

@eecp
Copy link

eecp commented Nov 28, 2015

I walked a little further down that path - in my case this validator worked only the first time a file was uploaded but not later on (e.g. when the model object containing the file field was just saved without having changed anything related to the picture). The reason turned out to be the fact that in the just-uploaded case the file in value is an instance of InMemoryUploadedFile, which provides the content_type and _size attribute. But later on when the validator is called upon an already uploaded file the file in value suddenly is just a File instance which doesn't have (anymore? maybe some Django version change, i'm using v1.8) those attributes. I suspect that the file type may even change in upload case if the file is big and not stored in memory. Haven't tested this yet.

Additionally i found related to other posts around that topic, that the content_type provided with the uploaded file is not based on in-depth-analysis of the file, but on the information provided by the client browser - thus can not really be trusted. If you want to get more accurate information regarding the file type i recommend using python-magic package. Using this i finally came up with the following modification:

    # Check the content type
    if value.file.__class__ == InMemoryUploadedFile:
        buffer = value.file.file.getvalue()
        #  ..  as the file is already in memory, buffer length should not exhaust the system ...
    else:
        buffer = value.file.file.read()
        # ... but here this may become an issue ...

    mimetype = magic.from_buffer(buffer, mime=True).decode("utf-8")

    if self.allowed_mimetypes and not mimetype in self.allowed_mimetypes:
        message = self.mime_message % {
            'mimetype': mimetype,
            'allowed_mimetypes': ', '.join(self.allowed_mimetypes)
        }

        raise ValidationError(message)

    # Check the file size
    if value.file.__class__ == InMemoryUploadedFile:
        filesize = value.file._size
    else:
        filesize = value.file.size

Anyway thanks a lot for that validator idea. I think that's the most 'django'ic way to validate a file and better than creating custom model fields or messing with the clean methods.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment