2 * jQuery File Upload Plugin GAE Go Example 2.0
3 * https://github.com/blueimp/jQuery-File-Upload
5 * Copyright 2011, Sebastian Tschan
8 * Licensed under the MIT license:
9 * http://www.opensource.org/licenses/MIT
40 WEBSITE = "http://blueimp.github.com/jQuery-File-Upload/"
41 MIN_FILE_SIZE = 1 // bytes
42 MAX_FILE_SIZE = 5000000 // bytes
43 IMAGE_TYPES = "image/(gif|p?jpeg|(x-)?png)"
44 ACCEPT_FILE_TYPES = IMAGE_TYPES
45 EXPIRATION_TIME = 300 // seconds
46 THUMBNAIL_MAX_WIDTH = 80
47 THUMBNAIL_MAX_HEIGHT = THUMBNAIL_MAX_WIDTH
51 imageTypes = regexp.MustCompile(IMAGE_TYPES)
52 acceptFileTypes = regexp.MustCompile(ACCEPT_FILE_TYPES)
55 type FileInfo struct {
56 Key appengine.BlobKey `json:"-"`
57 Url string `json:"url,omitempty"`
58 ThumbnailUrl string `json:"thumbnail_url,omitempty"`
59 Name string `json:"name"`
60 Type string `json:"type"`
61 Size int64 `json:"size"`
62 Error string `json:"error,omitempty"`
63 DeleteUrl string `json:"delete_url,omitempty"`
64 DeleteType string `json:"delete_type,omitempty"`
67 func (fi *FileInfo) ValidateType() (valid bool) {
68 if acceptFileTypes.MatchString(fi.Type) {
71 fi.Error = "acceptFileTypes"
75 func (fi *FileInfo) ValidateSize() (valid bool) {
76 if fi.Size < MIN_FILE_SIZE {
77 fi.Error = "minFileSize"
78 } else if fi.Size > MAX_FILE_SIZE {
79 fi.Error = "maxFileSize"
86 func (fi *FileInfo) CreateUrls(r *http.Request, c appengine.Context) {
89 Host: appengine.DefaultVersionHostname(c),
93 fi.Url = uString + escape(string(fi.Key)) + "/" +
94 escape(string(fi.Name))
96 fi.DeleteType = "DELETE"
97 if fi.ThumbnailUrl != "" && -1 == strings.Index(
98 r.Header.Get("Accept"),
101 fi.ThumbnailUrl = uString + "thumbnails/" +
102 escape(string(fi.Key))
106 func (fi *FileInfo) CreateThumbnail(r io.Reader, c appengine.Context) (data []byte, err error) {
108 if rec := recover(); rec != nil {
110 // 1x1 pixel transparent GIf, bas64 encoded:
111 s := "R0lGODlhAQABAIAAAP///////yH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=="
112 data, _ = base64.StdEncoding.DecodeString(s)
113 fi.ThumbnailUrl = "data:image/gif;base64," + s
115 memcache.Add(c, &memcache.Item{
118 Expiration: EXPIRATION_TIME,
121 img, _, err := image.Decode(r)
123 if bounds := img.Bounds(); bounds.Dx() > THUMBNAIL_MAX_WIDTH ||
124 bounds.Dy() > THUMBNAIL_MAX_HEIGHT {
125 w, h := THUMBNAIL_MAX_WIDTH, THUMBNAIL_MAX_HEIGHT
126 if bounds.Dx() > bounds.Dy() {
127 h = bounds.Dy() * h / bounds.Dx()
129 w = bounds.Dx() * w / bounds.Dy()
131 img = resize.Resize(img, img.Bounds(), w, h)
134 err = png.Encode(&b, img)
137 fi.ThumbnailUrl = "data:image/png;base64," +
138 base64.StdEncoding.EncodeToString(data)
142 func check(err error) {
148 func escape(s string) string {
149 return strings.Replace(url.QueryEscape(s), "+", "%20", -1)
152 func delayedDelete(c appengine.Context, fi *FileInfo) {
153 if key := string(fi.Key); key != "" {
154 task := &taskqueue.Task{
155 Path: "/" + escape(key) + "/-",
157 Delay: time.Duration(EXPIRATION_TIME) * time.Second,
159 taskqueue.Add(c, task, "")
163 func handleUpload(r *http.Request, p *multipart.Part) (fi *FileInfo) {
166 Type: p.Header.Get("Content-Type"),
168 if !fi.ValidateType() {
172 if rec := recover(); rec != nil {
174 fi.Error = rec.(error).Error()
178 lr := &io.LimitedReader{R: p, N: MAX_FILE_SIZE + 1}
179 context := appengine.NewContext(r)
180 w, err := blobstore.Create(context, fi.Type)
183 fi.Size = MAX_FILE_SIZE + 1 - lr.N
184 fi.Key, err = w.Key()
186 if !fi.ValidateSize() {
187 err := blobstore.Delete(context, fi.Key)
191 delayedDelete(context, fi)
193 fi.CreateThumbnail(&b, context)
195 fi.CreateUrls(r, context)
199 if imageTypes.MatchString(fi.Type) {
200 wr = io.MultiWriter(&b, w)
202 _, err = io.Copy(wr, lr)
206 func getFormValue(p *multipart.Part) string {
208 io.CopyN(&b, p, int64(1<<20)) // Copy max: 1 MiB
212 func handleUploads(r *http.Request) (fileInfos []*FileInfo) {
213 fileInfos = make([]*FileInfo, 0)
214 mr, err := r.MultipartReader()
216 r.Form, err = url.ParseQuery(r.URL.RawQuery)
218 part, err := mr.NextPart()
220 if name := part.FormName(); name != "" {
221 if part.FileName() != "" {
222 fileInfos = append(fileInfos, handleUpload(r, part))
224 r.Form[name] = append(r.Form[name], getFormValue(part))
227 part, err = mr.NextPart()
232 func get(w http.ResponseWriter, r *http.Request) {
233 if r.URL.Path == "/" {
234 http.Redirect(w, r, WEBSITE, http.StatusFound)
237 parts := strings.Split(r.URL.Path, "/")
239 if key := parts[1]; key != "" {
240 blobKey := appengine.BlobKey(key)
241 bi, err := blobstore.Stat(appengine.NewContext(r), blobKey)
245 fmt.Sprintf("public,max-age=%d", EXPIRATION_TIME),
247 if imageTypes.MatchString(bi.ContentType) {
248 w.Header().Add("X-Content-Type-Options", "nosniff")
250 w.Header().Add("Content-Type", "application/octet-stream")
252 "Content-Disposition:",
253 fmt.Sprintf("attachment; filename=%s;", parts[2]),
256 blobstore.Send(w, appengine.BlobKey(key))
261 http.Error(w, "404 Not Found", http.StatusNotFound)
264 func post(w http.ResponseWriter, r *http.Request) {
265 b, err := json.Marshal(handleUploads(r))
267 if redirect := r.FormValue("redirect"); redirect != "" {
268 http.Redirect(w, r, fmt.Sprintf(
274 jsonType := "application/json"
275 if strings.Index(r.Header.Get("Accept"), jsonType) != -1 {
276 w.Header().Set("Content-Type", jsonType)
278 fmt.Fprintln(w, string(b))
281 func delete(w http.ResponseWriter, r *http.Request) {
282 parts := strings.Split(r.URL.Path, "/")
286 if key := parts[1]; key != "" {
287 c := appengine.NewContext(r)
288 blobstore.Delete(c, appengine.BlobKey(key))
289 memcache.Delete(c, key)
293 func serveThumbnail(w http.ResponseWriter, r *http.Request) {
294 parts := strings.Split(r.URL.Path, "/")
296 if key := parts[2]; key != "" {
298 c := appengine.NewContext(r)
299 item, err := memcache.Get(c, key)
303 blobKey := appengine.BlobKey(key)
304 if _, err = blobstore.Stat(c, blobKey); err == nil {
305 fi := FileInfo{Key: blobKey}
306 data, _ = fi.CreateThumbnail(
307 blobstore.NewReader(c, blobKey),
312 if err == nil && len(data) > 3 {
315 fmt.Sprintf("public,max-age=%d", EXPIRATION_TIME),
317 contentType := "image/png"
318 if string(data[:3]) == "GIF" {
319 contentType = "image/gif"
320 } else if string(data[1:4]) != "PNG" {
321 contentType = "image/jpeg"
323 w.Header().Set("Content-Type", contentType)
324 fmt.Fprintln(w, string(data))
329 http.Error(w, "404 Not Found", http.StatusNotFound)
332 func handle(w http.ResponseWriter, r *http.Request) {
333 params, err := url.ParseQuery(r.URL.RawQuery)
335 w.Header().Add("Access-Control-Allow-Origin", "*")
337 "Access-Control-Allow-Methods",
338 "OPTIONS, HEAD, GET, POST, PUT, DELETE",
346 if len(params["_method"]) > 0 && params["_method"][0] == "DELETE" {
354 http.Error(w, "501 Not Implemented", http.StatusNotImplemented)
359 http.HandleFunc("/", handle)
360 http.HandleFunc("/thumbnails/", serveThumbnail)