parent
91e3bdaa53
commit
c1995efb7a
9 changed files with 304 additions and 8 deletions
@ -1,4 +1,5 @@ |
||||
bin/ |
||||
pvt-www/static/photos/ |
||||
pkg/bundled/ |
||||
public_html/.settings.json |
||||
*.sqlite |
||||
|
@ -0,0 +1,192 @@ |
||||
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" |
||||
"github.com/edwvee/exiffix" |
||||
"github.com/nfnt/resize" |
||||
) |
||||
|
||||
// TODO: configurable max image width.
|
||||
var ( |
||||
MaxImageWidth = 1280 |
||||
JpegQuality = 90 |
||||
ImagePath = "static/photos" // images folder for upload, relative to web root.
|
||||
) |
||||
|
||||
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. TODO
|
||||
outputPath := filepath.Join("./pvt-www", "static", "photos") |
||||
if _, err := os.Stat(outputPath); os.IsNotExist(err) { |
||||
os.MkdirAll(outputPath, 0755) |
||||
} |
||||
|
||||
// Ensure the filename is unique.
|
||||
filename = uniqueFilename("./pvt-www/static/photos", 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) |
||||
} |
@ -0,0 +1,15 @@ |
||||
package responses |
||||
|
||||
import ( |
||||
"encoding/json" |
||||
"net/http" |
||||
) |
||||
|
||||
// JSON sends a JSON payload as response.
|
||||
func JSON(w http.ResponseWriter, statusCode int, v interface{}) { |
||||
w.Header().Set("Content-Type", "application/json") |
||||
w.WriteHeader(statusCode) |
||||
|
||||
enc := json.NewEncoder(w) |
||||
enc.Encode(v) |
||||
} |
Loading…
Reference in new issue