initial commit
[namibia] / public / scripts / blueimp-jQuery-File-Upload / server / gae-go / app / main.go
1 /*
2  * jQuery File Upload Plugin GAE Go Example 2.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://www.opensource.org/licenses/MIT
10  */
11
12 package app
13
14 import (
15         "appengine"
16         "appengine/blobstore"
17         "appengine/memcache"
18         "appengine/taskqueue"
19         "bytes"
20         "encoding/base64"
21         "encoding/json"
22         "fmt"
23         "image"
24         "image/png"
25         "io"
26         "log"
27         "mime/multipart"
28         "net/http"
29         "net/url"
30         "regexp"
31         "resize"
32         "strings"
33         "time"
34 )
35
36 import _ "image/gif"
37 import _ "image/jpeg"
38
39 const (
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
48 )
49
50 var (
51         imageTypes      = regexp.MustCompile(IMAGE_TYPES)
52         acceptFileTypes = regexp.MustCompile(ACCEPT_FILE_TYPES)
53 )
54
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"`
65 }
66
67 func (fi *FileInfo) ValidateType() (valid bool) {
68         if acceptFileTypes.MatchString(fi.Type) {
69                 return true
70         }
71         fi.Error = "acceptFileTypes"
72         return false
73 }
74
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"
80         } else {
81                 return true
82         }
83         return false
84 }
85
86 func (fi *FileInfo) CreateUrls(r *http.Request, c appengine.Context) {
87         u := &url.URL{
88                 Scheme: r.URL.Scheme,
89                 Host:   appengine.DefaultVersionHostname(c),
90                 Path:   "/",
91         }
92         uString := u.String()
93         fi.Url = uString + escape(string(fi.Key)) + "/" +
94                 escape(string(fi.Name))
95         fi.DeleteUrl = fi.Url
96         fi.DeleteType = "DELETE"
97         if fi.ThumbnailUrl != "" && -1 == strings.Index(
98                 r.Header.Get("Accept"),
99                 "application/json",
100         ) {
101                 fi.ThumbnailUrl = uString + "thumbnails/" +
102                         escape(string(fi.Key))
103         }
104 }
105
106 func (fi *FileInfo) CreateThumbnail(r io.Reader, c appengine.Context) (data []byte, err error) {
107         defer func() {
108                 if rec := recover(); rec != nil {
109                         log.Println(rec)
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
114                 }
115                 memcache.Add(c, &memcache.Item{
116                         Key:        string(fi.Key),
117                         Value:      data,
118                         Expiration: EXPIRATION_TIME,
119                 })
120         }()
121         img, _, err := image.Decode(r)
122         check(err)
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()
128                 } else {
129                         w = bounds.Dx() * w / bounds.Dy()
130                 }
131                 img = resize.Resize(img, img.Bounds(), w, h)
132         }
133         var b bytes.Buffer
134         err = png.Encode(&b, img)
135         check(err)
136         data = b.Bytes()
137         fi.ThumbnailUrl = "data:image/png;base64," +
138                 base64.StdEncoding.EncodeToString(data)
139         return
140 }
141
142 func check(err error) {
143         if err != nil {
144                 panic(err)
145         }
146 }
147
148 func escape(s string) string {
149         return strings.Replace(url.QueryEscape(s), "+", "%20", -1)
150 }
151
152 func delayedDelete(c appengine.Context, fi *FileInfo) {
153         if key := string(fi.Key); key != "" {
154                 task := &taskqueue.Task{
155                         Path:   "/" + escape(key) + "/-",
156                         Method: "DELETE",
157                         Delay:  time.Duration(EXPIRATION_TIME) * time.Second,
158                 }
159                 taskqueue.Add(c, task, "")
160         }
161 }
162
163 func handleUpload(r *http.Request, p *multipart.Part) (fi *FileInfo) {
164         fi = &FileInfo{
165                 Name: p.FileName(),
166                 Type: p.Header.Get("Content-Type"),
167         }
168         if !fi.ValidateType() {
169                 return
170         }
171         defer func() {
172                 if rec := recover(); rec != nil {
173                         log.Println(rec)
174                         fi.Error = rec.(error).Error()
175                 }
176         }()
177         var b bytes.Buffer
178         lr := &io.LimitedReader{R: p, N: MAX_FILE_SIZE + 1}
179         context := appengine.NewContext(r)
180         w, err := blobstore.Create(context, fi.Type)
181         defer func() {
182                 w.Close()
183                 fi.Size = MAX_FILE_SIZE + 1 - lr.N
184                 fi.Key, err = w.Key()
185                 check(err)
186                 if !fi.ValidateSize() {
187                         err := blobstore.Delete(context, fi.Key)
188                         check(err)
189                         return
190                 }
191                 delayedDelete(context, fi)
192                 if b.Len() > 0 {
193                         fi.CreateThumbnail(&b, context)
194                 }
195                 fi.CreateUrls(r, context)
196         }()
197         check(err)
198         var wr io.Writer = w
199         if imageTypes.MatchString(fi.Type) {
200                 wr = io.MultiWriter(&b, w)
201         }
202         _, err = io.Copy(wr, lr)
203         return
204 }
205
206 func getFormValue(p *multipart.Part) string {
207         var b bytes.Buffer
208         io.CopyN(&b, p, int64(1<<20)) // Copy max: 1 MiB
209         return b.String()
210 }
211
212 func handleUploads(r *http.Request) (fileInfos []*FileInfo) {
213         fileInfos = make([]*FileInfo, 0)
214         mr, err := r.MultipartReader()
215         check(err)
216         r.Form, err = url.ParseQuery(r.URL.RawQuery)
217         check(err)
218         part, err := mr.NextPart()
219         for err == nil {
220                 if name := part.FormName(); name != "" {
221                         if part.FileName() != "" {
222                                 fileInfos = append(fileInfos, handleUpload(r, part))
223                         } else {
224                                 r.Form[name] = append(r.Form[name], getFormValue(part))
225                         }
226                 }
227                 part, err = mr.NextPart()
228         }
229         return
230 }
231
232 func get(w http.ResponseWriter, r *http.Request) {
233         if r.URL.Path == "/" {
234                 http.Redirect(w, r, WEBSITE, http.StatusFound)
235                 return
236         }
237         parts := strings.Split(r.URL.Path, "/")
238         if len(parts) == 3 {
239                 if key := parts[1]; key != "" {
240                         blobKey := appengine.BlobKey(key)
241                         bi, err := blobstore.Stat(appengine.NewContext(r), blobKey)
242                         if err == nil {
243                                 w.Header().Add(
244                                         "Cache-Control",
245                                         fmt.Sprintf("public,max-age=%d", EXPIRATION_TIME),
246                                 )
247                                 if imageTypes.MatchString(bi.ContentType) {
248                                         w.Header().Add("X-Content-Type-Options", "nosniff")
249                                 } else {
250                                         w.Header().Add("Content-Type", "application/octet-stream")
251                                         w.Header().Add(
252                                                 "Content-Disposition:",
253                                                 fmt.Sprintf("attachment; filename=%s;", parts[2]),
254                                         )
255                                 }
256                                 blobstore.Send(w, appengine.BlobKey(key))
257                                 return
258                         }
259                 }
260         }
261         http.Error(w, "404 Not Found", http.StatusNotFound)
262 }
263
264 func post(w http.ResponseWriter, r *http.Request) {
265         b, err := json.Marshal(handleUploads(r))
266         check(err)
267         if redirect := r.FormValue("redirect"); redirect != "" {
268                 http.Redirect(w, r, fmt.Sprintf(
269                         redirect,
270                         escape(string(b)),
271                 ), http.StatusFound)
272                 return
273         }
274         jsonType := "application/json"
275         if strings.Index(r.Header.Get("Accept"), jsonType) != -1 {
276                 w.Header().Set("Content-Type", jsonType)
277         }
278         fmt.Fprintln(w, string(b))
279 }
280
281 func delete(w http.ResponseWriter, r *http.Request) {
282         parts := strings.Split(r.URL.Path, "/")
283         if len(parts) != 3 {
284                 return
285         }
286         if key := parts[1]; key != "" {
287                 c := appengine.NewContext(r)
288                 blobstore.Delete(c, appengine.BlobKey(key))
289                 memcache.Delete(c, key)
290         }
291 }
292
293 func serveThumbnail(w http.ResponseWriter, r *http.Request) {
294         parts := strings.Split(r.URL.Path, "/")
295         if len(parts) == 3 {
296                 if key := parts[2]; key != "" {
297                         var data []byte
298                         c := appengine.NewContext(r)
299                         item, err := memcache.Get(c, key)
300                         if err == nil {
301                                 data = item.Value
302                         } else {
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),
308                                                 c,
309                                         )
310                                 }
311                         }
312                         if err == nil && len(data) > 3 {
313                                 w.Header().Add(
314                                         "Cache-Control",
315                                         fmt.Sprintf("public,max-age=%d", EXPIRATION_TIME),
316                                 )
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"
322                                 }
323                                 w.Header().Set("Content-Type", contentType)
324                                 fmt.Fprintln(w, string(data))
325                                 return
326                         }
327                 }
328         }
329         http.Error(w, "404 Not Found", http.StatusNotFound)
330 }
331
332 func handle(w http.ResponseWriter, r *http.Request) {
333         params, err := url.ParseQuery(r.URL.RawQuery)
334         check(err)
335         w.Header().Add("Access-Control-Allow-Origin", "*")
336         w.Header().Add(
337                 "Access-Control-Allow-Methods",
338                 "OPTIONS, HEAD, GET, POST, PUT, DELETE",
339         )
340         switch r.Method {
341         case "OPTIONS":
342         case "HEAD":
343         case "GET":
344                 get(w, r)
345         case "POST":
346                 if len(params["_method"]) > 0 && params["_method"][0] == "DELETE" {
347                         delete(w, r)
348                 } else {
349                         post(w, r)
350                 }
351         case "DELETE":
352                 delete(w, r)
353         default:
354                 http.Error(w, "501 Not Implemented", http.StatusNotImplemented)
355         }
356 }
357
358 func init() {
359         http.HandleFunc("/", handle)
360         http.HandleFunc("/thumbnails/", serveThumbnail)
361 }