157 lines
3.4 KiB
Go
157 lines
3.4 KiB
Go
package routes
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"encoding/base64"
|
|
"io"
|
|
"io/ioutil"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/gorilla/mux"
|
|
"github.com/h2non/filetype"
|
|
"sour.is/x/toolbox/httpsrv"
|
|
"sour.is/x/toolbox/log"
|
|
|
|
"sour.is/x/paste/src/pkg/readutil"
|
|
)
|
|
|
|
func init() {
|
|
a := &image{}
|
|
httpsrv.RegisterModule("image", a.config)
|
|
|
|
httpsrv.HttpRegister("image", httpsrv.HttpRoutes{
|
|
{Name: "getImage", Method: "GET", Pattern: "/i/{name}", HandlerFunc: a.get},
|
|
{Name: "putImage", Method: "PUT", Pattern: "/i", HandlerFunc: a.put},
|
|
{Name: "getStyle", Method: "GET", Pattern: "/{style:avatar|bg|cover}/", HandlerFunc: a.getStyle},
|
|
})
|
|
}
|
|
|
|
type image struct {
|
|
store string
|
|
maxSize int64
|
|
}
|
|
|
|
func (a *image) config(config map[string]string) {
|
|
a.store = "data/"
|
|
if config["store"] != "" {
|
|
a.store = config["store"]
|
|
}
|
|
|
|
if !chkStore(a.store) {
|
|
log.Criticalf("Image: store location [%s] does not exist or is not writable.", a.store)
|
|
}
|
|
log.Noticef("Image: store location set to [%s]", a.store)
|
|
|
|
a.maxSize = 10 * 1024 * 1024
|
|
if s, ok := config["max-size"]; ok {
|
|
if i, err := strconv.Atoi(s); err == nil {
|
|
a.maxSize = int64(i)
|
|
}
|
|
}
|
|
log.Noticef("Image: store max-size set to [%d]", a.maxSize)
|
|
}
|
|
|
|
func (a *image) get(w http.ResponseWriter, r *http.Request) {
|
|
vars := mux.Vars(r)
|
|
|
|
name := vars["name"]
|
|
|
|
ext := filepath.Ext(name)
|
|
id := strings.TrimSuffix(name, ext)
|
|
|
|
fname := filepath.Join(a.store, id)
|
|
|
|
if !chkFile(fname) {
|
|
httpsrv.WriteError(w, http.StatusNotFound, "Not Found")
|
|
|
|
return
|
|
}
|
|
|
|
if chkGone(fname) {
|
|
httpsrv.WriteError(w, http.StatusGone, "Gone")
|
|
|
|
return
|
|
}
|
|
|
|
f, err := os.Open(fname)
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
defer f.Close()
|
|
|
|
pr := readutil.NewPreviewReader(f)
|
|
|
|
mime, _ := readutil.ReadMIME(pr, name)
|
|
w.Header().Set("Content-Type", mime)
|
|
w.Header().Set("X-Content-Type-Options", "nosniff")
|
|
|
|
_, _ = io.Copy(w, pr.Drain())
|
|
}
|
|
|
|
func (a *image) put(w http.ResponseWriter, r *http.Request) {
|
|
defer r.Body.Close()
|
|
|
|
if h := r.Header.Get("Content-Length"); h != "" {
|
|
log.Debugs("Artifact:", "content-length", h, "max-length", a.maxSize)
|
|
if i, err := strconv.Atoi(h); err == nil {
|
|
if int64(i) > a.maxSize {
|
|
log.Error("ERR Size Too Large")
|
|
http.Error(w, "ERR Size Too Large", http.StatusRequestEntityTooLarge)
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
rdr := io.LimitReader(r.Body, a.maxSize)
|
|
pr := readutil.NewPreviewReader(rdr)
|
|
if !isImageOrVideo(pr) {
|
|
httpsrv.WriteError(w, http.StatusUnsupportedMediaType, "ERR Not Image")
|
|
return
|
|
}
|
|
rdr = pr.Drain()
|
|
|
|
s256 := sha256.New()
|
|
tmp, err := ioutil.TempFile(a.store, "image-")
|
|
if err != nil {
|
|
httpsrv.WriteError(w, 400, err.Error())
|
|
return
|
|
}
|
|
defer os.Remove(tmp.Name())
|
|
|
|
m := io.MultiWriter(s256, tmp)
|
|
if _, err = io.Copy(m, rdr); err != nil {
|
|
httpsrv.WriteError(w, 400, err.Error())
|
|
return
|
|
}
|
|
tmp.Close()
|
|
|
|
id := base64.RawURLEncoding.EncodeToString(s256.Sum(nil)[12:])
|
|
fname := filepath.Join(a.store, id)
|
|
|
|
log.Debugs("Image: moving file", "src", tmp.Name(), "dst", fname)
|
|
err = os.Rename(tmp.Name(), fname)
|
|
if err != nil {
|
|
httpsrv.WriteError(w, 400, err.Error())
|
|
return
|
|
}
|
|
|
|
httpsrv.WriteText(w, http.StatusCreated, id)
|
|
}
|
|
|
|
func isImageOrVideo(in io.Reader) bool {
|
|
buf := make([]byte, 320)
|
|
_, err := in.Read(buf)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
return filetype.IsImage(buf) || filetype.IsVideo(buf)
|
|
}
|
|
|
|
func (a *image) getStyle(w http.ResponseWriter, r *http.Request) {
|
|
|
|
}
|