main.py 7.4 KB

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