forked from qwerty/tupali
205 lines
7.4 KiB
Python
205 lines
7.4 KiB
Python
|
# -*- coding: utf-8 -*-
|
||
|
#
|
||
|
# 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
|
||
|
#
|
||
|
|
||
|
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 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
|
||
|
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 json_stringify(self, obj):
|
||
|
return json.dumps(obj, separators=(',', ':'))
|
||
|
|
||
|
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'
|
||
|
elif file['size'] > MAX_FILE_SIZE:
|
||
|
file['error'] = 'File is too big'
|
||
|
elif not ACCEPT_FILE_TYPES.match(file['type']):
|
||
|
file['error'] = 'Filetype not allowed'
|
||
|
else:
|
||
|
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
|
||
|
file.seek(0) # Reset the file position to the beginning
|
||
|
return size
|
||
|
|
||
|
def write_blob(self, data, info):
|
||
|
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 = []
|
||
|
for name, fieldStorage in self.request.POST.items():
|
||
|
if type(fieldStorage) is unicode:
|
||
|
continue
|
||
|
result = {}
|
||
|
result['name'] = urllib.unquote(fieldStorage.filename)
|
||
|
result['type'] = fieldStorage.type
|
||
|
result['size'] = self.get_file_size(fieldStorage.file)
|
||
|
if self.validate(result):
|
||
|
key, thumbnail_key = self.write_blob(
|
||
|
fieldStorage.value,
|
||
|
result
|
||
|
)
|
||
|
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)
|
||
|
return results
|
||
|
|
||
|
def head(self):
|
||
|
pass
|
||
|
|
||
|
def get(self):
|
||
|
self.redirect(WEBSITE)
|
||
|
|
||
|
def post(self):
|
||
|
if (self.request.get('_method') == 'DELETE'):
|
||
|
return self.delete()
|
||
|
result = {'files': self.handle_upload()}
|
||
|
s = self.json_stringify(result)
|
||
|
redirect = self.request.get('redirect')
|
||
|
if self.validate_redirect(redirect):
|
||
|
return self.redirect(str(
|
||
|
redirect.replace('%s', urllib.quote(s, ''), 1)
|
||
|
))
|
||
|
if 'application/json' in self.request.headers.get('Accept'):
|
||
|
self.response.headers['Content-Type'] = 'application/json'
|
||
|
self.response.write(s)
|
||
|
|
||
|
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)
|
||
|
|
||
|
app = webapp2.WSGIApplication(
|
||
|
[
|
||
|
('/', UploadHandler),
|
||
|
('/(.+)/([^/]+)/([^/]+)', FileHandler)
|
||
|
],
|
||
|
debug=DEBUG
|
||
|
)
|