aboutsummaryrefslogtreecommitdiffstats
path: root/library/blueimp_upload/server/gae-python
diff options
context:
space:
mode:
Diffstat (limited to 'library/blueimp_upload/server/gae-python')
-rw-r--r--library/blueimp_upload/server/gae-python/app.yaml5
-rw-r--r--library/blueimp_upload/server/gae-python/main.py208
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
)