-
-
Save dokterbob/1183767 to your computer and use it in GitHub Desktop.
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) |
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.
In order to get the validator working with django migration,one may use a @deconstructible class decorator.