package controllers import ( "bytes" "fmt" "image" "image/gif" "image/jpeg" "image/png" "io" "net/http" "os" "path/filepath" "strings" "git.kirsle.net/apps/gophertype/pkg/console" "git.kirsle.net/apps/gophertype/pkg/glue" "git.kirsle.net/apps/gophertype/pkg/responses" "git.kirsle.net/apps/gophertype/pkg/settings" "github.com/edwvee/exiffix" "github.com/nfnt/resize" ) // TODO: configurable max image width. var ( MaxImageWidth = 1280 JpegQuality = 90 // images folder for upload, relative to web root. ImagePath = filepath.Join("static", "photos") ) func init() { glue.Register(glue.Endpoint{ Path: "/admin/upload", Methods: []string{"GET", "POST"}, Handler: UploadHandler, }) } // UploadHandler handles quick file uploads from the front-end for logged-in users. func UploadHandler(w http.ResponseWriter, r *http.Request) { // Parameters. var ( filetype = r.FormValue("type") // image only for now filename = r.FormValue("filename") ) var buf bytes.Buffer type response struct { Success bool `json:"success"` Error string `json:"error,omitempty"` Filename string `json:"filename,omitempty"` URI string `json:"uri,omitempty"` Checksum string `json:"checksum,omitempty"` } // Validate the upload type. if filetype != "image" { responses.JSON(w, http.StatusBadRequest, response{ Error: "Only 'image' type uploads supported for now.", }) return } // Get the file from the form data. file, header, err := r.FormFile("file") if err != nil { responses.JSON(w, http.StatusBadRequest, response{ Error: err.Error(), }) return } defer file.Close() // Validate the extension is an image type. ext := strings.ToLower(filepath.Ext(header.Filename)) if ext != ".jpg" && ext != ".jpeg" && ext != ".png" && ext != ".gif" { responses.JSON(w, http.StatusBadRequest, response{ Error: "Invalid file type, only common image types are supported: jpg, png, gif", }) return } // Default filename? if filename == "" { filename = filepath.Base(header.Filename) } // Read the file. io.Copy(&buf, file) binary := buf.Bytes() // Process and image and resize it down, strip metadata, etc. binary, err = processImage(binary, ext) if err != nil { responses.JSON(w, http.StatusBadRequest, response{ Error: "Resize error: " + err.Error(), }) } console.Info("Uploaded file named: %s name=%s", header.Filename, filename) // Write to the /static/photos directory of the user root. Ensure the path // exists or create it if not. outputPath := filepath.Join(settings.UserRoot, ImagePath) if _, err := os.Stat(outputPath); os.IsNotExist(err) { os.MkdirAll(outputPath, 0755) } // Ensure the filename is unique. filename = uniqueFilename(filepath.Join(settings.UserRoot, ImagePath), filename) // Write the output file. console.Info("Uploaded image: %s", filename) outfh, err := os.Create(filename) if err != nil { responses.JSON(w, http.StatusBadRequest, response{ Error: err.Error(), }) return } defer outfh.Close() outfh.Write(binary) responses.JSON(w, http.StatusOK, response{ Success: true, Filename: header.Filename, URI: fmt.Sprintf("/static/photos/%s", filepath.Base(filename)), }) } // processImage manhandles an image's binary data, scaling it down to <= 1280 // pixels and stripping off any metadata. func processImage(input []byte, ext string) ([]byte, error) { if ext == ".gif" { return input, nil } reader := bytes.NewReader(input) // Decode the image using exiffix, which will auto-rotate jpeg images etc. // based on their EXIF values. origImage, _, err := exiffix.Decode(reader) if err != nil { return input, err } // Read the config to get the image width. reader.Seek(0, io.SeekStart) config, _, _ := image.DecodeConfig(reader) width := config.Width // If the width is too great, scale it down. if width > MaxImageWidth { width = MaxImageWidth } newImage := resize.Resize(uint(width), 0, origImage, resize.Lanczos3) var output bytes.Buffer switch ext { case ".jpeg": fallthrough case ".jpg": jpeg.Encode(&output, newImage, &jpeg.Options{ Quality: JpegQuality, }) case ".png": png.Encode(&output, newImage) case ".gif": gif.Encode(&output, newImage, nil) } return output.Bytes(), nil } // uniqueFilename gets a filename in a folder that doesn't already exist. // Returns the file path with unique filename included. func uniqueFilename(path string, filename string) string { ext := filepath.Ext(filename) basename := strings.TrimSuffix(filename, ext) // Try files. var i = 1 for { if _, err := os.Stat(filepath.Join(path, filename)); !os.IsNotExist(err) { filename = fmt.Sprintf("%s~%d%s", basename, i, ext) i++ continue } break } return filepath.Join(path, filename) }