1
0
Fork 0
eighty/serve/static_fasthttp.go

154 lines
4.3 KiB
Go

package serve
import (
"amuz.es/src/go/eighty"
"amuz.es/src/go/misc/strutil"
"bytes"
"crypto/subtle"
"github.com/valyala/fasthttp"
"io"
"log"
"mime"
"net/http"
"os"
"path"
"path/filepath"
"strconv"
"sync"
"time"
)
const cacheControlPrefix = "public, max-age="
type staticFileHandler struct {
contentPool sync.Pool
pathPrefix []byte
baseVersion int64
cacheFiller func(header *fasthttp.ResponseHeader, versionPrefix, discretePrefix []byte)
fileProvider FileProvider
}
// NewStaticFileHandler returns a static handler for fasthttp with advanced caching and url prefix stripping.
func NewStaticFileHandler(debug bool, urlPrefix, staticUrl string, startupTime time.Time, fileProvider FileProvider) fasthttp.RequestHandler {
h := staticFileHandler{
pathPrefix: []byte(path.Join(urlPrefix, staticUrl)),
baseVersion: startupTime.Unix(),
fileProvider: fileProvider,
}
h.contentPool.New = func() any { return [sniffLen]byte{} }
if debug {
h.cacheFiller = func(header *fasthttp.ResponseHeader, versionPrefix, discretePrefix []byte) {}
} else {
h.cacheFiller = func(header *fasthttp.ResponseHeader, versionPrefix, discretePrefix []byte) {
if receivedVersion, err := strconv.ParseInt(strutil.B2S(versionPrefix), 10, 64); err != nil || receivedVersion > 0 && receivedVersion < h.baseVersion {
return
}
var cacheDuration int64 = 2592000
if pushedDuration, err := strconv.ParseInt(strutil.B2S(discretePrefix), 10, 64); err == nil && pushedDuration > 0 && pushedDuration > cacheDuration {
cacheDuration = pushedDuration
}
//add header
header.Set(eighty.CacheControlHeader, cacheControlPrefix+strconv.FormatInt(cacheDuration, 10))
header.Set(eighty.ExpiresHeader, strconv.FormatInt(cacheDuration, 10))
}
}
return h.Handle
}
func (h *staticFileHandler) resolvFile(pathData []byte) (f File, stat os.FileInfo) {
var err error
// normalizePath
requestPath := strutil.B2S(bytes.TrimPrefix(pathData, h.pathPrefix))
strippedPath := path.Clean(requestPath)
if f, err = h.fileProvider(strippedPath); err != nil || f == nil {
panic(eighty.HandledErrorNotFound)
}
// resolvFileInfo
if stat, err = f.Stat(); err != nil {
_, code := toHTTPError(err)
err, _ = eighty.HandledErrorCodeOf(code)
panic(err)
} else if stat.IsDir() {
panic(eighty.HandledErrorNotFound)
}
return
}
func (h *staticFileHandler) resolvContentType(name string, header *fasthttp.ResponseHeader, file File) {
if ctypes := header.ContentType(); len(ctypes) > 0 {
return
}
if ctype := mime.TypeByExtension(filepath.Ext(name)); len(ctype) > 0 {
header.SetContentType(ctype)
return
}
buf := h.contentPool.Get().([sniffLen]byte)
defer h.contentPool.Put(buf)
// read a chunk to decide between utf-8 text and binary
if n, _ := file.Read(buf[:]); n > 0 {
// rewind to output whole file
if _, seekErr := file.Seek(0, io.SeekStart); seekErr != nil {
log.Printf("file %s seeker can't seek", name)
panic(http.StatusInternalServerError)
}
header.SetContentType(http.DetectContentType(buf[:n]))
} else {
header.SetContentType("application/octet-stream")
}
}
// Handle is a handler method for http request.
func (h *staticFileHandler) Handle(ctx *fasthttp.RequestCtx) {
var (
// step 1 stripping url
// step 2 openfile
f, stat = h.resolvFile(ctx.Path())
queryArgs = ctx.QueryArgs()
modTime = stat.ModTime()
name = f.Name()
respHdr = &ctx.Response.Header
etag []byte
)
// step 3 cache control
h.cacheFiller(respHdr, queryArgs.Peek("v"), queryArgs.Peek("d"))
respHdr.Set(eighty.VaryHeader, eighty.UserAgentHeader)
// step 4 serve file
if !isZeroTime(modTime) {
respHdr.SetLastModified(modTime)
}
etag = f.Hash()
if len(etag) > 0 {
respHdr.SetBytesV(eighty.EtagHeader, etag)
}
// serveContent will check modification time
if checkPreconditions(&ctx.Request, &ctx.Response, modTime, etag) {
// not modified
return
}
// step 5 resolve content type
h.resolvContentType(name, respHdr, f)
if size := stat.Size(); size <= 0 {
ctx.Response.SkipBody = true
ctx.SetStatusCode(http.StatusNoContent)
return
} else if subtle.ConstantTimeCompare(ctx.Method(), eighty.MethodHEAD) == 1 {
ctx.Response.SkipBody = true
ctx.SetStatusCode(http.StatusOK)
} else {
ctx.SetBodyStream(f, int(size))
ctx.SetStatusCode(http.StatusOK)
}
}