A web blog and personal homepage engine written in Go.
Não pode escolher mais do que 25 tópicos Os tópicos devem começar com uma letra ou um número, podem incluir traços ('-') e podem ter até 35 caracteres.

196 linhas
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. }