Skip to content

Commit 59d85b0

Browse files
committed
Added Google Go backend.
1 parent edb4036 commit 59d85b0

File tree

11 files changed

+604
-2
lines changed

11 files changed

+604
-2
lines changed

gae-go/app.yaml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
application: jquery-file-upload
2+
version: 2
3+
runtime: go
4+
api_version: 3
5+
6+
handlers:
7+
- url: /(favicon\.ico|robots\.txt|postmessage\.html)
8+
static_files: static/\1
9+
upload: static/(.*)
10+
expiration: '1d'
11+
- url: /.*
12+
script: _go_app

gae-go/app/main.go

Lines changed: 278 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,278 @@
1+
/*
2+
* jQuery File Upload Plugin GAE Go Example 1.0
3+
* https://github.com/blueimp/jQuery-File-Upload
4+
*
5+
* Copyright 2011, Sebastian Tschan
6+
* https://blueimp.net
7+
*
8+
* Licensed under the MIT license:
9+
* http://creativecommons.org/licenses/MIT/
10+
*/
11+
12+
package app
13+
14+
import (
15+
"appengine"
16+
"appengine/blobstore"
17+
"appengine/taskqueue"
18+
"fmt"
19+
"http"
20+
"image"
21+
"image/png"
22+
"io"
23+
"json"
24+
"log"
25+
"mime/multipart"
26+
"os"
27+
"regexp"
28+
"resize"
29+
"strings"
30+
"url"
31+
)
32+
33+
import _ "image/gif"
34+
import _ "image/jpeg"
35+
36+
const (
37+
WEBSITE = "http://blueimp.github.com/jQuery-File-Upload/"
38+
MIN_FILE_SIZE = 1 // bytes
39+
MAX_FILE_SIZE = 5000000 // bytes
40+
IMAGE_TYPES = "image/(gif|p?jpeg|(x-)?png)"
41+
ACCEPT_FILE_TYPES = IMAGE_TYPES
42+
EXPIRATION_TIME = 300 // seconds
43+
THUMBNAIL_MAX_WIDTH = 80
44+
THUMBNAIL_MAX_HEIGHT = THUMBNAIL_MAX_WIDTH
45+
)
46+
47+
var imageTypes, acceptFileTypes = regexp.MustCompile(IMAGE_TYPES),
48+
regexp.MustCompile(ACCEPT_FILE_TYPES)
49+
50+
type FileInfo struct {
51+
Key appengine.BlobKey `json:"-"`
52+
ThumbnailKey appengine.BlobKey `json:"-"`
53+
Index int `json:"-"`
54+
Url string `json:"url,omitempty"`
55+
ThumbnailUrl string `json:"thumbnail_url,omitempty"`
56+
Name string `json:"name"`
57+
Type string `json:"type"`
58+
Size int64 `json:"size"`
59+
Error string `json:"error,omitempty"`
60+
DeleteUrl string `json:"delete_url,omitempty"`
61+
DeleteType string `json:"delete_type,omitempty"`
62+
}
63+
64+
func (fi *FileInfo) Validate() (valid bool) {
65+
if fi.Size < MIN_FILE_SIZE {
66+
fi.Error = "minFileSize"
67+
} else if fi.Size > MAX_FILE_SIZE {
68+
fi.Error = "maxFileSize"
69+
} else if !acceptFileTypes.MatchString(fi.Type) {
70+
fi.Error = "acceptFileTypes"
71+
} else {
72+
return true
73+
}
74+
return false
75+
}
76+
77+
func (fi *FileInfo) Finalize(c appengine.Context, rURL *url.URL) {
78+
u := &url.URL{Scheme: rURL.Scheme,
79+
Host: appengine.DefaultVersionHostname(c)}
80+
u.Path = "/" + url.QueryEscape(string(fi.Key)) + "/" +
81+
url.QueryEscape(string(fi.Name))
82+
fi.Url = u.String()
83+
var tk string
84+
if fi.ThumbnailKey != "" {
85+
tk = url.QueryEscape(string(fi.ThumbnailKey))
86+
u.Path = "/" + tk + "/thumbnail.png"
87+
fi.ThumbnailUrl = u.String()
88+
}
89+
fi.DeleteUrl = fi.Url + "?thumbnail=" + tk
90+
fi.DeleteType = "DELETE"
91+
}
92+
93+
func (fi *FileInfo) CreateThumbnail(c appengine.Context, f multipart.File) {
94+
defer func() {
95+
if rec := recover(); rec != nil {
96+
log.Println(rec)
97+
}
98+
}()
99+
img, _, err := image.Decode(f)
100+
check(err)
101+
if b := img.Bounds(); b.Dx() > THUMBNAIL_MAX_WIDTH || b.Dy() > THUMBNAIL_MAX_HEIGHT {
102+
w, h := THUMBNAIL_MAX_WIDTH, THUMBNAIL_MAX_HEIGHT
103+
if b.Dx() > b.Dy() {
104+
h = b.Dy() * h / b.Dx()
105+
} else {
106+
w = b.Dx() * w / b.Dy()
107+
}
108+
img = resize.Resize(img, img.Bounds(), w, h)
109+
}
110+
w, err := blobstore.Create(c, "image/png")
111+
defer func() {
112+
w.Close()
113+
check(err)
114+
fi.ThumbnailKey, err = w.Key()
115+
check(err)
116+
}()
117+
check(err)
118+
err = png.Encode(w, img)
119+
check(err)
120+
}
121+
122+
func check(err os.Error) {
123+
if err != nil {
124+
panic(err)
125+
}
126+
}
127+
128+
func queueDeletion(c appengine.Context, fi *FileInfo) {
129+
if key := string(fi.Key); key != "" {
130+
task := &taskqueue.Task{Path: "/" + url.QueryEscape(key) + "/-",
131+
Method: "DELETE", Delay: EXPIRATION_TIME * 1000000}
132+
taskqueue.Add(c, task, "")
133+
if key = string(fi.ThumbnailKey); key != "" {
134+
task = &taskqueue.Task{Path: "/" + url.QueryEscape(key) + "/-",
135+
Method: "DELETE", Delay: EXPIRATION_TIME * 1000000}
136+
taskqueue.Add(c, task, "")
137+
}
138+
}
139+
}
140+
141+
func handleUpload(r *http.Request, fh *multipart.FileHeader, i int, c chan *FileInfo) {
142+
fi := FileInfo{Index: i, Name: fh.Filename}
143+
context := appengine.NewContext(r)
144+
defer func() {
145+
queueDeletion(context, &fi)
146+
if rec := recover(); rec != nil {
147+
fi.Error = rec.(os.Error).String()
148+
log.Println(rec)
149+
}
150+
c <- &fi
151+
}()
152+
if h, b := fh.Header["Content-Type"]; b && len(h) > 0 {
153+
fi.Type = h[0]
154+
}
155+
f, err := fh.Open()
156+
defer f.Close()
157+
check(err)
158+
fi.Size, err = f.Seek(0, 2)
159+
check(err)
160+
if fi.Validate() {
161+
_, err = f.Seek(0, 0)
162+
check(err)
163+
var w *blobstore.Writer
164+
w, err = blobstore.Create(context, fi.Type)
165+
defer func() {
166+
w.Close()
167+
check(err)
168+
fi.Key, err = w.Key()
169+
check(err)
170+
if imageTypes.MatchString(fi.Type) {
171+
_, err = f.Seek(0, 0)
172+
check(err)
173+
fi.CreateThumbnail(context, f)
174+
}
175+
fi.Finalize(context, r.URL)
176+
}()
177+
_, err = io.Copy(w, f)
178+
}
179+
}
180+
181+
func handleUploads(r *http.Request) (fileInfos []*FileInfo) {
182+
if r.MultipartForm != nil && r.MultipartForm.File != nil {
183+
for _, mfs := range r.MultipartForm.File {
184+
mfsLen := len(mfs)
185+
c := make(chan *FileInfo, mfsLen)
186+
for i, fh := range mfs {
187+
go handleUpload(r, fh, i, c)
188+
}
189+
fileInfos = make([]*FileInfo, mfsLen)
190+
for i := 0; i < mfsLen; i++ {
191+
fi := <-c
192+
fileInfos[fi.Index] = fi
193+
}
194+
}
195+
}
196+
return
197+
}
198+
199+
func get(w http.ResponseWriter, r *http.Request) {
200+
if r.URL.Path == "/" {
201+
http.Redirect(w, r, WEBSITE, http.StatusFound)
202+
return
203+
}
204+
parts := strings.Split(r.URL.Path, "/")
205+
if len(parts) == 3 {
206+
if key := parts[1]; key != "" {
207+
blobKey := appengine.BlobKey(key)
208+
blobInfo, err := blobstore.Stat(appengine.NewContext(r), blobKey)
209+
if err == nil {
210+
w.Header().Add("Cache-Control", fmt.Sprintf("public,max-age=%d", EXPIRATION_TIME))
211+
if imageTypes.MatchString(blobInfo.ContentType) {
212+
w.Header().Add("X-Content-Type-Options", "nosniff")
213+
} else {
214+
w.Header().Add("Content-Type", "application/octet-stream")
215+
w.Header().Add("Content-Disposition:", fmt.Sprintf("attachment; filename=%s;", blobInfo.Filename))
216+
}
217+
blobstore.Send(w, appengine.BlobKey(key))
218+
return
219+
}
220+
}
221+
}
222+
http.Error(w, "404 Not Found", http.StatusNotFound)
223+
}
224+
225+
func post(w http.ResponseWriter, r *http.Request) {
226+
b, err := json.Marshal(handleUploads(r))
227+
check(err)
228+
if redirect := r.FormValue("redirect"); redirect != "" {
229+
http.Redirect(w, r, fmt.Sprintf(redirect, url.QueryEscape(string(b))),
230+
http.StatusFound)
231+
return
232+
}
233+
if r.Header.Get("application/json") != "" {
234+
w.Header().Set("Content-Type", "application/json")
235+
}
236+
fmt.Fprintln(w, string(b))
237+
}
238+
239+
func delete(w http.ResponseWriter, r *http.Request) {
240+
parts := strings.Split(r.URL.Path, "/")
241+
if len(parts) != 3 {
242+
return
243+
}
244+
keys := make([]appengine.BlobKey, 0, 2)
245+
if key := parts[1]; key != "" {
246+
keys = append(keys, appengine.BlobKey(key))
247+
if key = r.FormValue("thumbnail"); key != "" {
248+
keys = append(keys, appengine.BlobKey(key))
249+
}
250+
err := blobstore.DeleteMulti(appengine.NewContext(r), keys)
251+
check(err)
252+
}
253+
}
254+
255+
func handle(w http.ResponseWriter, r *http.Request) {
256+
w.Header().Add("Access-Control-Allow-Origin", "*")
257+
w.Header().Add("Access-Control-Allow-Methods", "OPTIONS, HEAD, GET, POST, PUT, DELETE")
258+
switch r.Method {
259+
case "OPTIONS":
260+
case "HEAD":
261+
case "GET":
262+
get(w, r)
263+
case "POST":
264+
if r.FormValue("_method") == "DELETE" {
265+
delete(w, r)
266+
} else {
267+
post(w, r)
268+
}
269+
case "DELETE":
270+
delete(w, r)
271+
default:
272+
http.Error(w, "501 Not Implemented", http.StatusNotImplemented)
273+
}
274+
}
275+
276+
func init() {
277+
http.HandleFunc("/", handle)
278+
}

0 commit comments

Comments
 (0)