1# -*- coding: utf-8 -*- 2# 3# jQuery File Upload Plugin GAE Python Example 4# https://github.com/blueimp/jQuery-File-Upload 5# 6# Copyright 2011, Sebastian Tschan 7# https://blueimp.net 8# 9# Licensed under the MIT license: 10# https://opensource.org/licenses/MIT 11# 12 13from google.appengine.api import memcache, images 14import json 15import os 16import re 17import urllib 18import webapp2 19 20DEBUG=os.environ.get('SERVER_SOFTWARE', '').startswith('Dev') 21WEBSITE = 'https://blueimp.github.io/jQuery-File-Upload/' 22MIN_FILE_SIZE = 1 # bytes 23# Max file size is memcache limit (1MB) minus key size minus overhead: 24MAX_FILE_SIZE = 999000 # bytes 25IMAGE_TYPES = re.compile('image/(gif|p?jpeg|(x-)?png)') 26ACCEPT_FILE_TYPES = IMAGE_TYPES 27THUMB_MAX_WIDTH = 80 28THUMB_MAX_HEIGHT = 80 29THUMB_SUFFIX = '.'+str(THUMB_MAX_WIDTH)+'x'+str(THUMB_MAX_HEIGHT)+'.png' 30EXPIRATION_TIME = 300 # seconds 31# If set to None, only allow redirects to the referer protocol+host. 32# Set to a regexp for custom pattern matching against the redirect value: 33REDIRECT_ALLOW_TARGET = None 34 35class CORSHandler(webapp2.RequestHandler): 36 def cors(self): 37 headers = self.response.headers 38 headers['Access-Control-Allow-Origin'] = '*' 39 headers['Access-Control-Allow-Methods'] =\ 40 'OPTIONS, HEAD, GET, POST, DELETE' 41 headers['Access-Control-Allow-Headers'] =\ 42 'Content-Type, Content-Range, Content-Disposition' 43 44 def initialize(self, request, response): 45 super(CORSHandler, self).initialize(request, response) 46 self.cors() 47 48 def json_stringify(self, obj): 49 return json.dumps(obj, separators=(',', ':')) 50 51 def options(self, *args, **kwargs): 52 pass 53 54class UploadHandler(CORSHandler): 55 def validate(self, file): 56 if file['size'] < MIN_FILE_SIZE: 57 file['error'] = 'File is too small' 58 elif file['size'] > MAX_FILE_SIZE: 59 file['error'] = 'File is too big' 60 elif not ACCEPT_FILE_TYPES.match(file['type']): 61 file['error'] = 'Filetype not allowed' 62 else: 63 return True 64 return False 65 66 def validate_redirect(self, redirect): 67 if redirect: 68 if REDIRECT_ALLOW_TARGET: 69 return REDIRECT_ALLOW_TARGET.match(redirect) 70 referer = self.request.headers['referer'] 71 if referer: 72 from urlparse import urlparse 73 parts = urlparse(referer) 74 redirect_allow_target = '^' + re.escape( 75 parts.scheme + '://' + parts.netloc + '/' 76 ) 77 return re.match(redirect_allow_target, redirect) 78 return False 79 80 def get_file_size(self, file): 81 file.seek(0, 2) # Seek to the end of the file 82 size = file.tell() # Get the position of EOF 83 file.seek(0) # Reset the file position to the beginning 84 return size 85 86 def write_blob(self, data, info): 87 key = urllib.quote(info['type'].encode('utf-8'), '') +\ 88 '/' + str(hash(data)) +\ 89 '/' + urllib.quote(info['name'].encode('utf-8'), '') 90 try: 91 memcache.set(key, data, time=EXPIRATION_TIME) 92 except: #Failed to add to memcache 93 return (None, None) 94 thumbnail_key = None 95 if IMAGE_TYPES.match(info['type']): 96 try: 97 img = images.Image(image_data=data) 98 img.resize( 99 width=THUMB_MAX_WIDTH, 100 height=THUMB_MAX_HEIGHT 101 ) 102 thumbnail_data = img.execute_transforms() 103 thumbnail_key = key + THUMB_SUFFIX 104 memcache.set( 105 thumbnail_key, 106 thumbnail_data, 107 time=EXPIRATION_TIME 108 ) 109 except: #Failed to resize Image or add to memcache 110 thumbnail_key = None 111 return (key, thumbnail_key) 112 113 def handle_upload(self): 114 results = [] 115 for name, fieldStorage in self.request.POST.items(): 116 if type(fieldStorage) is unicode: 117 continue 118 result = {} 119 result['name'] = urllib.unquote(fieldStorage.filename) 120 result['type'] = fieldStorage.type 121 result['size'] = self.get_file_size(fieldStorage.file) 122 if self.validate(result): 123 key, thumbnail_key = self.write_blob( 124 fieldStorage.value, 125 result 126 ) 127 if key is not None: 128 result['url'] = self.request.host_url + '/' + key 129 result['deleteUrl'] = result['url'] 130 result['deleteType'] = 'DELETE' 131 if thumbnail_key is not None: 132 result['thumbnailUrl'] = self.request.host_url +\ 133 '/' + thumbnail_key 134 else: 135 result['error'] = 'Failed to store uploaded file.' 136 results.append(result) 137 return results 138 139 def head(self): 140 pass 141 142 def get(self): 143 self.redirect(WEBSITE) 144 145 def post(self): 146 if (self.request.get('_method') == 'DELETE'): 147 return self.delete() 148 result = {'files': self.handle_upload()} 149 s = self.json_stringify(result) 150 redirect = self.request.get('redirect') 151 if self.validate_redirect(redirect): 152 return self.redirect(str( 153 redirect.replace('%s', urllib.quote(s, ''), 1) 154 )) 155 if 'application/json' in self.request.headers.get('Accept'): 156 self.response.headers['Content-Type'] = 'application/json' 157 self.response.write(s) 158 159class FileHandler(CORSHandler): 160 def normalize(self, str): 161 return urllib.quote(urllib.unquote(str), '') 162 163 def get(self, content_type, data_hash, file_name): 164 content_type = self.normalize(content_type) 165 file_name = self.normalize(file_name) 166 key = content_type + '/' + data_hash + '/' + file_name 167 data = memcache.get(key) 168 if data is None: 169 return self.error(404) 170 # Prevent browsers from MIME-sniffing the content-type: 171 self.response.headers['X-Content-Type-Options'] = 'nosniff' 172 content_type = urllib.unquote(content_type) 173 if not IMAGE_TYPES.match(content_type): 174 # Force a download dialog for non-image types: 175 content_type = 'application/octet-stream' 176 elif file_name.endswith(THUMB_SUFFIX): 177 content_type = 'image/png' 178 self.response.headers['Content-Type'] = content_type 179 # Cache for the expiration time: 180 self.response.headers['Cache-Control'] = 'public,max-age=%d' \ 181 % EXPIRATION_TIME 182 self.response.write(data) 183 184 def delete(self, content_type, data_hash, file_name): 185 content_type = self.normalize(content_type) 186 file_name = self.normalize(file_name) 187 key = content_type + '/' + data_hash + '/' + file_name 188 result = {key: memcache.delete(key)} 189 content_type = urllib.unquote(content_type) 190 if IMAGE_TYPES.match(content_type): 191 thumbnail_key = key + THUMB_SUFFIX 192 result[thumbnail_key] = memcache.delete(thumbnail_key) 193 if 'application/json' in self.request.headers.get('Accept'): 194 self.response.headers['Content-Type'] = 'application/json' 195 s = self.json_stringify(result) 196 self.response.write(s) 197 198app = webapp2.WSGIApplication( 199 [ 200 ('/', UploadHandler), 201 ('/(.+)/([^/]+)/([^/]+)', FileHandler) 202 ], 203 debug=DEBUG 204) 205