diff options
Diffstat (limited to 'library/blueimp_upload/server/gae-python')
-rw-r--r-- | library/blueimp_upload/server/gae-python/app.yaml | 5 | ||||
-rw-r--r-- | library/blueimp_upload/server/gae-python/main.py | 208 |
2 files changed, 124 insertions, 89 deletions
diff --git a/library/blueimp_upload/server/gae-python/app.yaml b/library/blueimp_upload/server/gae-python/app.yaml index 5fe123f59..764449b74 100644 --- a/library/blueimp_upload/server/gae-python/app.yaml +++ b/library/blueimp_upload/server/gae-python/app.yaml @@ -4,8 +4,9 @@ runtime: python27 api_version: 1 threadsafe: true -builtins: -- deferred: on +libraries: +- name: PIL + version: latest handlers: - url: /(favicon\.ico|robots\.txt) diff --git a/library/blueimp_upload/server/gae-python/main.py b/library/blueimp_upload/server/gae-python/main.py index 6276be6a0..1955ac00a 100644 --- a/library/blueimp_upload/server/gae-python/main.py +++ b/library/blueimp_upload/server/gae-python/main.py @@ -1,49 +1,57 @@ # -*- coding: utf-8 -*- # -# jQuery File Upload Plugin GAE Python Example 2.2.0 +# jQuery File Upload Plugin GAE Python Example # https://github.com/blueimp/jQuery-File-Upload # # Copyright 2011, Sebastian Tschan # https://blueimp.net # # Licensed under the MIT license: -# http://www.opensource.org/licenses/MIT +# https://opensource.org/licenses/MIT # -from __future__ import with_statement -from google.appengine.api import files, images -from google.appengine.ext import blobstore, deferred -from google.appengine.ext.webapp import blobstore_handlers +from google.appengine.api import memcache, images import json +import os import re import urllib import webapp2 +DEBUG=os.environ.get('SERVER_SOFTWARE', '').startswith('Dev') WEBSITE = 'https://blueimp.github.io/jQuery-File-Upload/' MIN_FILE_SIZE = 1 # bytes -MAX_FILE_SIZE = 5000000 # bytes +# Max file size is memcache limit (1MB) minus key size minus overhead: +MAX_FILE_SIZE = 999000 # bytes IMAGE_TYPES = re.compile('image/(gif|p?jpeg|(x-)?png)') ACCEPT_FILE_TYPES = IMAGE_TYPES -THUMBNAIL_MODIFICATOR = '=s80' # max width / height +THUMB_MAX_WIDTH = 80 +THUMB_MAX_HEIGHT = 80 +THUMB_SUFFIX = '.'+str(THUMB_MAX_WIDTH)+'x'+str(THUMB_MAX_HEIGHT)+'.png' EXPIRATION_TIME = 300 # seconds +# If set to None, only allow redirects to the referer protocol+host. +# Set to a regexp for custom pattern matching against the redirect value: +REDIRECT_ALLOW_TARGET = None + +class CORSHandler(webapp2.RequestHandler): + def cors(self): + headers = self.response.headers + headers['Access-Control-Allow-Origin'] = '*' + headers['Access-Control-Allow-Methods'] =\ + 'OPTIONS, HEAD, GET, POST, DELETE' + headers['Access-Control-Allow-Headers'] =\ + 'Content-Type, Content-Range, Content-Disposition' + def initialize(self, request, response): + super(CORSHandler, self).initialize(request, response) + self.cors() -def cleanup(blob_keys): - blobstore.delete(blob_keys) - - -class UploadHandler(webapp2.RequestHandler): + def json_stringify(self, obj): + return json.dumps(obj, separators=(',', ':')) - def initialize(self, request, response): - super(UploadHandler, self).initialize(request, response) - self.response.headers['Access-Control-Allow-Origin'] = '*' - self.response.headers[ - 'Access-Control-Allow-Methods' - ] = 'OPTIONS, HEAD, GET, POST, PUT, DELETE' - self.response.headers[ - 'Access-Control-Allow-Headers' - ] = 'Content-Type, Content-Range, Content-Disposition' + def options(self, *args, **kwargs): + pass +class UploadHandler(CORSHandler): def validate(self, file): if file['size'] < MIN_FILE_SIZE: file['error'] = 'File is too small' @@ -55,6 +63,20 @@ class UploadHandler(webapp2.RequestHandler): return True return False + def validate_redirect(self, redirect): + if redirect: + if REDIRECT_ALLOW_TARGET: + return REDIRECT_ALLOW_TARGET.match(redirect) + referer = self.request.headers['referer'] + if referer: + from urlparse import urlparse + parts = urlparse(referer) + redirect_allow_target = '^' + re.escape( + parts.scheme + '://' + parts.netloc + '/' + ) + return re.match(redirect_allow_target, redirect) + return False + def get_file_size(self, file): file.seek(0, 2) # Seek to the end of the file size = file.tell() # Get the position of EOF @@ -62,64 +84,58 @@ class UploadHandler(webapp2.RequestHandler): return size def write_blob(self, data, info): - blob = files.blobstore.create( - mime_type=info['type'], - _blobinfo_uploaded_filename=info['name'] - ) - with files.open(blob, 'a') as f: - f.write(data) - files.finalize(blob) - return files.blobstore.get_blob_key(blob) + key = urllib.quote(info['type'].encode('utf-8'), '') +\ + '/' + str(hash(data)) +\ + '/' + urllib.quote(info['name'].encode('utf-8'), '') + try: + memcache.set(key, data, time=EXPIRATION_TIME) + except: #Failed to add to memcache + return (None, None) + thumbnail_key = None + if IMAGE_TYPES.match(info['type']): + try: + img = images.Image(image_data=data) + img.resize( + width=THUMB_MAX_WIDTH, + height=THUMB_MAX_HEIGHT + ) + thumbnail_data = img.execute_transforms() + thumbnail_key = key + THUMB_SUFFIX + memcache.set( + thumbnail_key, + thumbnail_data, + time=EXPIRATION_TIME + ) + except: #Failed to resize Image or add to memcache + thumbnail_key = None + return (key, thumbnail_key) def handle_upload(self): results = [] - blob_keys = [] for name, fieldStorage in self.request.POST.items(): if type(fieldStorage) is unicode: continue result = {} - result['name'] = re.sub( - r'^.*\\', - '', - fieldStorage.filename - ) + result['name'] = urllib.unquote(fieldStorage.filename) result['type'] = fieldStorage.type result['size'] = self.get_file_size(fieldStorage.file) if self.validate(result): - blob_key = str( - self.write_blob(fieldStorage.value, result) + key, thumbnail_key = self.write_blob( + fieldStorage.value, + result ) - blob_keys.append(blob_key) - result['deleteType'] = 'DELETE' - result['deleteUrl'] = self.request.host_url +\ - '/?key=' + urllib.quote(blob_key, '') - if (IMAGE_TYPES.match(result['type'])): - try: - result['url'] = images.get_serving_url( - blob_key, - secure_url=self.request.host_url.startswith( - 'https' - ) - ) - result['thumbnailUrl'] = result['url'] +\ - THUMBNAIL_MODIFICATOR - except: # Could not get an image serving url - pass - if not 'url' in result: - result['url'] = self.request.host_url +\ - '/' + blob_key + '/' + urllib.quote( - result['name'].encode('utf-8'), '') + if key is not None: + result['url'] = self.request.host_url + '/' + key + result['deleteUrl'] = result['url'] + result['deleteType'] = 'DELETE' + if thumbnail_key is not None: + result['thumbnailUrl'] = self.request.host_url +\ + '/' + thumbnail_key + else: + result['error'] = 'Failed to store uploaded file.' results.append(result) - deferred.defer( - cleanup, - blob_keys, - _countdown=EXPIRATION_TIME - ) return results - def options(self): - pass - def head(self): pass @@ -130,9 +146,9 @@ class UploadHandler(webapp2.RequestHandler): if (self.request.get('_method') == 'DELETE'): return self.delete() result = {'files': self.handle_upload()} - s = json.dumps(result, separators=(',', ':')) + s = self.json_stringify(result) redirect = self.request.get('redirect') - if redirect: + if self.validate_redirect(redirect): return self.redirect(str( redirect.replace('%s', urllib.quote(s, ''), 1) )) @@ -140,31 +156,49 @@ class UploadHandler(webapp2.RequestHandler): self.response.headers['Content-Type'] = 'application/json' self.response.write(s) - def delete(self): - key = self.request.get('key') or '' - blobstore.delete(key) - s = json.dumps({key: True}, separators=(',', ':')) +class FileHandler(CORSHandler): + def normalize(self, str): + return urllib.quote(urllib.unquote(str), '') + + def get(self, content_type, data_hash, file_name): + content_type = self.normalize(content_type) + file_name = self.normalize(file_name) + key = content_type + '/' + data_hash + '/' + file_name + data = memcache.get(key) + if data is None: + return self.error(404) + # Prevent browsers from MIME-sniffing the content-type: + self.response.headers['X-Content-Type-Options'] = 'nosniff' + content_type = urllib.unquote(content_type) + if not IMAGE_TYPES.match(content_type): + # Force a download dialog for non-image types: + content_type = 'application/octet-stream' + elif file_name.endswith(THUMB_SUFFIX): + content_type = 'image/png' + self.response.headers['Content-Type'] = content_type + # Cache for the expiration time: + self.response.headers['Cache-Control'] = 'public,max-age=%d' \ + % EXPIRATION_TIME + self.response.write(data) + + def delete(self, content_type, data_hash, file_name): + content_type = self.normalize(content_type) + file_name = self.normalize(file_name) + key = content_type + '/' + data_hash + '/' + file_name + result = {key: memcache.delete(key)} + content_type = urllib.unquote(content_type) + if IMAGE_TYPES.match(content_type): + thumbnail_key = key + THUMB_SUFFIX + result[thumbnail_key] = memcache.delete(thumbnail_key) if 'application/json' in self.request.headers.get('Accept'): self.response.headers['Content-Type'] = 'application/json' + s = self.json_stringify(result) self.response.write(s) - -class DownloadHandler(blobstore_handlers.BlobstoreDownloadHandler): - def get(self, key, filename): - if not blobstore.get(key): - self.error(404) - else: - # Prevent browsers from MIME-sniffing the content-type: - self.response.headers['X-Content-Type-Options'] = 'nosniff' - # Cache for the expiration time: - self.response.headers['Cache-Control'] = 'public,max-age=%d' % EXPIRATION_TIME - # Send the file forcing a download dialog: - self.send_blob(key, save_as=filename, content_type='application/octet-stream') - app = webapp2.WSGIApplication( [ ('/', UploadHandler), - ('/([^/]+)/([^/]+)', DownloadHandler) + ('/(.+)/([^/]+)/([^/]+)', FileHandler) ], - debug=True + debug=DEBUG ) |