main.go 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361
  1. /*
  2. * jQuery File Upload Plugin GAE Go Example
  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. * https://opensource.org/licenses/MIT
  10. */
  11. package app
  12. import (
  13. "bufio"
  14. "bytes"
  15. "encoding/json"
  16. "fmt"
  17. "github.com/disintegration/gift"
  18. "golang.org/x/net/context"
  19. "google.golang.org/appengine"
  20. "google.golang.org/appengine/memcache"
  21. "hash/crc32"
  22. "image"
  23. "image/gif"
  24. "image/jpeg"
  25. "image/png"
  26. "io"
  27. "log"
  28. "mime/multipart"
  29. "net/http"
  30. "net/url"
  31. "path/filepath"
  32. "regexp"
  33. "strings"
  34. )
  35. const (
  36. WEBSITE = "https://blueimp.github.io/jQuery-File-Upload/"
  37. MIN_FILE_SIZE = 1 // bytes
  38. // Max file size is memcache limit (1MB) minus key size minus overhead:
  39. MAX_FILE_SIZE = 999000 // bytes
  40. IMAGE_TYPES = "image/(gif|p?jpeg|(x-)?png)"
  41. ACCEPT_FILE_TYPES = IMAGE_TYPES
  42. THUMB_MAX_WIDTH = 80
  43. THUMB_MAX_HEIGHT = 80
  44. EXPIRATION_TIME = 300 // seconds
  45. // If empty, only allow redirects to the referer protocol+host.
  46. // Set to a regexp string for custom pattern matching:
  47. REDIRECT_ALLOW_TARGET = ""
  48. )
  49. var (
  50. imageTypes = regexp.MustCompile(IMAGE_TYPES)
  51. acceptFileTypes = regexp.MustCompile(ACCEPT_FILE_TYPES)
  52. thumbSuffix = "." + fmt.Sprint(THUMB_MAX_WIDTH) + "x" +
  53. fmt.Sprint(THUMB_MAX_HEIGHT)
  54. )
  55. func escape(s string) string {
  56. return strings.Replace(url.QueryEscape(s), "+", "%20", -1)
  57. }
  58. func extractKey(r *http.Request) string {
  59. // Use RequestURI instead of r.URL.Path, as we need the encoded form:
  60. path := strings.Split(r.RequestURI, "?")[0]
  61. // Also adjust double encoded slashes:
  62. return strings.Replace(path[1:], "%252F", "%2F", -1)
  63. }
  64. func check(err error) {
  65. if err != nil {
  66. panic(err)
  67. }
  68. }
  69. type FileInfo struct {
  70. Key string `json:"-"`
  71. ThumbnailKey string `json:"-"`
  72. Url string `json:"url,omitempty"`
  73. ThumbnailUrl string `json:"thumbnailUrl,omitempty"`
  74. Name string `json:"name"`
  75. Type string `json:"type"`
  76. Size int64 `json:"size"`
  77. Error string `json:"error,omitempty"`
  78. DeleteUrl string `json:"deleteUrl,omitempty"`
  79. DeleteType string `json:"deleteType,omitempty"`
  80. }
  81. func (fi *FileInfo) ValidateType() (valid bool) {
  82. if acceptFileTypes.MatchString(fi.Type) {
  83. return true
  84. }
  85. fi.Error = "Filetype not allowed"
  86. return false
  87. }
  88. func (fi *FileInfo) ValidateSize() (valid bool) {
  89. if fi.Size < MIN_FILE_SIZE {
  90. fi.Error = "File is too small"
  91. } else if fi.Size > MAX_FILE_SIZE {
  92. fi.Error = "File is too big"
  93. } else {
  94. return true
  95. }
  96. return false
  97. }
  98. func (fi *FileInfo) CreateUrls(r *http.Request, c context.Context) {
  99. u := &url.URL{
  100. Scheme: r.URL.Scheme,
  101. Host: appengine.DefaultVersionHostname(c),
  102. Path: "/",
  103. }
  104. uString := u.String()
  105. fi.Url = uString + fi.Key
  106. fi.DeleteUrl = fi.Url
  107. fi.DeleteType = "DELETE"
  108. if fi.ThumbnailKey != "" {
  109. fi.ThumbnailUrl = uString + fi.ThumbnailKey
  110. }
  111. }
  112. func (fi *FileInfo) SetKey(checksum uint32) {
  113. fi.Key = escape(string(fi.Type)) + "/" +
  114. escape(fmt.Sprint(checksum)) + "/" +
  115. escape(string(fi.Name))
  116. }
  117. func (fi *FileInfo) createThumb(buffer *bytes.Buffer, c context.Context) {
  118. if imageTypes.MatchString(fi.Type) {
  119. src, _, err := image.Decode(bytes.NewReader(buffer.Bytes()))
  120. check(err)
  121. filter := gift.New(gift.ResizeToFit(
  122. THUMB_MAX_WIDTH,
  123. THUMB_MAX_HEIGHT,
  124. gift.LanczosResampling,
  125. ))
  126. dst := image.NewNRGBA(filter.Bounds(src.Bounds()))
  127. filter.Draw(dst, src)
  128. buffer.Reset()
  129. bWriter := bufio.NewWriter(buffer)
  130. switch fi.Type {
  131. case "image/jpeg", "image/pjpeg":
  132. err = jpeg.Encode(bWriter, dst, nil)
  133. case "image/gif":
  134. err = gif.Encode(bWriter, dst, nil)
  135. default:
  136. err = png.Encode(bWriter, dst)
  137. }
  138. check(err)
  139. bWriter.Flush()
  140. thumbnailKey := fi.Key + thumbSuffix + filepath.Ext(fi.Name)
  141. item := &memcache.Item{
  142. Key: thumbnailKey,
  143. Value: buffer.Bytes(),
  144. }
  145. err = memcache.Set(c, item)
  146. check(err)
  147. fi.ThumbnailKey = thumbnailKey
  148. }
  149. }
  150. func handleUpload(r *http.Request, p *multipart.Part) (fi *FileInfo) {
  151. fi = &FileInfo{
  152. Name: p.FileName(),
  153. Type: p.Header.Get("Content-Type"),
  154. }
  155. if !fi.ValidateType() {
  156. return
  157. }
  158. defer func() {
  159. if rec := recover(); rec != nil {
  160. log.Println(rec)
  161. fi.Error = rec.(error).Error()
  162. }
  163. }()
  164. var buffer bytes.Buffer
  165. hash := crc32.NewIEEE()
  166. mw := io.MultiWriter(&buffer, hash)
  167. lr := &io.LimitedReader{R: p, N: MAX_FILE_SIZE + 1}
  168. _, err := io.Copy(mw, lr)
  169. check(err)
  170. fi.Size = MAX_FILE_SIZE + 1 - lr.N
  171. if !fi.ValidateSize() {
  172. return
  173. }
  174. fi.SetKey(hash.Sum32())
  175. item := &memcache.Item{
  176. Key: fi.Key,
  177. Value: buffer.Bytes(),
  178. }
  179. context := appengine.NewContext(r)
  180. err = memcache.Set(context, item)
  181. check(err)
  182. fi.createThumb(&buffer, context)
  183. fi.CreateUrls(r, context)
  184. return
  185. }
  186. func getFormValue(p *multipart.Part) string {
  187. var b bytes.Buffer
  188. io.CopyN(&b, p, int64(1<<20)) // Copy max: 1 MiB
  189. return b.String()
  190. }
  191. func handleUploads(r *http.Request) (fileInfos []*FileInfo) {
  192. fileInfos = make([]*FileInfo, 0)
  193. mr, err := r.MultipartReader()
  194. check(err)
  195. r.Form, err = url.ParseQuery(r.URL.RawQuery)
  196. check(err)
  197. part, err := mr.NextPart()
  198. for err == nil {
  199. if name := part.FormName(); name != "" {
  200. if part.FileName() != "" {
  201. fileInfos = append(fileInfos, handleUpload(r, part))
  202. } else {
  203. r.Form[name] = append(r.Form[name], getFormValue(part))
  204. }
  205. }
  206. part, err = mr.NextPart()
  207. }
  208. return
  209. }
  210. func validateRedirect(r *http.Request, redirect string) bool {
  211. if redirect != "" {
  212. var redirectAllowTarget *regexp.Regexp
  213. if REDIRECT_ALLOW_TARGET != "" {
  214. redirectAllowTarget = regexp.MustCompile(REDIRECT_ALLOW_TARGET)
  215. } else {
  216. referer := r.Referer()
  217. if referer == "" {
  218. return false
  219. }
  220. refererUrl, err := url.Parse(referer)
  221. if err != nil {
  222. return false
  223. }
  224. redirectAllowTarget = regexp.MustCompile("^" + regexp.QuoteMeta(
  225. refererUrl.Scheme+"://"+refererUrl.Host+"/",
  226. ))
  227. }
  228. return redirectAllowTarget.MatchString(redirect)
  229. }
  230. return false
  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. // Use RequestURI instead of r.URL.Path, as we need the encoded form:
  238. key := extractKey(r)
  239. parts := strings.Split(key, "/")
  240. if len(parts) == 3 {
  241. context := appengine.NewContext(r)
  242. item, err := memcache.Get(context, key)
  243. if err == nil {
  244. w.Header().Add("X-Content-Type-Options", "nosniff")
  245. contentType, _ := url.QueryUnescape(parts[0])
  246. if !imageTypes.MatchString(contentType) {
  247. contentType = "application/octet-stream"
  248. }
  249. w.Header().Add("Content-Type", contentType)
  250. w.Header().Add(
  251. "Cache-Control",
  252. fmt.Sprintf("public,max-age=%d", EXPIRATION_TIME),
  253. )
  254. w.Write(item.Value)
  255. return
  256. }
  257. }
  258. http.Error(w, "404 Not Found", http.StatusNotFound)
  259. }
  260. func post(w http.ResponseWriter, r *http.Request) {
  261. result := make(map[string][]*FileInfo, 1)
  262. result["files"] = handleUploads(r)
  263. b, err := json.Marshal(result)
  264. check(err)
  265. if redirect := r.FormValue("redirect"); validateRedirect(r, redirect) {
  266. if strings.Contains(redirect, "%s") {
  267. redirect = fmt.Sprintf(
  268. redirect,
  269. escape(string(b)),
  270. )
  271. }
  272. http.Redirect(w, r, redirect, http.StatusFound)
  273. return
  274. }
  275. w.Header().Set("Cache-Control", "no-cache")
  276. jsonType := "application/json"
  277. if strings.Index(r.Header.Get("Accept"), jsonType) != -1 {
  278. w.Header().Set("Content-Type", jsonType)
  279. }
  280. fmt.Fprintln(w, string(b))
  281. }
  282. func delete(w http.ResponseWriter, r *http.Request) {
  283. key := extractKey(r)
  284. parts := strings.Split(key, "/")
  285. if len(parts) == 3 {
  286. result := make(map[string]bool, 1)
  287. context := appengine.NewContext(r)
  288. err := memcache.Delete(context, key)
  289. if err == nil {
  290. result[key] = true
  291. contentType, _ := url.QueryUnescape(parts[0])
  292. if imageTypes.MatchString(contentType) {
  293. thumbnailKey := key + thumbSuffix + filepath.Ext(parts[2])
  294. err := memcache.Delete(context, thumbnailKey)
  295. if err == nil {
  296. result[thumbnailKey] = true
  297. }
  298. }
  299. }
  300. w.Header().Set("Content-Type", "application/json")
  301. b, err := json.Marshal(result)
  302. check(err)
  303. fmt.Fprintln(w, string(b))
  304. } else {
  305. http.Error(w, "405 Method not allowed", http.StatusMethodNotAllowed)
  306. }
  307. }
  308. func handle(w http.ResponseWriter, r *http.Request) {
  309. params, err := url.ParseQuery(r.URL.RawQuery)
  310. check(err)
  311. w.Header().Add("Access-Control-Allow-Origin", "*")
  312. w.Header().Add(
  313. "Access-Control-Allow-Methods",
  314. "OPTIONS, HEAD, GET, POST, DELETE",
  315. )
  316. w.Header().Add(
  317. "Access-Control-Allow-Headers",
  318. "Content-Type, Content-Range, Content-Disposition",
  319. )
  320. switch r.Method {
  321. case "OPTIONS", "HEAD":
  322. return
  323. case "GET":
  324. get(w, r)
  325. case "POST":
  326. if len(params["_method"]) > 0 && params["_method"][0] == "DELETE" {
  327. delete(w, r)
  328. } else {
  329. post(w, r)
  330. }
  331. case "DELETE":
  332. delete(w, r)
  333. default:
  334. http.Error(w, "501 Not Implemented", http.StatusNotImplemented)
  335. }
  336. }
  337. func init() {
  338. http.HandleFunc("/", handle)
  339. }