1
0
Fork 0
A web blog and personal homepage engine written in Go.
Du kannst nicht mehr als 25 Themen auswählen Themen müssen entweder mit einem Buchstaben oder einer Ziffer beginnen. Sie können Bindestriche („-“) enthalten und bis zu 35 Zeichen lang sein.
 
 

195 Zeilen
4.7 KiB

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)
}