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