1 # -*- coding: utf-8 -*-
3 # jQuery File Upload Plugin GAE Python Example
4 # https://github.com/blueimp/jQuery-File-Upload
6 # Copyright 2011, Sebastian Tschan
9 # Licensed under the MIT license:
10 # https://opensource.org/licenses/MIT
13 from google.appengine.api import memcache, images
20 DEBUG=os.environ.get('SERVER_SOFTWARE', '').startswith('Dev')
21 WEBSITE = 'https://blueimp.github.io/jQuery-File-Upload/'
22 MIN_FILE_SIZE = 1 # bytes
23 # Max file size is memcache limit (1MB) minus key size minus overhead:
24 MAX_FILE_SIZE = 999000 # bytes
25 IMAGE_TYPES = re.compile('image/(gif|p?jpeg|(x-)?png)')
26 ACCEPT_FILE_TYPES = IMAGE_TYPES
29 THUMB_SUFFIX = '.'+str(THUMB_MAX_WIDTH)+'x'+str(THUMB_MAX_HEIGHT)+'.png'
30 EXPIRATION_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:
33 REDIRECT_ALLOW_TARGET = None
35 class CORSHandler(webapp2.RequestHandler):
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'
44 def initialize(self, request, response):
45 super(CORSHandler, self).initialize(request, response)
48 def json_stringify(self, obj):
49 return json.dumps(obj, separators=(',', ':'))
51 def options(self, *args, **kwargs):
54 class 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'
66 def validate_redirect(self, redirect):
68 if REDIRECT_ALLOW_TARGET:
69 return REDIRECT_ALLOW_TARGET.match(redirect)
70 referer = self.request.headers['referer']
72 from urlparse import urlparse
73 parts = urlparse(referer)
74 redirect_allow_target = '^' + re.escape(
75 parts.scheme + '://' + parts.netloc + '/'
77 return re.match(redirect_allow_target, redirect)
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
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'), '')
91 memcache.set(key, data, time=EXPIRATION_TIME)
92 except: #Failed to add to memcache
95 if IMAGE_TYPES.match(info['type']):
97 img = images.Image(image_data=data)
99 width=THUMB_MAX_WIDTH,
100 height=THUMB_MAX_HEIGHT
102 thumbnail_data = img.execute_transforms()
103 thumbnail_key = key + THUMB_SUFFIX
109 except: #Failed to resize Image or add to memcache
111 return (key, thumbnail_key)
113 def handle_upload(self):
115 for name, fieldStorage in self.request.POST.items():
116 if type(fieldStorage) is unicode:
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(
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 +\
135 result['error'] = 'Failed to store uploaded file.'
136 results.append(result)
143 self.redirect(WEBSITE)
146 if (self.request.get('_method') == '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)
155 if 'application/json' in self.request.headers.get('Accept'):
156 self.response.headers['Content-Type'] = 'application/json'
157 self.response.write(s)
159 class FileHandler(CORSHandler):
160 def normalize(self, str):
161 return urllib.quote(urllib.unquote(str), '')
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)
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' \
182 self.response.write(data)
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)
198 app = webapp2.WSGIApplication(
200 ('/', UploadHandler),
201 ('/(.+)/([^/]+)/([^/]+)', FileHandler)