297 lines
		
	
	
		
			6.9 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			297 lines
		
	
	
		
			6.9 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| /*
 | |
|  * jQuery File Upload Plugin GAE Go Example 3.1.1
 | |
|  * https://github.com/blueimp/jQuery-File-Upload
 | |
|  *
 | |
|  * Copyright 2011, Sebastian Tschan
 | |
|  * https://blueimp.net
 | |
|  *
 | |
|  * Licensed under the MIT license:
 | |
|  * http://www.opensource.org/licenses/MIT
 | |
|  */
 | |
| 
 | |
| package app
 | |
| 
 | |
| import (
 | |
| 	"appengine"
 | |
| 	"appengine/blobstore"
 | |
| 	"appengine/image"
 | |
| 	"appengine/taskqueue"
 | |
| 	"bytes"
 | |
| 	"encoding/json"
 | |
| 	"fmt"
 | |
| 	"io"
 | |
| 	"log"
 | |
| 	"mime/multipart"
 | |
| 	"net/http"
 | |
| 	"net/url"
 | |
| 	"regexp"
 | |
| 	"strings"
 | |
| 	"time"
 | |
| )
 | |
| 
 | |
| const (
 | |
| 	WEBSITE           = "http://blueimp.github.io/jQuery-File-Upload/"
 | |
| 	MIN_FILE_SIZE     = 1       // bytes
 | |
| 	MAX_FILE_SIZE     = 5000000 // bytes
 | |
| 	IMAGE_TYPES       = "image/(gif|p?jpeg|(x-)?png)"
 | |
| 	ACCEPT_FILE_TYPES = IMAGE_TYPES
 | |
| 	EXPIRATION_TIME   = 300 // seconds
 | |
| 	THUMBNAIL_PARAM   = "=s80"
 | |
| )
 | |
| 
 | |
| var (
 | |
| 	imageTypes      = regexp.MustCompile(IMAGE_TYPES)
 | |
| 	acceptFileTypes = regexp.MustCompile(ACCEPT_FILE_TYPES)
 | |
| )
 | |
| 
 | |
| type FileInfo struct {
 | |
| 	Key          appengine.BlobKey `json:"-"`
 | |
| 	Url          string            `json:"url,omitempty"`
 | |
| 	ThumbnailUrl string            `json:"thumbnailUrl,omitempty"`
 | |
| 	Name         string            `json:"name"`
 | |
| 	Type         string            `json:"type"`
 | |
| 	Size         int64             `json:"size"`
 | |
| 	Error        string            `json:"error,omitempty"`
 | |
| 	DeleteUrl    string            `json:"deleteUrl,omitempty"`
 | |
| 	DeleteType   string            `json:"deleteType,omitempty"`
 | |
| }
 | |
| 
 | |
| func (fi *FileInfo) ValidateType() (valid bool) {
 | |
| 	if acceptFileTypes.MatchString(fi.Type) {
 | |
| 		return true
 | |
| 	}
 | |
| 	fi.Error = "Filetype not allowed"
 | |
| 	return false
 | |
| }
 | |
| 
 | |
| func (fi *FileInfo) ValidateSize() (valid bool) {
 | |
| 	if fi.Size < MIN_FILE_SIZE {
 | |
| 		fi.Error = "File is too small"
 | |
| 	} else if fi.Size > MAX_FILE_SIZE {
 | |
| 		fi.Error = "File is too big"
 | |
| 	} else {
 | |
| 		return true
 | |
| 	}
 | |
| 	return false
 | |
| }
 | |
| 
 | |
| func (fi *FileInfo) CreateUrls(r *http.Request, c appengine.Context) {
 | |
| 	u := &url.URL{
 | |
| 		Scheme: r.URL.Scheme,
 | |
| 		Host:   appengine.DefaultVersionHostname(c),
 | |
| 		Path:   "/",
 | |
| 	}
 | |
| 	uString := u.String()
 | |
| 	fi.Url = uString + escape(string(fi.Key)) + "/" +
 | |
| 		escape(string(fi.Name))
 | |
| 	fi.DeleteUrl = fi.Url + "?delete=true"
 | |
| 	fi.DeleteType = "DELETE"
 | |
| 	if imageTypes.MatchString(fi.Type) {
 | |
| 		servingUrl, err := image.ServingURL(
 | |
| 			c,
 | |
| 			fi.Key,
 | |
| 			&image.ServingURLOptions{
 | |
| 				Secure: strings.HasSuffix(u.Scheme, "s"),
 | |
| 				Size:   0,
 | |
| 				Crop:   false,
 | |
| 			},
 | |
| 		)
 | |
| 		check(err)
 | |
| 		fi.ThumbnailUrl = servingUrl.String() + THUMBNAIL_PARAM
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func check(err error) {
 | |
| 	if err != nil {
 | |
| 		panic(err)
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func escape(s string) string {
 | |
| 	return strings.Replace(url.QueryEscape(s), "+", "%20", -1)
 | |
| }
 | |
| 
 | |
| func delayedDelete(c appengine.Context, fi *FileInfo) {
 | |
| 	if key := string(fi.Key); key != "" {
 | |
| 		task := &taskqueue.Task{
 | |
| 			Path:   "/" + escape(key) + "/-",
 | |
| 			Method: "DELETE",
 | |
| 			Delay:  time.Duration(EXPIRATION_TIME) * time.Second,
 | |
| 		}
 | |
| 		taskqueue.Add(c, task, "")
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func handleUpload(r *http.Request, p *multipart.Part) (fi *FileInfo) {
 | |
| 	fi = &FileInfo{
 | |
| 		Name: p.FileName(),
 | |
| 		Type: p.Header.Get("Content-Type"),
 | |
| 	}
 | |
| 	if !fi.ValidateType() {
 | |
| 		return
 | |
| 	}
 | |
| 	defer func() {
 | |
| 		if rec := recover(); rec != nil {
 | |
| 			log.Println(rec)
 | |
| 			fi.Error = rec.(error).Error()
 | |
| 		}
 | |
| 	}()
 | |
| 	lr := &io.LimitedReader{R: p, N: MAX_FILE_SIZE + 1}
 | |
| 	context := appengine.NewContext(r)
 | |
| 	w, err := blobstore.Create(context, fi.Type)
 | |
| 	defer func() {
 | |
| 		w.Close()
 | |
| 		fi.Size = MAX_FILE_SIZE + 1 - lr.N
 | |
| 		fi.Key, err = w.Key()
 | |
| 		check(err)
 | |
| 		if !fi.ValidateSize() {
 | |
| 			err := blobstore.Delete(context, fi.Key)
 | |
| 			check(err)
 | |
| 			return
 | |
| 		}
 | |
| 		delayedDelete(context, fi)
 | |
| 		fi.CreateUrls(r, context)
 | |
| 	}()
 | |
| 	check(err)
 | |
| 	_, err = io.Copy(w, lr)
 | |
| 	return
 | |
| }
 | |
| 
 | |
| func getFormValue(p *multipart.Part) string {
 | |
| 	var b bytes.Buffer
 | |
| 	io.CopyN(&b, p, int64(1<<20)) // Copy max: 1 MiB
 | |
| 	return b.String()
 | |
| }
 | |
| 
 | |
| func handleUploads(r *http.Request) (fileInfos []*FileInfo) {
 | |
| 	fileInfos = make([]*FileInfo, 0)
 | |
| 	mr, err := r.MultipartReader()
 | |
| 	check(err)
 | |
| 	r.Form, err = url.ParseQuery(r.URL.RawQuery)
 | |
| 	check(err)
 | |
| 	part, err := mr.NextPart()
 | |
| 	for err == nil {
 | |
| 		if name := part.FormName(); name != "" {
 | |
| 			if part.FileName() != "" {
 | |
| 				fileInfos = append(fileInfos, handleUpload(r, part))
 | |
| 			} else {
 | |
| 				r.Form[name] = append(r.Form[name], getFormValue(part))
 | |
| 			}
 | |
| 		}
 | |
| 		part, err = mr.NextPart()
 | |
| 	}
 | |
| 	return
 | |
| }
 | |
| 
 | |
| func get(w http.ResponseWriter, r *http.Request) {
 | |
| 	if r.URL.Path == "/" {
 | |
| 		http.Redirect(w, r, WEBSITE, http.StatusFound)
 | |
| 		return
 | |
| 	}
 | |
| 	parts := strings.Split(r.URL.Path, "/")
 | |
| 	if len(parts) == 3 {
 | |
| 		if key := parts[1]; key != "" {
 | |
| 			blobKey := appengine.BlobKey(key)
 | |
| 			bi, err := blobstore.Stat(appengine.NewContext(r), blobKey)
 | |
| 			if err == nil {
 | |
| 				w.Header().Add("X-Content-Type-Options", "nosniff")
 | |
| 				if !imageTypes.MatchString(bi.ContentType) {
 | |
| 					w.Header().Add("Content-Type", "application/octet-stream")
 | |
| 					w.Header().Add(
 | |
| 						"Content-Disposition",
 | |
| 						fmt.Sprintf("attachment; filename=\"%s\"", parts[2]),
 | |
| 					)
 | |
| 				}
 | |
| 				w.Header().Add(
 | |
| 					"Cache-Control",
 | |
| 					fmt.Sprintf("public,max-age=%d", EXPIRATION_TIME),
 | |
| 				)
 | |
| 				blobstore.Send(w, blobKey)
 | |
| 				return
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 	http.Error(w, "404 Not Found", http.StatusNotFound)
 | |
| }
 | |
| 
 | |
| func post(w http.ResponseWriter, r *http.Request) {
 | |
|     result := make(map[string][]*FileInfo, 1)
 | |
|     result["files"] = handleUploads(r)
 | |
| 	b, err := json.Marshal(result)
 | |
| 	check(err)
 | |
| 	if redirect := r.FormValue("redirect"); redirect != "" {
 | |
| 	    if strings.Contains(redirect, "%s") {
 | |
| 	        redirect = fmt.Sprintf(
 | |
|     			redirect,
 | |
|     			escape(string(b)),
 | |
|     		)
 | |
| 	    }
 | |
| 		http.Redirect(w, r, redirect, http.StatusFound)
 | |
| 		return
 | |
| 	}
 | |
| 	w.Header().Set("Cache-Control", "no-cache")
 | |
| 	jsonType := "application/json"
 | |
| 	if strings.Index(r.Header.Get("Accept"), jsonType) != -1 {
 | |
| 		w.Header().Set("Content-Type", jsonType)
 | |
| 	}
 | |
| 	fmt.Fprintln(w, string(b))
 | |
| }
 | |
| 
 | |
| func delete(w http.ResponseWriter, r *http.Request) {
 | |
| 	parts := strings.Split(r.URL.Path, "/")
 | |
| 	if len(parts) != 3 {
 | |
| 		return
 | |
| 	}
 | |
| 	result := make(map[string]bool, 1)
 | |
| 	if key := parts[1]; key != "" {
 | |
| 		c := appengine.NewContext(r)
 | |
| 		blobKey := appengine.BlobKey(key)
 | |
| 		err := blobstore.Delete(c, blobKey)
 | |
| 		check(err)
 | |
| 		err = image.DeleteServingURL(c, blobKey)
 | |
| 		check(err)
 | |
| 		result[key] = true
 | |
| 	}
 | |
| 	jsonType := "application/json"
 | |
| 	if strings.Index(r.Header.Get("Accept"), jsonType) != -1 {
 | |
| 		w.Header().Set("Content-Type", jsonType)
 | |
| 	}
 | |
| 	b, err := json.Marshal(result)
 | |
| 	check(err)
 | |
| 	fmt.Fprintln(w, string(b))
 | |
| }
 | |
| 
 | |
| func handle(w http.ResponseWriter, r *http.Request) {
 | |
| 	params, err := url.ParseQuery(r.URL.RawQuery)
 | |
| 	check(err)
 | |
| 	w.Header().Add("Access-Control-Allow-Origin", "*")
 | |
| 	w.Header().Add(
 | |
| 		"Access-Control-Allow-Methods",
 | |
| 		"OPTIONS, HEAD, GET, POST, PUT, DELETE",
 | |
| 	)
 | |
| 	w.Header().Add(
 | |
| 		"Access-Control-Allow-Headers",
 | |
| 		"Content-Type, Content-Range, Content-Disposition",
 | |
| 	)
 | |
| 	switch r.Method {
 | |
| 	case "OPTIONS":
 | |
| 	case "HEAD":
 | |
| 	case "GET":
 | |
| 		get(w, r)
 | |
| 	case "POST":
 | |
| 		if len(params["_method"]) > 0 && params["_method"][0] == "DELETE" {
 | |
| 			delete(w, r)
 | |
| 		} else {
 | |
| 			post(w, r)
 | |
| 		}
 | |
| 	case "DELETE":
 | |
| 		delete(w, r)
 | |
| 	default:
 | |
| 		http.Error(w, "501 Not Implemented", http.StatusNotImplemented)
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func init() {
 | |
| 	http.HandleFunc("/", handle)
 | |
| }
 |