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:
 | |
| # https://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
 | |
| )
 |