239 lines
7.5 KiB
Go
239 lines
7.5 KiB
Go
package servlet
|
|
|
|
import (
|
|
"errors"
|
|
util "geniuscartel.xyz/vinegar/vinegarUtil"
|
|
"net/http"
|
|
"path"
|
|
"path/filepath"
|
|
"strings"
|
|
)
|
|
|
|
type (
|
|
// FileRoute implements a static file serving route.
|
|
// It serves files from a given file path.
|
|
FileRoute struct {
|
|
|
|
// VinegarRoute is the base route containing the URL pattern and handler.
|
|
VinegarRoute *VinegarRoute
|
|
|
|
// srv is the VinegarServlet instance that this route is attached to.
|
|
srv *VinegarServlet
|
|
|
|
// fileRoot is the base file path to serve files from.
|
|
fileRoot string
|
|
|
|
// UseCache indicates whether to use caching for this route.
|
|
UseCache bool
|
|
}
|
|
|
|
//RouteConstructor
|
|
//
|
|
//Params:
|
|
//
|
|
//servlet - The VinegarServlet instance to add the route to
|
|
//
|
|
//urlPattern - The URL regex pattern for route to match
|
|
//
|
|
//pathlike - The file path to serve
|
|
//
|
|
//useCache - Whether to use caching for this route
|
|
//
|
|
// A RouteConstructor is a function that accepts a VinegarServlet, urlPattern, file path, and cache option. It uses
|
|
// these to construct and return a FileRoute.
|
|
// The return value is a FileRoute that will serve the files from the given path.
|
|
//
|
|
// This function signature allows encapsulating the creation of different types of FileRoutes. It is used to define
|
|
// constructor functions for each file type, like NewTextRoute or NewImageRoute.
|
|
RouteConstructor func(servlet *VinegarServlet, urlPattern string, pathlike string, useCache bool) *FileRoute
|
|
)
|
|
|
|
// NewTextRoute creates a new FileRoute for serving text files.
|
|
//
|
|
// It handles text files as compressible content, gzipping them when the client sends the Accept-Encoding: gzip header.
|
|
//
|
|
// Parameters:
|
|
//
|
|
// servlet - The VinegarServlet instance to attach the route to.
|
|
//
|
|
// urlPattern - The URL regex pattern that triggers this route.
|
|
//
|
|
// pathlike - The file path on disk to serve files from.
|
|
//
|
|
// useCache - Whether to use caching for this route.
|
|
//
|
|
// Returns:
|
|
//
|
|
// A FileRoute instance configured for serving text files, added to
|
|
// the provided VinegarServlet.
|
|
var NewTextRoute RouteConstructor = func(servlet *VinegarServlet, urlPattern string, pathlike string, useCache bool) *FileRoute {
|
|
defaultPrune := strings.Replace(urlPattern, ".*", "", -1)
|
|
route := FileRoute{srv: servlet, fileRoot: pathlike, UseCache: useCache}
|
|
textRouteHandler := createCompressibleFileServletFunction(&route, defaultPrune, pathlike)
|
|
rootRoute := NewServletRoute(urlPattern, textRouteHandler) //i *still* kinda don't like this pattern
|
|
route.VinegarRoute = rootRoute
|
|
|
|
servlet.AddRoute(route.VinegarRoute)
|
|
|
|
return &route
|
|
}
|
|
|
|
var NewImageRoute RouteConstructor = func(servlet *VinegarServlet, urlPattern string, pathlike string, useCache bool) *FileRoute {
|
|
defaultPrune := strings.Replace(urlPattern, ".*", "", -1)
|
|
route := FileRoute{srv: servlet, fileRoot: pathlike, UseCache: useCache}
|
|
rootRoute := NewServletRoute(urlPattern, createUncompressedFileServletFunction(&route, defaultPrune, pathlike))
|
|
route.VinegarRoute = rootRoute //i *kinda* don't like this pattern
|
|
|
|
servlet.AddRoute(route.VinegarRoute)
|
|
return &route
|
|
}
|
|
|
|
var NewSingleFileRoute RouteConstructor = func(servlet *VinegarServlet, urlPattern string, pathlike string, useCache bool) *FileRoute {
|
|
route := FileRoute{
|
|
srv: servlet,
|
|
fileRoot: pathlike,
|
|
UseCache: useCache,
|
|
}
|
|
singleFileServletHandler := createSingleFileServletFunction(&route)
|
|
sfCache := util.NewSingleFileCache(pathlike)
|
|
|
|
parentRoute := NewServletRoute(urlPattern, singleFileServletHandler)
|
|
parentRoute.Handler = singleFileServletHandler
|
|
parentRoute.Cache = sfCache
|
|
|
|
route.VinegarRoute = parentRoute
|
|
|
|
servlet.AddRoute(route.VinegarRoute)
|
|
|
|
return &route
|
|
}
|
|
|
|
// createSingleFileServletFunction creates a handler function for serving a single file.
|
|
//
|
|
// It looks up the file content from the route's cache and writes the appropriate
|
|
// headers and file content to the response.
|
|
//
|
|
// Parameters:
|
|
//
|
|
// route - The FileRoute instance this handler is attached to.
|
|
//
|
|
// Returns:
|
|
//
|
|
// A VinegarHandlerFunction that serves the single file for the provided route.
|
|
//
|
|
// The handler function checks the route's cache for the file content.
|
|
// If caching is enabled, it first checks the cache directly.
|
|
// If caching is disabled, it calls GetFresh to avoid the cache.
|
|
//
|
|
// If the content is not found, it returns a 404 error.
|
|
//
|
|
// Otherwise, it sets the Content-Type header based on the cache's MIME type,
|
|
// and writes the file content to the response.
|
|
//
|
|
// If the client accepts gzip encoding, it compresses the content before writing.
|
|
|
|
func createSingleFileServletFunction(route *FileRoute) VinegarHandlerFunction {
|
|
|
|
var fun VinegarHandlerFunction = func(w http.ResponseWriter, req *http.Request) {
|
|
var cache *util.LruEntry
|
|
var exists bool
|
|
|
|
if route.UseCache {
|
|
cache, exists = route.VinegarRoute.Cache.Get("")
|
|
} else {
|
|
cache, exists = route.VinegarRoute.Cache.GetFresh("")
|
|
}
|
|
|
|
if !exists {
|
|
route.srv.SendError(w, req, 404, "File not found.", errors.New("could not find file: "+route.fileRoot))
|
|
return
|
|
}
|
|
|
|
var content []byte
|
|
if clientAcceptsGzip(req) {
|
|
content = cache.CompressedContent
|
|
w.Header().Add(ContentEncodingHeaderKey, "gzip")
|
|
} else {
|
|
content = cache.Content
|
|
}
|
|
w.Header().Add(ContentTypeHeaderKey, cache.MimeType)
|
|
_, err := w.Write(content)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
}
|
|
|
|
return fun
|
|
}
|
|
|
|
func createCompressibleFileServletFunction(route *FileRoute, basePattern string, pathlike string) VinegarHandlerFunction {
|
|
var fun VinegarHandlerFunction = func(w http.ResponseWriter, req *http.Request) {
|
|
stub := strings.Replace(req.URL.Path, basePattern, "", 1)
|
|
cachedContent, exists := route.VinegarRoute.Cache.Get(stub)
|
|
//i don't like this logic below. we need to streamline this a lot better. it's a twisty jungle right now
|
|
|
|
resourcePath := path.Join(pathlike, filepath.Clean(stub))
|
|
|
|
if !exists {
|
|
content, fileExists := util.GetDiskContent(resourcePath)
|
|
if fileExists {
|
|
if route.UseCache {
|
|
route.VinegarRoute.Cache.Put(stub, resourcePath)
|
|
cachedContent, _ = route.VinegarRoute.Cache.Get(stub)
|
|
} else {
|
|
w.Header().Add(ContentTypeHeaderKey, util.GuessMimetype(stub))
|
|
w.Write(*content)
|
|
return
|
|
}
|
|
} else {
|
|
route.srv.SendError(w, req, 404, "Couldn't find your content.", errors.New("could not find valid file at ["+resourcePath+"]"))
|
|
return
|
|
}
|
|
}
|
|
w.Header().Add(ContentTypeHeaderKey, cachedContent.MimeType)
|
|
var err error = nil
|
|
|
|
if clientAcceptsGzip(req) {
|
|
w.Header().Add(ContentEncodingHeaderKey, "gzip")
|
|
_, err = w.Write(cachedContent.CompressedContent)
|
|
} else {
|
|
_, err = w.Write(cachedContent.Content)
|
|
}
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
}
|
|
return fun
|
|
}
|
|
|
|
func createUncompressedFileServletFunction(route *FileRoute, basePattern string, pathlike string) VinegarHandlerFunction {
|
|
var fun VinegarHandlerFunction = func(w http.ResponseWriter, req *http.Request) {
|
|
stub := filepath.Clean(strings.Replace(req.URL.Path, basePattern, "", 1))
|
|
resourcePath := path.Join(pathlike, stub)
|
|
entry, exists := route.VinegarRoute.Cache.Get(stub)
|
|
if !exists {
|
|
route.VinegarRoute.Cache.Put(stub, resourcePath)
|
|
entry, exists = route.VinegarRoute.Cache.Get(stub)
|
|
}
|
|
|
|
if exists {
|
|
w.Header().Add(ContentTypeHeaderKey, util.GuessMimetype(stub))
|
|
_, err := w.Write(entry.Content)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return
|
|
|
|
} else {
|
|
route.srv.SendError(w, req, 404, "Couldn't find your content.", errors.New("could not find file for ["+stub+"]"))
|
|
}
|
|
}
|
|
return fun
|
|
}
|
|
|
|
func clientAcceptsGzip(req *http.Request) bool {
|
|
encodings := req.Header.Get(AcceptEncodingHeaderKey)
|
|
return strings.Contains(encodings, "gzip")
|
|
}
|