mirror of https://github.com/gofiber/fiber.git
✨ v3 (feature): add configuration support to c.SendFile() (#3017)
* ✨ v3 (feature): add configuration support to c.SendFile()
* update
* cover more edge-cases
* optimize
* update compression, add mutex for sendfile slice
* fix data races
* add benchmark
* update docs
* update docs
* update
* update tests
* fix linter
* update
* update tests
---------
Co-authored-by: Juan Calderon-Perez <835733+gaby@users.noreply.github.com>
pull/3054/head^2
parent
83731cef85
commit
21ede5954c
5
app.go
5
app.go
|
@ -123,6 +123,10 @@ type App struct {
|
|||
configured Config
|
||||
// customConstraints is a list of external constraints
|
||||
customConstraints []CustomConstraint
|
||||
// sendfiles stores configurations for handling ctx.SendFile operations
|
||||
sendfiles []*sendFileStore
|
||||
// sendfilesMutex is a mutex used for sendfile operations
|
||||
sendfilesMutex sync.RWMutex
|
||||
}
|
||||
|
||||
// Config is a struct holding the server settings.
|
||||
|
@ -440,6 +444,7 @@ func New(config ...Config) *App {
|
|||
getString: utils.UnsafeString,
|
||||
latestRoute: &Route{},
|
||||
customBinders: []CustomBinder{},
|
||||
sendfiles: []*sendFileStore{},
|
||||
}
|
||||
|
||||
// Create Ctx pool
|
||||
|
|
190
ctx.go
190
ctx.go
|
@ -11,6 +11,7 @@ import (
|
|||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"mime/multipart"
|
||||
"net"
|
||||
"net/http"
|
||||
|
@ -69,6 +70,84 @@ type DefaultCtx struct {
|
|||
redirectionMessages []string // Messages of the previous redirect
|
||||
}
|
||||
|
||||
// SendFile defines configuration options when to transfer file with SendFile.
|
||||
type SendFile struct {
|
||||
// FS is the file system to serve the static files from.
|
||||
// You can use interfaces compatible with fs.FS like embed.FS, os.DirFS etc.
|
||||
//
|
||||
// Optional. Default: nil
|
||||
FS fs.FS
|
||||
|
||||
// When set to true, the server tries minimizing CPU usage by caching compressed files.
|
||||
// This works differently than the github.com/gofiber/compression middleware.
|
||||
// You have to set Content-Encoding header to compress the file.
|
||||
// Available compression methods are gzip, br, and zstd.
|
||||
//
|
||||
// Optional. Default value false
|
||||
Compress bool `json:"compress"`
|
||||
|
||||
// When set to true, enables byte range requests.
|
||||
//
|
||||
// Optional. Default value false
|
||||
ByteRange bool `json:"byte_range"`
|
||||
|
||||
// When set to true, enables direct download.
|
||||
//
|
||||
// Optional. Default: false.
|
||||
Download bool `json:"download"`
|
||||
|
||||
// Expiration duration for inactive file handlers.
|
||||
// Use a negative time.Duration to disable it.
|
||||
//
|
||||
// Optional. Default value 10 * time.Second.
|
||||
CacheDuration time.Duration `json:"cache_duration"`
|
||||
|
||||
// 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"`
|
||||
}
|
||||
|
||||
// sendFileStore is used to keep the SendFile configuration and the handler.
|
||||
type sendFileStore struct {
|
||||
handler fasthttp.RequestHandler
|
||||
config SendFile
|
||||
cacheControlValue string
|
||||
}
|
||||
|
||||
// compareConfig compares the current SendFile config with the new one
|
||||
// and returns true if they are different.
|
||||
//
|
||||
// Here we don't use reflect.DeepEqual because it is quite slow compared to manual comparison.
|
||||
func (sf *sendFileStore) compareConfig(cfg SendFile) bool {
|
||||
if sf.config.FS != cfg.FS {
|
||||
return false
|
||||
}
|
||||
|
||||
if sf.config.Compress != cfg.Compress {
|
||||
return false
|
||||
}
|
||||
|
||||
if sf.config.ByteRange != cfg.ByteRange {
|
||||
return false
|
||||
}
|
||||
|
||||
if sf.config.Download != cfg.Download {
|
||||
return false
|
||||
}
|
||||
|
||||
if sf.config.CacheDuration != cfg.CacheDuration {
|
||||
return false
|
||||
}
|
||||
|
||||
if sf.config.MaxAge != cfg.MaxAge {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// TLSHandler object
|
||||
type TLSHandler struct {
|
||||
clientHelloInfo *tls.ClientHelloInfo
|
||||
|
@ -1414,48 +1493,87 @@ func (c *DefaultCtx) Send(body []byte) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
var (
|
||||
sendFileOnce sync.Once
|
||||
sendFileFS *fasthttp.FS
|
||||
sendFileHandler fasthttp.RequestHandler
|
||||
)
|
||||
|
||||
// SendFile transfers the file from the given path.
|
||||
// The file is not compressed by default, enable this by passing a 'true' argument
|
||||
// Sets the Content-Type response HTTP header field based on the filenames extension.
|
||||
func (c *DefaultCtx) SendFile(file string, compress ...bool) error {
|
||||
func (c *DefaultCtx) SendFile(file string, config ...SendFile) error {
|
||||
// Save the filename, we will need it in the error message if the file isn't found
|
||||
filename := file
|
||||
|
||||
// https://github.com/valyala/fasthttp/blob/c7576cc10cabfc9c993317a2d3f8355497bea156/fs.go#L129-L134
|
||||
sendFileOnce.Do(func() {
|
||||
const cacheDuration = 10 * time.Second
|
||||
sendFileFS = &fasthttp.FS{
|
||||
var cfg SendFile
|
||||
if len(config) > 0 {
|
||||
cfg = config[0]
|
||||
}
|
||||
|
||||
if cfg.CacheDuration == 0 {
|
||||
cfg.CacheDuration = 10 * time.Second
|
||||
}
|
||||
|
||||
var fsHandler fasthttp.RequestHandler
|
||||
var cacheControlValue string
|
||||
|
||||
c.app.sendfilesMutex.RLock()
|
||||
for _, sf := range c.app.sendfiles {
|
||||
if sf.compareConfig(cfg) {
|
||||
fsHandler = sf.handler
|
||||
cacheControlValue = sf.cacheControlValue
|
||||
break
|
||||
}
|
||||
}
|
||||
c.app.sendfilesMutex.RUnlock()
|
||||
|
||||
if fsHandler == nil {
|
||||
fasthttpFS := &fasthttp.FS{
|
||||
Root: "",
|
||||
FS: cfg.FS,
|
||||
AllowEmptyRoot: true,
|
||||
GenerateIndexPages: false,
|
||||
AcceptByteRange: true,
|
||||
Compress: true,
|
||||
CompressBrotli: true,
|
||||
AcceptByteRange: cfg.ByteRange,
|
||||
Compress: cfg.Compress,
|
||||
CompressBrotli: cfg.Compress,
|
||||
CompressedFileSuffixes: c.app.config.CompressedFileSuffixes,
|
||||
CacheDuration: cacheDuration,
|
||||
CacheDuration: cfg.CacheDuration,
|
||||
SkipCache: cfg.CacheDuration < 0,
|
||||
IndexNames: []string{"index.html"},
|
||||
PathNotFound: func(ctx *fasthttp.RequestCtx) {
|
||||
ctx.Response.SetStatusCode(StatusNotFound)
|
||||
},
|
||||
}
|
||||
sendFileHandler = sendFileFS.NewRequestHandler()
|
||||
})
|
||||
|
||||
if cfg.FS != nil {
|
||||
fasthttpFS.Root = "."
|
||||
}
|
||||
|
||||
sf := &sendFileStore{
|
||||
config: cfg,
|
||||
handler: fasthttpFS.NewRequestHandler(),
|
||||
}
|
||||
|
||||
maxAge := cfg.MaxAge
|
||||
if maxAge > 0 {
|
||||
sf.cacheControlValue = "public, max-age=" + strconv.Itoa(maxAge)
|
||||
}
|
||||
|
||||
// set vars
|
||||
fsHandler = sf.handler
|
||||
cacheControlValue = sf.cacheControlValue
|
||||
|
||||
c.app.sendfilesMutex.Lock()
|
||||
c.app.sendfiles = append(c.app.sendfiles, sf)
|
||||
c.app.sendfilesMutex.Unlock()
|
||||
}
|
||||
|
||||
// Keep original path for mutable params
|
||||
c.pathOriginal = utils.CopyString(c.pathOriginal)
|
||||
// Disable compression
|
||||
if len(compress) == 0 || !compress[0] {
|
||||
|
||||
// Delete the Accept-Encoding header if compression is disabled
|
||||
if !cfg.Compress {
|
||||
// https://github.com/valyala/fasthttp/blob/7cc6f4c513f9e0d3686142e0a1a5aa2f76b3194a/fs.go#L55
|
||||
c.fasthttp.Request.Header.Del(HeaderAcceptEncoding)
|
||||
}
|
||||
|
||||
// copy of https://github.com/valyala/fasthttp/blob/7cc6f4c513f9e0d3686142e0a1a5aa2f76b3194a/fs.go#L103-L121 with small adjustments
|
||||
if len(file) == 0 || !filepath.IsAbs(file) {
|
||||
if len(file) == 0 || (!filepath.IsAbs(file) && cfg.FS == nil) {
|
||||
// extend relative path to absolute path
|
||||
hasTrailingSlash := len(file) > 0 && (file[len(file)-1] == '/' || file[len(file)-1] == '\\')
|
||||
|
||||
|
@ -1468,6 +1586,7 @@ func (c *DefaultCtx) SendFile(file string, compress ...bool) error {
|
|||
file += "/"
|
||||
}
|
||||
}
|
||||
|
||||
// convert the path to forward slashes regardless the OS in order to set the URI properly
|
||||
// the handler will convert back to OS path separator before opening the file
|
||||
file = filepath.ToSlash(file)
|
||||
|
@ -1475,22 +1594,43 @@ func (c *DefaultCtx) SendFile(file string, compress ...bool) error {
|
|||
// Restore the original requested URL
|
||||
originalURL := utils.CopyString(c.OriginalURL())
|
||||
defer c.fasthttp.Request.SetRequestURI(originalURL)
|
||||
|
||||
// Set new URI for fileHandler
|
||||
c.fasthttp.Request.SetRequestURI(file)
|
||||
|
||||
// Save status code
|
||||
status := c.fasthttp.Response.StatusCode()
|
||||
|
||||
// Serve file
|
||||
sendFileHandler(c.fasthttp)
|
||||
fsHandler(c.fasthttp)
|
||||
|
||||
// Sets the response Content-Disposition header to attachment if the Download option is true
|
||||
if cfg.Download {
|
||||
c.Attachment()
|
||||
}
|
||||
|
||||
// Get the status code which is set by fasthttp
|
||||
fsStatus := c.fasthttp.Response.StatusCode()
|
||||
// Set the status code set by the user if it is different from the fasthttp status code and 200
|
||||
if status != fsStatus && status != StatusOK {
|
||||
c.Status(status)
|
||||
}
|
||||
|
||||
// Check for error
|
||||
if status != StatusNotFound && fsStatus == StatusNotFound {
|
||||
return NewError(StatusNotFound, fmt.Sprintf("sendfile: file %s not found", filename))
|
||||
}
|
||||
|
||||
// Set the status code set by the user if it is different from the fasthttp status code and 200
|
||||
if status != fsStatus && status != StatusOK {
|
||||
c.Status(status)
|
||||
}
|
||||
|
||||
// Apply cache control header
|
||||
if status != StatusNotFound && status != StatusForbidden {
|
||||
if len(cacheControlValue) > 0 {
|
||||
c.Context().Response.Header.Set(HeaderCacheControl, cacheControlValue)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
|
@ -273,7 +273,7 @@ type Ctx interface {
|
|||
// SendFile transfers the file from the given path.
|
||||
// The file is not compressed by default, enable this by passing a 'true' argument
|
||||
// Sets the Content-Type response HTTP header field based on the filenames extension.
|
||||
SendFile(file string, compress ...bool) error
|
||||
SendFile(file string, config ...SendFile) error
|
||||
// SendStatus sets the HTTP status code and if the response body is empty,
|
||||
// it sets the correct status message in the body.
|
||||
SendStatus(status int) error
|
||||
|
|
292
ctx_test.go
292
ctx_test.go
|
@ -11,6 +11,7 @@ import (
|
|||
"compress/zlib"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"embed"
|
||||
"encoding/xml"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
@ -2970,19 +2971,254 @@ func Test_Ctx_SendFile(t *testing.T) {
|
|||
app.ReleaseCtx(c)
|
||||
}
|
||||
|
||||
func Test_Ctx_SendFile_Download(t *testing.T) {
|
||||
t.Parallel()
|
||||
app := New()
|
||||
|
||||
// fetch file content
|
||||
f, err := os.Open("./ctx.go")
|
||||
require.NoError(t, err)
|
||||
defer func() {
|
||||
require.NoError(t, f.Close())
|
||||
}()
|
||||
expectFileContent, err := io.ReadAll(f)
|
||||
require.NoError(t, err)
|
||||
// fetch file info for the not modified test case
|
||||
_, err = os.Stat("./ctx.go")
|
||||
require.NoError(t, err)
|
||||
|
||||
// simple test case
|
||||
c := app.AcquireCtx(&fasthttp.RequestCtx{})
|
||||
err = c.SendFile("ctx.go", SendFile{
|
||||
Download: true,
|
||||
})
|
||||
// check expectation
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, expectFileContent, c.Response().Body())
|
||||
require.Equal(t, "attachment", string(c.Response().Header.Peek(HeaderContentDisposition)))
|
||||
require.Equal(t, StatusOK, c.Response().StatusCode())
|
||||
app.ReleaseCtx(c)
|
||||
}
|
||||
|
||||
func Test_Ctx_SendFile_MaxAge(t *testing.T) {
|
||||
t.Parallel()
|
||||
app := New()
|
||||
|
||||
// fetch file content
|
||||
f, err := os.Open("./ctx.go")
|
||||
require.NoError(t, err)
|
||||
defer func() {
|
||||
require.NoError(t, f.Close())
|
||||
}()
|
||||
expectFileContent, err := io.ReadAll(f)
|
||||
require.NoError(t, err)
|
||||
|
||||
// fetch file info for the not modified test case
|
||||
_, err = os.Stat("./ctx.go")
|
||||
require.NoError(t, err)
|
||||
|
||||
// simple test case
|
||||
c := app.AcquireCtx(&fasthttp.RequestCtx{})
|
||||
err = c.SendFile("ctx.go", SendFile{
|
||||
MaxAge: 100,
|
||||
})
|
||||
|
||||
// check expectation
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, expectFileContent, c.Response().Body())
|
||||
require.Equal(t, "public, max-age=100", string(c.Context().Response.Header.Peek(HeaderCacheControl)), "CacheControl Control")
|
||||
require.Equal(t, StatusOK, c.Response().StatusCode())
|
||||
app.ReleaseCtx(c)
|
||||
}
|
||||
|
||||
func Test_Static_Compress(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
app := New()
|
||||
app.Get("/file", func(c Ctx) error {
|
||||
return c.SendFile("ctx.go", SendFile{
|
||||
Compress: true,
|
||||
})
|
||||
})
|
||||
|
||||
// Note: deflate is not supported by fasthttp.FS
|
||||
algorithms := []string{"zstd", "gzip", "br"}
|
||||
for _, algo := range algorithms {
|
||||
algo := algo
|
||||
|
||||
t.Run(algo+"_compression", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
req := httptest.NewRequest(MethodGet, "/file", nil)
|
||||
req.Header.Set("Accept-Encoding", algo)
|
||||
resp, err := app.Test(req, 10*time.Second)
|
||||
|
||||
require.NoError(t, err, "app.Test(req)")
|
||||
require.Equal(t, 200, resp.StatusCode, "Status code")
|
||||
require.NotEqual(t, "58726", resp.Header.Get(HeaderContentLength))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_Ctx_SendFile_Compress_CheckCompressed(t *testing.T) {
|
||||
t.Parallel()
|
||||
app := New()
|
||||
|
||||
// fetch file content
|
||||
f, err := os.Open("./ctx.go")
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Cleanup(func() {
|
||||
require.NoError(t, f.Close())
|
||||
})
|
||||
|
||||
expectedFileContent, err := io.ReadAll(f)
|
||||
require.NoError(t, err)
|
||||
|
||||
sendFileBodyReader := func(compression string) []byte {
|
||||
reqCtx := &fasthttp.RequestCtx{}
|
||||
reqCtx.Request.Header.Add(HeaderAcceptEncoding, compression)
|
||||
|
||||
c := app.AcquireCtx(reqCtx)
|
||||
err = c.SendFile("./ctx.go", SendFile{
|
||||
Compress: true,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
return c.Response().Body()
|
||||
}
|
||||
|
||||
t.Run("gzip", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
body, err := fasthttp.AppendGunzipBytes(nil, sendFileBodyReader("gzip"))
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, expectedFileContent, body)
|
||||
})
|
||||
|
||||
t.Run("zstd", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
body, err := fasthttp.AppendUnzstdBytes(nil, sendFileBodyReader("zstd"))
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, expectedFileContent, body)
|
||||
})
|
||||
|
||||
t.Run("br", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
body, err := fasthttp.AppendUnbrotliBytes(nil, sendFileBodyReader("br"))
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, expectedFileContent, body)
|
||||
})
|
||||
}
|
||||
|
||||
//go:embed ctx.go
|
||||
var embedFile embed.FS
|
||||
|
||||
func Test_Ctx_SendFile_EmbedFS(t *testing.T) {
|
||||
t.Parallel()
|
||||
app := New()
|
||||
|
||||
f, err := os.Open("./ctx.go")
|
||||
require.NoError(t, err)
|
||||
|
||||
defer func() {
|
||||
require.NoError(t, f.Close())
|
||||
}()
|
||||
|
||||
expectFileContent, err := io.ReadAll(f)
|
||||
require.NoError(t, err)
|
||||
|
||||
app.Get("/test", func(c Ctx) error {
|
||||
return c.SendFile("ctx.go", SendFile{
|
||||
FS: embedFile,
|
||||
})
|
||||
})
|
||||
|
||||
resp, err := app.Test(httptest.NewRequest(MethodGet, "/test", nil))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, StatusOK, resp.StatusCode)
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, expectFileContent, body)
|
||||
}
|
||||
|
||||
// go test -race -run Test_Ctx_SendFile_404
|
||||
func Test_Ctx_SendFile_404(t *testing.T) {
|
||||
t.Parallel()
|
||||
app := New()
|
||||
app.Get("/", func(c Ctx) error {
|
||||
err := c.SendFile(filepath.FromSlash("john_dow.go/"))
|
||||
require.Error(t, err)
|
||||
return err
|
||||
return c.SendFile("ctx12.go")
|
||||
})
|
||||
|
||||
resp, err := app.Test(httptest.NewRequest(MethodGet, "/", nil))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, StatusNotFound, resp.StatusCode)
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "sendfile: file ctx12.go not found", string(body))
|
||||
}
|
||||
|
||||
func Test_Ctx_SendFile_Multiple(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
app := New()
|
||||
app.Get("/test", func(c Ctx) error {
|
||||
switch c.Query("file") {
|
||||
case "1":
|
||||
return c.SendFile("ctx.go")
|
||||
case "2":
|
||||
return c.SendFile("app.go")
|
||||
case "3":
|
||||
return c.SendFile("ctx.go", SendFile{
|
||||
Download: true,
|
||||
})
|
||||
case "4":
|
||||
return c.SendFile("app_test.go", SendFile{
|
||||
FS: os.DirFS("."),
|
||||
})
|
||||
default:
|
||||
return c.SendStatus(StatusNotFound)
|
||||
}
|
||||
})
|
||||
|
||||
app.Get("/test2", func(c Ctx) error {
|
||||
return c.SendFile("ctx.go", SendFile{
|
||||
Download: true,
|
||||
})
|
||||
})
|
||||
|
||||
testCases := []struct {
|
||||
url string
|
||||
body string
|
||||
contentDisposition string
|
||||
}{
|
||||
{"/test?file=1", "type DefaultCtx struct", ""},
|
||||
{"/test?file=2", "type App struct", ""},
|
||||
{"/test?file=3", "type DefaultCtx struct", "attachment"},
|
||||
{"/test?file=4", "Test_App_MethodNotAllowed", ""},
|
||||
{"/test2", "type DefaultCtx struct", "attachment"},
|
||||
{"/test2", "type DefaultCtx struct", "attachment"},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
resp, err := app.Test(httptest.NewRequest(MethodGet, tc.url, nil))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, StatusOK, resp.StatusCode)
|
||||
require.Equal(t, tc.contentDisposition, resp.Header.Get(HeaderContentDisposition))
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
require.NoError(t, err)
|
||||
require.Contains(t, string(body), tc.body)
|
||||
}
|
||||
|
||||
require.Len(t, app.sendfiles, 3)
|
||||
}
|
||||
|
||||
// go test -race -run Test_Ctx_SendFile_Immutable
|
||||
|
@ -3050,6 +3286,56 @@ func Test_Ctx_SendFile_RestoreOriginalURL(t *testing.T) {
|
|||
require.NoError(t, err2)
|
||||
}
|
||||
|
||||
func Test_SendFile_withRoutes(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
app := New()
|
||||
app.Get("/file", func(c Ctx) error {
|
||||
return c.SendFile("ctx.go")
|
||||
})
|
||||
|
||||
app.Get("/file/download", func(c Ctx) error {
|
||||
return c.SendFile("ctx.go", SendFile{
|
||||
Download: true,
|
||||
})
|
||||
})
|
||||
|
||||
app.Get("/file/fs", func(c Ctx) error {
|
||||
return c.SendFile("ctx.go", SendFile{
|
||||
FS: os.DirFS("."),
|
||||
})
|
||||
})
|
||||
|
||||
resp, err := app.Test(httptest.NewRequest(MethodGet, "/file", nil))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, StatusOK, resp.StatusCode)
|
||||
|
||||
resp, err = app.Test(httptest.NewRequest(MethodGet, "/file/download", nil))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, StatusOK, resp.StatusCode)
|
||||
require.Equal(t, "attachment", resp.Header.Get(HeaderContentDisposition))
|
||||
|
||||
resp, err = app.Test(httptest.NewRequest(MethodGet, "/file/fs", nil))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, StatusOK, resp.StatusCode)
|
||||
}
|
||||
|
||||
func Benchmark_Ctx_SendFile(b *testing.B) {
|
||||
app := New()
|
||||
c := app.AcquireCtx(&fasthttp.RequestCtx{})
|
||||
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
|
||||
var err error
|
||||
for n := 0; n < b.N; n++ {
|
||||
err = c.SendFile("ctx.go")
|
||||
}
|
||||
|
||||
require.NoError(b, err)
|
||||
require.Contains(b, string(c.Response().Body()), "type DefaultCtx struct")
|
||||
}
|
||||
|
||||
// go test -run Test_Ctx_JSON
|
||||
func Test_Ctx_JSON(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
|
|
@ -1689,12 +1689,49 @@ app.Get("/", func(c fiber.Ctx) error {
|
|||
|
||||
Transfers the file from the given path. Sets the [Content-Type](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Type) response HTTP header field based on the **filenames** extension.
|
||||
|
||||
:::caution
|
||||
Method doesn´t use **gzipping** by default, set it to **true** to enable.
|
||||
:::
|
||||
```go title="Config" title="Config"
|
||||
// SendFile defines configuration options when to transfer file with SendFile.
|
||||
type SendFile struct {
|
||||
// FS is the file system to serve the static files from.
|
||||
// You can use interfaces compatible with fs.FS like embed.FS, os.DirFS etc.
|
||||
//
|
||||
// Optional. Default: nil
|
||||
FS fs.FS
|
||||
|
||||
// When set to true, the server tries minimizing CPU usage by caching compressed files.
|
||||
// This works differently than the github.com/gofiber/compression middleware.
|
||||
// You have to set Content-Encoding header to compress the file.
|
||||
// Available compression methods are gzip, br, and zstd.
|
||||
//
|
||||
// Optional. Default value false
|
||||
Compress bool `json:"compress"`
|
||||
|
||||
// When set to true, enables byte range requests.
|
||||
//
|
||||
// Optional. Default value false
|
||||
ByteRange bool `json:"byte_range"`
|
||||
|
||||
// When set to true, enables direct download.
|
||||
//
|
||||
// Optional. Default: false.
|
||||
Download bool `json:"download"`
|
||||
|
||||
// Expiration duration for inactive file handlers.
|
||||
// Use a negative time.Duration to disable it.
|
||||
//
|
||||
// Optional. Default value 10 * time.Second.
|
||||
CacheDuration time.Duration `json:"cache_duration"`
|
||||
|
||||
// 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"`
|
||||
}
|
||||
```
|
||||
|
||||
```go title="Signature" title="Signature"
|
||||
func (c Ctx) SendFile(file string, compress ...bool) error
|
||||
func (c Ctx) SendFile(file string, config ...SendFile) error
|
||||
```
|
||||
|
||||
```go title="Example"
|
||||
|
@ -1702,7 +1739,9 @@ app.Get("/not-found", func(c fiber.Ctx) error {
|
|||
return c.SendFile("./public/404.html");
|
||||
|
||||
// Disable compression
|
||||
return c.SendFile("./static/index.html", false);
|
||||
return c.SendFile("./static/index.html", SendFile{
|
||||
Compress: false,
|
||||
});
|
||||
})
|
||||
```
|
||||
|
||||
|
@ -1717,7 +1756,47 @@ app.Get("/file-with-url-chars", func(c fiber.Ctx) error {
|
|||
```
|
||||
|
||||
:::info
|
||||
For sending files from embedded file system [this functionality](../middleware/static.md#serving-files-using-embedfs) can be used
|
||||
You can set `CacheDuration` config property to `-1` to disable caching.
|
||||
:::
|
||||
|
||||
```go title="Example"
|
||||
app.Get("/file", func(c fiber.Ctx) error {
|
||||
return c.SendFile("style.css", SendFile{
|
||||
CacheDuration: -1,
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
:::info
|
||||
You can use multiple SendFile with different configurations in single route. Fiber creates different filesystem handler per config.
|
||||
:::
|
||||
|
||||
```go title="Example"
|
||||
app.Get("/file", func(c fiber.Ctx) error {
|
||||
switch c.Query("config") {
|
||||
case "filesystem":
|
||||
return c.SendFile("style.css", SendFile{
|
||||
FS: os.DirFS(".")
|
||||
})
|
||||
case "filesystem-compress":
|
||||
return c.SendFile("style.css", SendFile{
|
||||
FS: os.DirFS("."),
|
||||
Compress: true,
|
||||
})
|
||||
case "compress":
|
||||
return c.SendFile("style.css", SendFile{
|
||||
Compress: true,
|
||||
})
|
||||
default:
|
||||
return c.SendFile("style.css")
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
```
|
||||
|
||||
:::info
|
||||
For sending multiple files from embedded file system [this functionality](../middleware/static.md#serving-files-using-embedfs) can be used
|
||||
:::
|
||||
|
||||
## SendStatus
|
||||
|
|
|
@ -219,6 +219,7 @@ DRAFT section
|
|||
* Bind -> for Binding instead of View, us c.ViewBind()
|
||||
* Format -> Param: body interface{} -> handlers ...ResFmt
|
||||
* Redirect -> c.Redirect().To()
|
||||
* SendFile now supports different configurations using the config parameter.
|
||||
|
||||
---
|
||||
|
||||
|
|
Loading…
Reference in New Issue