/*
* jQuery File Upload Plugin GAE Go Example 2.0
* 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/memcache"
"appengine/taskqueue"
"bytes"
"encoding/base64"
"encoding/json"
"fmt"
"image"
"image/png"
"io"
"log"
"mime/multipart"
"net/http"
"net/url"
"regexp"
"resize"
"strings"
"time"
)
import _ "image/gif"
import _ "image/jpeg"
const (
WEBSITE = "http://blueimp.github.com/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_MAX_WIDTH = 80
THUMBNAIL_MAX_HEIGHT = THUMBNAIL_MAX_WIDTH
)
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:"thumbnail_url,omitempty"`
Name string `json:"name"`
Type string `json:"type"`
Size int64 `json:"size"`
Error string `json:"error,omitempty"`
DeleteUrl string `json:"delete_url,omitempty"`
DeleteType string `json:"delete_type,omitempty"`
}
func (fi *FileInfo) ValidateType() (valid bool) {
if acceptFileTypes.MatchString(fi.Type) {
return true
}
fi.Error = "acceptFileTypes"
return false
}
func (fi *FileInfo) ValidateSize() (valid bool) {
if fi.Size < MIN_FILE_SIZE {
fi.Error = "minFileSize"
} else if fi.Size > MAX_FILE_SIZE {
fi.Error = "maxFileSize"
} 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
fi.DeleteType = "DELETE"
if fi.ThumbnailUrl != "" && -1 == strings.Index(
r.Header.Get("Accept"),
"application/json",
) {
fi.ThumbnailUrl = uString + "thumbnails/" +
escape(string(fi.Key))
}
}
func (fi *FileInfo) CreateThumbnail(r io.Reader, c appengine.Context) (data []byte, err error) {
defer func() {
if rec := recover(); rec != nil {
log.Println(rec)
// 1x1 pixel transparent GIf, bas64 encoded:
s := "R0lGODlhAQABAIAAAP///////yH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=="
data, _ = base64.StdEncoding.DecodeString(s)
fi.ThumbnailUrl = "data:image/gif;base64," + s
}
memcache.Add(c, &memcache.Item{
Key: string(fi.Key),
Value: data,
Expiration: EXPIRATION_TIME,
})
}()
img, _, err := image.Decode(r)
check(err)
if bounds := img.Bounds(); bounds.Dx() > THUMBNAIL_MAX_WIDTH ||
bounds.Dy() > THUMBNAIL_MAX_HEIGHT {
w, h := THUMBNAIL_MAX_WIDTH, THUMBNAIL_MAX_HEIGHT
if bounds.Dx() > bounds.Dy() {
h = bounds.Dy() * h / bounds.Dx()
} else {
w = bounds.Dx() * w / bounds.Dy()
}
img = resize.Resize(img, img.Bounds(), w, h)
}
var b bytes.Buffer
err = png.Encode(&b, img)
check(err)
data = b.Bytes()
fi.ThumbnailUrl = "data:image/png;base64," +
base64.StdEncoding.EncodeToString(data)
return
}
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()
}
}()
var b bytes.Buffer
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)
if b.Len() > 0 {
fi.CreateThumbnail(&b, context)
}
fi.CreateUrls(r, context)
}()
check(err)
var wr io.Writer = w
if imageTypes.MatchString(fi.Type) {
wr = io.MultiWriter(&b, w)
}
_, err = io.Copy(wr, 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(
"Cache-Control",
fmt.Sprintf("public,max-age=%d", EXPIRATION_TIME),
)
if imageTypes.MatchString(bi.ContentType) {
w.Header().Add("X-Content-Type-Options", "nosniff")
} else {
w.Header().Add("Content-Type", "application/octet-stream")
w.Header().Add(
"Content-Disposition:",
fmt.Sprintf("attachment; filename=%s;", parts[2]),
)
}
blobstore.Send(w, appengine.BlobKey(key))
return
}
}
}
http.Error(w, "404 Not Found", http.StatusNotFound)
}
func post(w http.ResponseWriter, r *http.Request) {
b, err := json.Marshal(handleUploads(r))
check(err)
if redirect := r.FormValue("redirect"); redirect != "" {
http.Redirect(w, r, fmt.Sprintf(
redirect,
escape(string(b)),
), http.StatusFound)
return
}
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
}
if key := parts[1]; key != "" {
c := appengine.NewContext(r)
blobstore.Delete(c, appengine.BlobKey(key))
memcache.Delete(c, key)
}
}
func serveThumbnail(w http.ResponseWriter, r *http.Request) {
parts := strings.Split(r.URL.Path, "/")
if len(parts) == 3 {
if key := parts[2]; key != "" {
var data []byte
c := appengine.NewContext(r)
item, err := memcache.Get(c, key)
if err == nil {
data = item.Value
} else {
blobKey := appengine.BlobKey(key)
if _, err = blobstore.Stat(c, blobKey); err == nil {
fi := FileInfo{Key: blobKey}
data, _ = fi.CreateThumbnail(
blobstore.NewReader(c, blobKey),
c,
)
}
}
if err == nil && len(data) > 3 {
w.Header().Add(
"Cache-Control",
fmt.Sprintf("public,max-age=%d", EXPIRATION_TIME),
)
contentType := "image/png"
if string(data[:3]) == "GIF" {
contentType = "image/gif"
} else if string(data[1:4]) != "PNG" {
contentType = "image/jpeg"
}
w.Header().Set("Content-Type", contentType)
fmt.Fprintln(w, string(data))
return
}
}
}
http.Error(w, "404 Not Found", http.StatusNotFound)
}
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",
)
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)
http.HandleFunc("/thumbnails/", serveThumbnail)
}
|