mirror of
https://github.com/Theodor-Springmann-Stiftung/lenz-web.git
synced 2025-10-29 01:05:32 +00:00
Cache Settings for DEV mode
This commit is contained in:
141
helpers/middleware/etag.go
Normal file
141
helpers/middleware/etag.go
Normal file
@@ -0,0 +1,141 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"hash/crc32"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
|
||||
"github.com/valyala/bytebufferpool"
|
||||
)
|
||||
|
||||
// Config defines the config for middleware.
|
||||
type Config struct {
|
||||
// Weak indicates that a weak validator is used. Weak etags are easy
|
||||
// to generate, but are far less useful for comparisons. Strong
|
||||
// validators are ideal for comparisons but can be very difficult
|
||||
// to generate efficiently. Weak ETag values of two representations
|
||||
// of the same resources might be semantically equivalent, but not
|
||||
// byte-for-byte identical. This means weak etags prevent caching
|
||||
// when byte range requests are used, but strong etags mean range
|
||||
// requests can still be cached.
|
||||
Weak bool
|
||||
|
||||
// Next defines a function to skip this middleware when returned true.
|
||||
//
|
||||
// Optional. Default: nil
|
||||
Next func(c *fiber.Ctx) bool
|
||||
}
|
||||
|
||||
// ConfigDefault is the default config
|
||||
var ConfigDefault = Config{
|
||||
Weak: false,
|
||||
Next: nil,
|
||||
}
|
||||
|
||||
// Helper function to set default values
|
||||
func configDefault(config ...Config) Config {
|
||||
// Return default config if nothing provided
|
||||
if len(config) < 1 {
|
||||
return ConfigDefault
|
||||
}
|
||||
|
||||
// Override default config
|
||||
cfg := config[0]
|
||||
|
||||
// Set default values
|
||||
|
||||
return cfg
|
||||
}
|
||||
|
||||
func New() fiber.Handler {
|
||||
var (
|
||||
normalizedHeaderETag = []byte("Etag")
|
||||
weakPrefix = []byte("W/")
|
||||
)
|
||||
|
||||
const crcPol = 0xD5828281
|
||||
crc32q := crc32.MakeTable(crcPol)
|
||||
|
||||
// Return new handler
|
||||
return func(c *fiber.Ctx) error {
|
||||
// Return err if next handler returns one
|
||||
if err := c.Next(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Don't generate ETags for invalid responses
|
||||
if c.Response().StatusCode() != fiber.StatusOK {
|
||||
return nil
|
||||
}
|
||||
body := c.Response().Body()
|
||||
// Skips ETag if no response body is present
|
||||
if len(body) == 0 {
|
||||
return nil
|
||||
}
|
||||
// Skip ETag if header is already present
|
||||
if c.Response().Header.PeekBytes(normalizedHeaderETag) != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Generate ETag for response
|
||||
bb := bytebufferpool.Get()
|
||||
defer bytebufferpool.Put(bb)
|
||||
|
||||
_ = bb.WriteByte('"') //nolint:errcheck // This will never fail
|
||||
bb.B = appendUint(bb.Bytes(), uint32(len(body)))
|
||||
_ = bb.WriteByte('-') //nolint:errcheck // This will never fail
|
||||
bb.B = appendUint(bb.Bytes(), crc32.Checksum(body, crc32q))
|
||||
_ = bb.WriteByte('"') //nolint:errcheck // This will never fail
|
||||
|
||||
etag := bb.Bytes()
|
||||
|
||||
// Get ETag header from request
|
||||
clientEtag := c.Request().Header.Peek(fiber.HeaderIfNoneMatch)
|
||||
|
||||
// Check if client's ETag is weak
|
||||
if bytes.HasPrefix(clientEtag, weakPrefix) {
|
||||
// Check if server's ETag is weak
|
||||
if bytes.Equal(clientEtag[2:], etag) || bytes.Equal(clientEtag[2:], etag[2:]) {
|
||||
// W/1 == 1 || W/1 == W/1
|
||||
c.Context().ResetBody()
|
||||
|
||||
return c.SendStatus(fiber.StatusNotModified)
|
||||
}
|
||||
// W/1 != W/2 || W/1 != 2
|
||||
c.Response().Header.SetCanonical(normalizedHeaderETag, etag)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
if bytes.Contains(clientEtag, etag) {
|
||||
// 1 == 1
|
||||
c.Context().ResetBody()
|
||||
|
||||
return c.SendStatus(fiber.StatusNotModified)
|
||||
}
|
||||
// 1 != 2
|
||||
c.Response().Header.SetCanonical(normalizedHeaderETag, etag)
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// appendUint appends n to dst and returns the extended dst.
|
||||
func appendUint(dst []byte, n uint32) []byte {
|
||||
var b [20]byte
|
||||
buf := b[:]
|
||||
i := len(buf)
|
||||
var q uint32
|
||||
for n >= 10 {
|
||||
i--
|
||||
q = n / 10
|
||||
buf[i] = '0' + byte(n-q*10)
|
||||
n = q
|
||||
}
|
||||
i--
|
||||
buf[i] = '0' + byte(n)
|
||||
|
||||
dst = append(dst, buf[i:]...)
|
||||
return dst
|
||||
}
|
||||
298
helpers/middleware/filesystem/filesystem.go
Normal file
298
helpers/middleware/filesystem/filesystem.go
Normal file
@@ -0,0 +1,298 @@
|
||||
package filesystem
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/gofiber/fiber/v2/utils"
|
||||
)
|
||||
|
||||
// Config defines the config for middleware.
|
||||
type Config struct {
|
||||
// Next defines a function to skip this middleware when returned true.
|
||||
//
|
||||
// Optional. Default: nil
|
||||
Next func(c *fiber.Ctx) bool
|
||||
|
||||
// Root is a FileSystem that provides access
|
||||
// to a collection of files and directories.
|
||||
//
|
||||
// Required. Default: nil
|
||||
Root http.FileSystem `json:"-"`
|
||||
|
||||
// PathPrefix defines a prefix to be added to a filepath when
|
||||
// reading a file from the FileSystem.
|
||||
//
|
||||
// Use when using Go 1.16 embed.FS
|
||||
//
|
||||
// Optional. Default ""
|
||||
PathPrefix string `json:"path_prefix"`
|
||||
|
||||
// Enable directory browsing.
|
||||
//
|
||||
// Optional. Default: false
|
||||
Browse bool `json:"browse"`
|
||||
|
||||
// Index file for serving a directory.
|
||||
//
|
||||
// Optional. Default: "index.html"
|
||||
Index string `json:"index"`
|
||||
|
||||
// The value for the Cache-Control HTTP-header
|
||||
// that is set on the file response. MaxAge is defined in seconds.
|
||||
//
|
||||
// Optional. Default value 0.
|
||||
MaxAge int `json:"max_age"`
|
||||
|
||||
// File to return if path is not found. Useful for SPA's.
|
||||
//
|
||||
// Optional. Default: ""
|
||||
NotFoundFile string `json:"not_found_file"`
|
||||
|
||||
// The value for the Content-Type HTTP-header
|
||||
// that is set on the file response
|
||||
//
|
||||
// Optional. Default: ""
|
||||
ContentTypeCharset string `json:"content_type_charset"`
|
||||
|
||||
// The value for the Content-Type HTTP-header
|
||||
// that is set on the file response
|
||||
//
|
||||
// Optional. Default: true
|
||||
Dev bool `json:"dev"`
|
||||
}
|
||||
|
||||
// ConfigDefault is the default config
|
||||
var ConfigDefault = Config{
|
||||
Next: nil,
|
||||
Root: nil,
|
||||
PathPrefix: "",
|
||||
Browse: false,
|
||||
Index: "/index.html",
|
||||
MaxAge: 0,
|
||||
ContentTypeCharset: "",
|
||||
Dev: true,
|
||||
}
|
||||
|
||||
// New creates a new middleware handler.
|
||||
//
|
||||
// filesystem does not handle url encoded values (for example spaces)
|
||||
// on it's own. If you need that functionality, set "UnescapePath"
|
||||
// in fiber.Config
|
||||
func New(config ...Config) fiber.Handler {
|
||||
// Set default config
|
||||
cfg := ConfigDefault
|
||||
|
||||
// Override config if provided
|
||||
if len(config) > 0 {
|
||||
cfg = config[0]
|
||||
|
||||
// Set default values
|
||||
if cfg.Index == "" {
|
||||
cfg.Index = ConfigDefault.Index
|
||||
}
|
||||
if !strings.HasPrefix(cfg.Index, "/") {
|
||||
cfg.Index = "/" + cfg.Index
|
||||
}
|
||||
if cfg.NotFoundFile != "" && !strings.HasPrefix(cfg.NotFoundFile, "/") {
|
||||
cfg.NotFoundFile = "/" + cfg.NotFoundFile
|
||||
}
|
||||
}
|
||||
|
||||
if cfg.Root == nil {
|
||||
panic("filesystem: Root cannot be nil")
|
||||
}
|
||||
|
||||
if cfg.PathPrefix != "" && !strings.HasPrefix(cfg.PathPrefix, "/") {
|
||||
cfg.PathPrefix = "/" + cfg.PathPrefix
|
||||
}
|
||||
|
||||
var once sync.Once
|
||||
var prefix string
|
||||
cacheControlStr := "public, max-age=" + strconv.Itoa(cfg.MaxAge)
|
||||
|
||||
if cfg.Dev {
|
||||
cacheControlStr = "no-cache, must-revalidate"
|
||||
}
|
||||
|
||||
// Return new handler
|
||||
return func(c *fiber.Ctx) error {
|
||||
// Don't execute middleware if Next returns true
|
||||
if cfg.Next != nil && cfg.Next(c) {
|
||||
return c.Next()
|
||||
}
|
||||
|
||||
method := c.Method()
|
||||
|
||||
// We only serve static assets on GET or HEAD methods
|
||||
if method != fiber.MethodGet && method != fiber.MethodHead {
|
||||
return c.Next()
|
||||
}
|
||||
|
||||
// Set prefix once
|
||||
once.Do(func() {
|
||||
prefix = c.Route().Path
|
||||
})
|
||||
|
||||
// Strip prefix
|
||||
path := strings.TrimPrefix(c.Path(), prefix)
|
||||
if !strings.HasPrefix(path, "/") {
|
||||
path = "/" + path
|
||||
}
|
||||
// Add PathPrefix
|
||||
if cfg.PathPrefix != "" {
|
||||
// PathPrefix already has a "/" prefix
|
||||
path = cfg.PathPrefix + path
|
||||
}
|
||||
|
||||
if len(path) > 1 {
|
||||
path = utils.TrimRight(path, '/')
|
||||
}
|
||||
file, err := cfg.Root.Open(path)
|
||||
if err != nil && errors.Is(err, fs.ErrNotExist) && cfg.NotFoundFile != "" {
|
||||
file, err = cfg.Root.Open(cfg.NotFoundFile)
|
||||
}
|
||||
if err != nil {
|
||||
if errors.Is(err, fs.ErrNotExist) {
|
||||
return c.Status(fiber.StatusNotFound).Next()
|
||||
}
|
||||
return fmt.Errorf("failed to open: %w", err)
|
||||
}
|
||||
|
||||
stat, err := file.Stat()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to stat: %w", err)
|
||||
}
|
||||
|
||||
// Serve index if path is directory
|
||||
if stat.IsDir() {
|
||||
indexPath := utils.TrimRight(path, '/') + cfg.Index
|
||||
index, err := cfg.Root.Open(indexPath)
|
||||
if err == nil {
|
||||
indexStat, err := index.Stat()
|
||||
if err == nil {
|
||||
file = index
|
||||
stat = indexStat
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Browse directory if no index found and browsing is enabled
|
||||
if stat.IsDir() {
|
||||
if cfg.Browse {
|
||||
return dirList(c, file)
|
||||
}
|
||||
return fiber.ErrForbidden
|
||||
}
|
||||
|
||||
c.Status(fiber.StatusOK)
|
||||
|
||||
modTime := stat.ModTime()
|
||||
contentLength := int(stat.Size())
|
||||
|
||||
// Set Content Type header
|
||||
if cfg.ContentTypeCharset == "" {
|
||||
c.Type(getFileExtension(stat.Name()))
|
||||
} else {
|
||||
c.Type(getFileExtension(stat.Name()), cfg.ContentTypeCharset)
|
||||
}
|
||||
|
||||
// Set Last Modified header
|
||||
if !modTime.IsZero() {
|
||||
c.Set(fiber.HeaderLastModified, modTime.UTC().Format(http.TimeFormat))
|
||||
}
|
||||
|
||||
if method == fiber.MethodGet {
|
||||
if cfg.MaxAge > 0 {
|
||||
c.Set(fiber.HeaderCacheControl, cacheControlStr)
|
||||
}
|
||||
c.Response().SetBodyStream(file, contentLength)
|
||||
return nil
|
||||
}
|
||||
if method == fiber.MethodHead {
|
||||
c.Request().ResetBody()
|
||||
// Fasthttp should skipbody by default if HEAD?
|
||||
c.Response().SkipBody = true
|
||||
c.Response().Header.SetContentLength(contentLength)
|
||||
if err := file.Close(); err != nil {
|
||||
return fmt.Errorf("failed to close: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
return c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// SendFile serves a file from an HTTP file system at the specified path.
|
||||
// It handles content serving, sets appropriate headers, and returns errors when needed.
|
||||
// Usage: err := SendFile(ctx, fs, "/path/to/file.txt")
|
||||
func SendFile(c *fiber.Ctx, filesystem http.FileSystem, path string) error {
|
||||
file, err := filesystem.Open(path)
|
||||
if err != nil {
|
||||
if errors.Is(err, fs.ErrNotExist) {
|
||||
return fiber.ErrNotFound
|
||||
}
|
||||
return fmt.Errorf("failed to open: %w", err)
|
||||
}
|
||||
|
||||
stat, err := file.Stat()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to stat: %w", err)
|
||||
}
|
||||
|
||||
// Serve index if path is directory
|
||||
if stat.IsDir() {
|
||||
indexPath := utils.TrimRight(path, '/') + ConfigDefault.Index
|
||||
index, err := filesystem.Open(indexPath)
|
||||
if err == nil {
|
||||
indexStat, err := index.Stat()
|
||||
if err == nil {
|
||||
file = index
|
||||
stat = indexStat
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Return forbidden if no index found
|
||||
if stat.IsDir() {
|
||||
return fiber.ErrForbidden
|
||||
}
|
||||
|
||||
c.Status(fiber.StatusOK)
|
||||
|
||||
modTime := stat.ModTime()
|
||||
contentLength := int(stat.Size())
|
||||
|
||||
// Set Content Type header
|
||||
c.Type(getFileExtension(stat.Name()))
|
||||
|
||||
// Set Last Modified header
|
||||
if !modTime.IsZero() {
|
||||
c.Set(fiber.HeaderLastModified, modTime.UTC().Format(http.TimeFormat))
|
||||
}
|
||||
|
||||
method := c.Method()
|
||||
if method == fiber.MethodGet {
|
||||
c.Response().SetBodyStream(file, contentLength)
|
||||
return nil
|
||||
}
|
||||
if method == fiber.MethodHead {
|
||||
c.Request().ResetBody()
|
||||
// Fasthttp should skipbody by default if HEAD?
|
||||
c.Response().SkipBody = true
|
||||
c.Response().Header.SetContentLength(contentLength)
|
||||
if err := file.Close(); err != nil {
|
||||
return fmt.Errorf("failed to close: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
235
helpers/middleware/filesystem/filesystem_test.go
Normal file
235
helpers/middleware/filesystem/filesystem_test.go
Normal file
@@ -0,0 +1,235 @@
|
||||
//nolint:bodyclose // Much easier to just ignore memory leaks in tests
|
||||
package filesystem
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/gofiber/fiber/v2/utils"
|
||||
)
|
||||
|
||||
// go test -run Test_FileSystem
|
||||
func Test_FileSystem(t *testing.T) {
|
||||
t.Parallel()
|
||||
app := fiber.New()
|
||||
|
||||
app.Use("/test", New(Config{
|
||||
Root: http.Dir("../../.github/testdata/fs"),
|
||||
}))
|
||||
|
||||
app.Use("/dir", New(Config{
|
||||
Root: http.Dir("../../.github/testdata/fs"),
|
||||
Browse: true,
|
||||
}))
|
||||
|
||||
app.Get("/", func(c *fiber.Ctx) error {
|
||||
return c.SendString("Hello, World!")
|
||||
})
|
||||
|
||||
app.Use("/spatest", New(Config{
|
||||
Root: http.Dir("../../.github/testdata/fs"),
|
||||
Index: "index.html",
|
||||
NotFoundFile: "index.html",
|
||||
}))
|
||||
|
||||
app.Use("/prefix", New(Config{
|
||||
Root: http.Dir("../../.github/testdata/fs"),
|
||||
PathPrefix: "img",
|
||||
}))
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
url string
|
||||
statusCode int
|
||||
contentType string
|
||||
modifiedTime string
|
||||
}{
|
||||
{
|
||||
name: "Should be returns status 200 with suitable content-type",
|
||||
url: "/test/index.html",
|
||||
statusCode: 200,
|
||||
contentType: "text/html",
|
||||
},
|
||||
{
|
||||
name: "Should be returns status 200 with suitable content-type",
|
||||
url: "/test",
|
||||
statusCode: 200,
|
||||
contentType: "text/html",
|
||||
},
|
||||
{
|
||||
name: "Should be returns status 200 with suitable content-type",
|
||||
url: "/test/css/style.css",
|
||||
statusCode: 200,
|
||||
contentType: "text/css",
|
||||
},
|
||||
{
|
||||
name: "Should be returns status 404",
|
||||
url: "/test/nofile.js",
|
||||
statusCode: 404,
|
||||
},
|
||||
{
|
||||
name: "Should be returns status 404",
|
||||
url: "/test/nofile",
|
||||
statusCode: 404,
|
||||
},
|
||||
{
|
||||
name: "Should be returns status 200",
|
||||
url: "/",
|
||||
statusCode: 200,
|
||||
contentType: "text/plain; charset=utf-8",
|
||||
},
|
||||
{
|
||||
name: "Should be returns status 403",
|
||||
url: "/test/img",
|
||||
statusCode: 403,
|
||||
},
|
||||
{
|
||||
name: "Should list the directory contents",
|
||||
url: "/dir/img",
|
||||
statusCode: 200,
|
||||
contentType: "text/html",
|
||||
},
|
||||
{
|
||||
name: "Should list the directory contents",
|
||||
url: "/dir/img/",
|
||||
statusCode: 200,
|
||||
contentType: "text/html",
|
||||
},
|
||||
{
|
||||
name: "Should be returns status 200",
|
||||
url: "/dir/img/fiber.png",
|
||||
statusCode: 200,
|
||||
contentType: "image/png",
|
||||
},
|
||||
{
|
||||
name: "Should be return status 200",
|
||||
url: "/spatest/doesnotexist",
|
||||
statusCode: 200,
|
||||
contentType: "text/html",
|
||||
},
|
||||
{
|
||||
name: "PathPrefix should be applied",
|
||||
url: "/prefix/fiber.png",
|
||||
statusCode: 200,
|
||||
contentType: "image/png",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, tt.url, nil))
|
||||
utils.AssertEqual(t, nil, err)
|
||||
utils.AssertEqual(t, tt.statusCode, resp.StatusCode)
|
||||
|
||||
if tt.contentType != "" {
|
||||
ct := resp.Header.Get("Content-Type")
|
||||
utils.AssertEqual(t, tt.contentType, ct)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// go test -run Test_FileSystem_Next
|
||||
func Test_FileSystem_Next(t *testing.T) {
|
||||
t.Parallel()
|
||||
app := fiber.New()
|
||||
app.Use(New(Config{
|
||||
Root: http.Dir("../../.github/testdata/fs"),
|
||||
Next: func(_ *fiber.Ctx) bool {
|
||||
return true
|
||||
},
|
||||
}))
|
||||
|
||||
resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil))
|
||||
utils.AssertEqual(t, nil, err)
|
||||
utils.AssertEqual(t, fiber.StatusNotFound, resp.StatusCode)
|
||||
}
|
||||
|
||||
func Test_FileSystem_NonGetAndHead(t *testing.T) {
|
||||
t.Parallel()
|
||||
app := fiber.New()
|
||||
|
||||
app.Use("/test", New(Config{
|
||||
Root: http.Dir("../../.github/testdata/fs"),
|
||||
}))
|
||||
|
||||
resp, err := app.Test(httptest.NewRequest(fiber.MethodPost, "/test", nil))
|
||||
utils.AssertEqual(t, nil, err)
|
||||
utils.AssertEqual(t, 404, resp.StatusCode)
|
||||
}
|
||||
|
||||
func Test_FileSystem_Head(t *testing.T) {
|
||||
t.Parallel()
|
||||
app := fiber.New()
|
||||
|
||||
app.Use("/test", New(Config{
|
||||
Root: http.Dir("../../.github/testdata/fs"),
|
||||
}))
|
||||
|
||||
req, err := http.NewRequestWithContext(context.Background(), fiber.MethodHead, "/test", nil)
|
||||
utils.AssertEqual(t, nil, err)
|
||||
resp, err := app.Test(req)
|
||||
utils.AssertEqual(t, nil, err)
|
||||
utils.AssertEqual(t, 200, resp.StatusCode)
|
||||
}
|
||||
|
||||
func Test_FileSystem_NoRoot(t *testing.T) {
|
||||
t.Parallel()
|
||||
defer func() {
|
||||
utils.AssertEqual(t, "filesystem: Root cannot be nil", recover())
|
||||
}()
|
||||
|
||||
app := fiber.New()
|
||||
app.Use(New())
|
||||
_, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil))
|
||||
utils.AssertEqual(t, nil, err)
|
||||
}
|
||||
|
||||
func Test_FileSystem_UsingParam(t *testing.T) {
|
||||
t.Parallel()
|
||||
app := fiber.New()
|
||||
|
||||
app.Use("/:path", func(c *fiber.Ctx) error {
|
||||
return SendFile(c, http.Dir("../../.github/testdata/fs"), c.Params("path")+".html")
|
||||
})
|
||||
|
||||
req, err := http.NewRequestWithContext(context.Background(), fiber.MethodHead, "/index", nil)
|
||||
utils.AssertEqual(t, nil, err)
|
||||
resp, err := app.Test(req)
|
||||
utils.AssertEqual(t, nil, err)
|
||||
utils.AssertEqual(t, 200, resp.StatusCode)
|
||||
}
|
||||
|
||||
func Test_FileSystem_UsingParam_NonFile(t *testing.T) {
|
||||
t.Parallel()
|
||||
app := fiber.New()
|
||||
|
||||
app.Use("/:path", func(c *fiber.Ctx) error {
|
||||
return SendFile(c, http.Dir("../../.github/testdata/fs"), c.Params("path")+".html")
|
||||
})
|
||||
|
||||
req, err := http.NewRequestWithContext(context.Background(), fiber.MethodHead, "/template", nil)
|
||||
utils.AssertEqual(t, nil, err)
|
||||
resp, err := app.Test(req)
|
||||
utils.AssertEqual(t, nil, err)
|
||||
utils.AssertEqual(t, 404, resp.StatusCode)
|
||||
}
|
||||
|
||||
func Test_FileSystem_UsingContentTypeCharset(t *testing.T) {
|
||||
t.Parallel()
|
||||
app := fiber.New()
|
||||
app.Use(New(Config{
|
||||
Root: http.Dir("../../.github/testdata/fs/index.html"),
|
||||
ContentTypeCharset: "UTF-8",
|
||||
}))
|
||||
|
||||
resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil))
|
||||
utils.AssertEqual(t, nil, err)
|
||||
utils.AssertEqual(t, 200, resp.StatusCode)
|
||||
utils.AssertEqual(t, "text/html; charset=UTF-8", resp.Header.Get("Content-Type"))
|
||||
}
|
||||
66
helpers/middleware/filesystem/utils.go
Normal file
66
helpers/middleware/filesystem/utils.go
Normal file
@@ -0,0 +1,66 @@
|
||||
package filesystem
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"html"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/gofiber/fiber/v2/utils"
|
||||
)
|
||||
|
||||
func getFileExtension(p string) string {
|
||||
n := strings.LastIndexByte(p, '.')
|
||||
if n < 0 {
|
||||
return ""
|
||||
}
|
||||
return p[n:]
|
||||
}
|
||||
|
||||
func dirList(c *fiber.Ctx, f http.File) error {
|
||||
fileinfos, err := f.Readdir(-1)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read dir: %w", err)
|
||||
}
|
||||
|
||||
fm := make(map[string]os.FileInfo, len(fileinfos))
|
||||
filenames := make([]string, 0, len(fileinfos))
|
||||
for _, fi := range fileinfos {
|
||||
name := fi.Name()
|
||||
fm[name] = fi
|
||||
filenames = append(filenames, name)
|
||||
}
|
||||
|
||||
basePathEscaped := html.EscapeString(c.Path())
|
||||
_, _ = fmt.Fprintf(c, "<html><head><title>%s</title><style>.dir { font-weight: bold }</style></head><body>", basePathEscaped)
|
||||
_, _ = fmt.Fprintf(c, "<h1>%s</h1>", basePathEscaped)
|
||||
_, _ = fmt.Fprint(c, "<ul>")
|
||||
|
||||
if len(basePathEscaped) > 1 {
|
||||
parentPathEscaped := html.EscapeString(utils.TrimRight(c.Path(), '/') + "/..")
|
||||
_, _ = fmt.Fprintf(c, `<li><a href="%s" class="dir">..</a></li>`, parentPathEscaped)
|
||||
}
|
||||
|
||||
sort.Strings(filenames)
|
||||
for _, name := range filenames {
|
||||
pathEscaped := html.EscapeString(path.Join(c.Path() + "/" + name))
|
||||
fi := fm[name]
|
||||
auxStr := "dir"
|
||||
className := "dir"
|
||||
if !fi.IsDir() {
|
||||
auxStr = fmt.Sprintf("file, %d bytes", fi.Size())
|
||||
className = "file"
|
||||
}
|
||||
_, _ = fmt.Fprintf(c, `<li><a href="%s" class="%s">%s</a>, %s, last modified %s</li>`,
|
||||
pathEscaped, className, html.EscapeString(name), auxStr, fi.ModTime())
|
||||
}
|
||||
_, _ = fmt.Fprint(c, "</ul></body></html>")
|
||||
|
||||
c.Type("html")
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -4,15 +4,16 @@ import (
|
||||
"io/fs"
|
||||
"net/http"
|
||||
|
||||
"github.com/Theodor-Springmann-Stiftung/lenz-web/helpers/middleware/filesystem"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/gofiber/fiber/v2/middleware/filesystem"
|
||||
)
|
||||
|
||||
func StaticHandler(fs *fs.FS) fiber.Handler {
|
||||
func StaticHandler(fs *fs.FS, dev bool) fiber.Handler {
|
||||
return filesystem.New(filesystem.Config{
|
||||
Root: http.FS(*fs),
|
||||
Browse: false,
|
||||
Index: "index.html",
|
||||
Dev: dev,
|
||||
MaxAge: 3600,
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user