A web blog and personal homepage engine written in Go.
Nie możesz wybrać więcej, niż 25 tematów Tematy muszą się zaczynać od litery lub cyfry, mogą zawierać myślniki ('-') i mogą mieć do 35 znaków.
 
 

196 wiersze
4.7 KiB

  1. package controllers
  2. import (
  3. "bytes"
  4. "fmt"
  5. "image"
  6. "image/gif"
  7. "image/jpeg"
  8. "image/png"
  9. "io"
  10. "net/http"
  11. "os"
  12. "path/filepath"
  13. "strings"
  14. "git.kirsle.net/apps/gophertype/pkg/console"
  15. "git.kirsle.net/apps/gophertype/pkg/glue"
  16. "git.kirsle.net/apps/gophertype/pkg/responses"
  17. "git.kirsle.net/apps/gophertype/pkg/settings"
  18. "github.com/edwvee/exiffix"
  19. "github.com/nfnt/resize"
  20. )
  21. // TODO: configurable max image width.
  22. var (
  23. MaxImageWidth = 1280
  24. JpegQuality = 90
  25. // images folder for upload, relative to web root.
  26. ImagePath = filepath.Join("static", "photos")
  27. )
  28. func init() {
  29. glue.Register(glue.Endpoint{
  30. Path: "/admin/upload",
  31. Methods: []string{"GET", "POST"},
  32. Handler: UploadHandler,
  33. })
  34. }
  35. // UploadHandler handles quick file uploads from the front-end for logged-in users.
  36. func UploadHandler(w http.ResponseWriter, r *http.Request) {
  37. // Parameters.
  38. var (
  39. filetype = r.FormValue("type") // image only for now
  40. filename = r.FormValue("filename")
  41. )
  42. var buf bytes.Buffer
  43. type response struct {
  44. Success bool `json:"success"`
  45. Error string `json:"error,omitempty"`
  46. Filename string `json:"filename,omitempty"`
  47. URI string `json:"uri,omitempty"`
  48. Checksum string `json:"checksum,omitempty"`
  49. }
  50. // Validate the upload type.
  51. if filetype != "image" {
  52. responses.JSON(w, http.StatusBadRequest, response{
  53. Error: "Only 'image' type uploads supported for now.",
  54. })
  55. return
  56. }
  57. // Get the file from the form data.
  58. file, header, err := r.FormFile("file")
  59. if err != nil {
  60. responses.JSON(w, http.StatusBadRequest, response{
  61. Error: err.Error(),
  62. })
  63. return
  64. }
  65. defer file.Close()
  66. // Validate the extension is an image type.
  67. ext := strings.ToLower(filepath.Ext(header.Filename))
  68. if ext != ".jpg" && ext != ".jpeg" && ext != ".png" && ext != ".gif" {
  69. responses.JSON(w, http.StatusBadRequest, response{
  70. Error: "Invalid file type, only common image types are supported: jpg, png, gif",
  71. })
  72. return
  73. }
  74. // Default filename?
  75. if filename == "" {
  76. filename = filepath.Base(header.Filename)
  77. }
  78. // Read the file.
  79. io.Copy(&buf, file)
  80. binary := buf.Bytes()
  81. // Process and image and resize it down, strip metadata, etc.
  82. binary, err = processImage(binary, ext)
  83. if err != nil {
  84. responses.JSON(w, http.StatusBadRequest, response{
  85. Error: "Resize error: " + err.Error(),
  86. })
  87. }
  88. console.Info("Uploaded file named: %s name=%s", header.Filename, filename)
  89. // Write to the /static/photos directory of the user root. Ensure the path
  90. // exists or create it if not.
  91. outputPath := filepath.Join(settings.UserRoot, ImagePath)
  92. if _, err := os.Stat(outputPath); os.IsNotExist(err) {
  93. os.MkdirAll(outputPath, 0755)
  94. }
  95. // Ensure the filename is unique.
  96. filename = uniqueFilename(filepath.Join(settings.UserRoot, ImagePath), filename)
  97. // Write the output file.
  98. console.Info("Uploaded image: %s", filename)
  99. outfh, err := os.Create(filename)
  100. if err != nil {
  101. responses.JSON(w, http.StatusBadRequest, response{
  102. Error: err.Error(),
  103. })
  104. return
  105. }
  106. defer outfh.Close()
  107. outfh.Write(binary)
  108. responses.JSON(w, http.StatusOK, response{
  109. Success: true,
  110. Filename: header.Filename,
  111. URI: fmt.Sprintf("/static/photos/%s", filepath.Base(filename)),
  112. })
  113. }
  114. // processImage manhandles an image's binary data, scaling it down to <= 1280
  115. // pixels and stripping off any metadata.
  116. func processImage(input []byte, ext string) ([]byte, error) {
  117. if ext == ".gif" {
  118. return input, nil
  119. }
  120. reader := bytes.NewReader(input)
  121. // Decode the image using exiffix, which will auto-rotate jpeg images etc.
  122. // based on their EXIF values.
  123. origImage, _, err := exiffix.Decode(reader)
  124. if err != nil {
  125. return input, err
  126. }
  127. // Read the config to get the image width.
  128. reader.Seek(0, io.SeekStart)
  129. config, _, _ := image.DecodeConfig(reader)
  130. width := config.Width
  131. // If the width is too great, scale it down.
  132. if width > MaxImageWidth {
  133. width = MaxImageWidth
  134. }
  135. newImage := resize.Resize(uint(width), 0, origImage, resize.Lanczos3)
  136. var output bytes.Buffer
  137. switch ext {
  138. case ".jpeg":
  139. fallthrough
  140. case ".jpg":
  141. jpeg.Encode(&output, newImage, &jpeg.Options{
  142. Quality: JpegQuality,
  143. })
  144. case ".png":
  145. png.Encode(&output, newImage)
  146. case ".gif":
  147. gif.Encode(&output, newImage, nil)
  148. }
  149. return output.Bytes(), nil
  150. }
  151. // uniqueFilename gets a filename in a folder that doesn't already exist.
  152. // Returns the file path with unique filename included.
  153. func uniqueFilename(path string, filename string) string {
  154. ext := filepath.Ext(filename)
  155. basename := strings.TrimSuffix(filename, ext)
  156. // Try files.
  157. var i = 1
  158. for {
  159. if _, err := os.Stat(filepath.Join(path, filename)); !os.IsNotExist(err) {
  160. filename = fmt.Sprintf("%s~%d%s", basename, i, ext)
  161. i++
  162. continue
  163. }
  164. break
  165. }
  166. return filepath.Join(path, filename)
  167. }