bgneal@695: """Forms for the user_photos application.""" bgneal@749: import hashlib bgneal@964: import os.path bgneal@971: import urlparse bgneal@700: bgneal@695: from django import forms bgneal@697: from django.conf import settings bgneal@695: bgneal@971: from core.download import download_file bgneal@976: from core.functions import remove_file, TemporaryFile bgneal@1195: from core.images.upload import process_upload bgneal@701: from core.services import get_redis_connection bgneal@696: from user_photos.models import Photo bgneal@696: bgneal@695: bgneal@701: def rate_limit(key, limit, seconds): bgneal@701: """Use Redis to do a rate limit check. Returns True if the limit is violated bgneal@701: and False otherwise. bgneal@701: bgneal@701: key - the key to check in Redis bgneal@701: limit - the rate limit maximum value bgneal@701: seconds - the rate limit period in seconds bgneal@701: bgneal@701: """ bgneal@701: conn = get_redis_connection() bgneal@701: val = conn.incr(key) bgneal@701: if val == 1: bgneal@701: conn.expire(key, seconds) bgneal@701: return val > limit bgneal@701: bgneal@701: bgneal@971: def rate_limit_user(user): bgneal@971: """Shared function to rate limit user uploads.""" bgneal@971: key = 'user_photos:counts:' + user.username bgneal@971: limit = settings.USER_PHOTOS_RATE_LIMIT bgneal@971: if rate_limit(key, *limit): bgneal@971: raise forms.ValidationError("You've exceeded your upload quota. " bgneal@971: "Please try again later.") bgneal@971: bgneal@971: bgneal@695: class UploadForm(forms.Form): bgneal@695: image_file = forms.ImageField() bgneal@696: bgneal@696: def __init__(self, *args, **kwargs): bgneal@696: self.user = kwargs.pop('user') bgneal@696: super(UploadForm, self).__init__(*args, **kwargs) bgneal@696: bgneal@701: def clean(self): bgneal@701: cleaned_data = super(UploadForm, self).clean() bgneal@971: rate_limit_user(self.user) bgneal@701: return cleaned_data bgneal@701: bgneal@696: def save(self): bgneal@696: """Processes the image and creates a new Photo object, which is saved to bgneal@696: the database. The new Photo instance is returned. bgneal@696: bgneal@749: Note that we do de-duplication. A signature is computed for the photo. bgneal@749: If the user has already uploaded a file with the same signature, that bgneal@749: photo object is returned instead. bgneal@749: bgneal@696: This function should only be called if is_valid() returns True. bgneal@696: bgneal@696: """ bgneal@749: # Check for duplicate uploads from this user bgneal@749: signature = self._signature() bgneal@749: try: bgneal@749: return Photo.objects.get(user=self.user, signature=signature) bgneal@749: except Photo.DoesNotExist: bgneal@749: pass bgneal@749: bgneal@964: # Trying to use PIL (or Pillow) on a Django UploadedFile is often bgneal@964: # problematic because the file is often an in-memory file if it is under bgneal@964: # a certain size. This complicates matters and many of the operations we try bgneal@964: # to perform on it fail if this is the case. To get around these issues, bgneal@964: # we make a copy of the file on the file system and operate on the copy. bgneal@700: bgneal@964: fp = self.cleaned_data['image_file'] bgneal@964: ext = os.path.splitext(fp.name)[1] bgneal@964: bgneal@964: # Write the UploadedFile to a temporary file on disk bgneal@964: with TemporaryFile(suffix=ext) as t: bgneal@964: for chunk in fp.chunks(): bgneal@964: t.file.write(chunk) bgneal@964: t.file.close() bgneal@964: bgneal@1195: url, thumb_url = process_upload(self.user, t.filename) bgneal@700: bgneal@749: photo = Photo(user=self.user, url=url, thumb_url=thumb_url, bgneal@749: signature=signature) bgneal@696: photo.save() bgneal@696: return photo bgneal@749: bgneal@749: def _signature(self): bgneal@749: """Calculates and returns a signature for the image file as a hex digest bgneal@749: string. bgneal@749: bgneal@749: This function should only be called if is_valid() is True. bgneal@749: bgneal@749: """ bgneal@749: fp = self.cleaned_data['image_file'] bgneal@749: md5 = hashlib.md5() bgneal@749: for chunk in fp.chunks(): bgneal@749: md5.update(chunk) bgneal@749: return md5.hexdigest() bgneal@971: bgneal@971: bgneal@971: class HotLinkImageForm(forms.Form): bgneal@971: """Form for hot-linking images. If the supplied URL's host is on bgneal@971: a white-list, return it as-is. Otherwise attempt to download the image at bgneal@971: the given URL and upload it into our S3 storage. The URL to the new location bgneal@971: is returned. bgneal@971: """ bgneal@971: url = forms.URLField() bgneal@971: bgneal@971: def __init__(self, *args, **kwargs): bgneal@971: self.user = kwargs.pop('user') bgneal@971: super(HotLinkImageForm, self).__init__(*args, **kwargs) bgneal@971: bgneal@971: def clean_url(self): bgneal@971: the_url = self.cleaned_data['url'] bgneal@971: self.url_parts = urlparse.urlsplit(the_url) bgneal@971: if self.url_parts.scheme not in ['http', 'https']: bgneal@971: raise forms.ValidationError("Invalid URL scheme") bgneal@971: return the_url bgneal@971: bgneal@971: def clean(self): bgneal@971: cleaned_data = super(HotLinkImageForm, self).clean() bgneal@971: rate_limit_user(self.user) bgneal@971: return cleaned_data bgneal@971: bgneal@971: def save(self): bgneal@971: url = self.url_parts.geturl() bgneal@971: if (self.url_parts.scheme == 'https' and bgneal@971: self.url_parts.hostname in settings.USER_IMAGES_SOURCES): bgneal@971: return url bgneal@971: bgneal@971: # Try to download the file bgneal@976: with remove_file(download_file(url)) as path: bgneal@1195: url, _ = process_upload(self.user, path) bgneal@971: bgneal@971: return url