fiber/middleware/static/static.go

178 lines
4.2 KiB
Go

package static
import (
"fmt"
"io/fs"
"os"
"path/filepath"
"strconv"
"strings"
"sync"
"github.com/gofiber/fiber/v3"
"github.com/gofiber/utils/v2"
"github.com/valyala/fasthttp"
)
// New creates a new middleware handler.
// The root argument specifies the root directory from which to serve static assets.
//
// Note: Root has to be string or fs.FS, otherwise it will panic.
func New(root string, cfg ...Config) fiber.Handler {
config := configDefault(cfg...)
var createFS sync.Once
var fileHandler fasthttp.RequestHandler
var cacheControlValue string
// adjustments for io/fs compatibility
if config.FS != nil && root == "" {
root = "."
}
return func(c fiber.Ctx) error {
// Don't execute middleware if Next returns true
if config.Next != nil && config.Next(c) {
return c.Next()
}
// We only serve static assets on GET or HEAD methods
method := c.Method()
if method != fiber.MethodGet && method != fiber.MethodHead {
return c.Next()
}
// Initialize FS
createFS.Do(func() {
prefix := c.Route().Path
// Is prefix a partial wildcard?
if strings.Contains(prefix, "*") {
// /john* -> /john
prefix = strings.Split(prefix, "*")[0]
}
prefixLen := len(prefix)
if prefixLen > 1 && prefix[prefixLen-1:] == "/" {
// /john/ -> /john
prefixLen--
}
fs := &fasthttp.FS{
Root: root,
FS: config.FS,
AllowEmptyRoot: true,
GenerateIndexPages: config.Browse,
AcceptByteRange: config.ByteRange,
Compress: config.Compress,
CompressBrotli: config.Compress, // Brotli compression won't work without this
CompressedFileSuffixes: c.App().Config().CompressedFileSuffixes,
CacheDuration: config.CacheDuration,
SkipCache: config.CacheDuration < 0,
IndexNames: config.IndexNames,
PathNotFound: func(fctx *fasthttp.RequestCtx) {
fctx.Response.SetStatusCode(fiber.StatusNotFound)
},
}
fs.PathRewrite = func(fctx *fasthttp.RequestCtx) []byte {
path := fctx.Path()
if len(path) >= prefixLen {
checkFile, err := isFile(root, fs.FS)
if err != nil {
return path
}
// If the root is a file, we need to reset the path to "/" always.
switch {
case checkFile && fs.FS == nil:
path = []byte("/")
case checkFile && fs.FS != nil:
path = utils.UnsafeBytes(root)
default:
path = path[prefixLen:]
if len(path) == 0 || path[len(path)-1] != '/' {
path = append(path, '/')
}
}
}
if len(path) > 0 && path[0] != '/' {
path = append([]byte("/"), path...)
}
return path
}
maxAge := config.MaxAge
if maxAge > 0 {
cacheControlValue = "public, max-age=" + strconv.Itoa(maxAge)
}
fileHandler = fs.NewRequestHandler()
})
// Serve file
fileHandler(c.RequestCtx())
// Sets the response Content-Disposition header to attachment if the Download option is true
if config.Download {
c.Attachment()
}
// Return request if found and not forbidden
status := c.RequestCtx().Response.StatusCode()
if status != fiber.StatusNotFound && status != fiber.StatusForbidden {
if len(cacheControlValue) > 0 {
c.RequestCtx().Response.Header.Set(fiber.HeaderCacheControl, cacheControlValue)
}
if config.ModifyResponse != nil {
return config.ModifyResponse(c)
}
return nil
}
// Return custom 404 handler if provided.
if config.NotFoundHandler != nil {
return config.NotFoundHandler(c)
}
// Reset response to default
c.RequestCtx().SetContentType("") // Issue #420
c.RequestCtx().Response.SetStatusCode(fiber.StatusOK)
c.RequestCtx().Response.SetBodyString("")
// Next middleware
return c.Next()
}
}
// isFile checks if the root is a file.
func isFile(root string, filesystem fs.FS) (bool, error) {
var file fs.File
var err error
if filesystem != nil {
file, err = filesystem.Open(root)
if err != nil {
return false, fmt.Errorf("static: %w", err)
}
} else {
file, err = os.Open(filepath.Clean(root))
if err != nil {
return false, fmt.Errorf("static: %w", err)
}
}
stat, err := file.Stat()
if err != nil {
return false, fmt.Errorf("static: %w", err)
}
return stat.Mode().IsRegular(), nil
}