Noah
383b5d7591
* Add the Age Gate middleware for NSFW sites. * Cache thumbnail images from blog entries. * Implement the user-root properly for loading web assets.
196 lines
4.7 KiB
Go
196 lines
4.7 KiB
Go
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)
|
|
}
|