-
-
Notifications
You must be signed in to change notification settings - Fork 1.9k
✨ v3 (feature): add configuration support to c.SendFile() #3017
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 6 commits
6f4c253
954ab45
366dd56
e4b4b0f
52cdab4
d151a02
e8246f3
ee2d704
fa20ed8
b3673a0
4743fde
be7f0b1
b5bcd0c
011c7d0
129b89d
0750870
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -11,6 +11,7 @@ import ( | |
"errors" | ||
"fmt" | ||
"io" | ||
"io/fs" | ||
"mime/multipart" | ||
"net" | ||
"net/http" | ||
|
@@ -1414,48 +1415,157 @@ func (c *DefaultCtx) Send(body []byte) error { | |
return nil | ||
} | ||
|
||
var ( | ||
sendFileOnce sync.Once | ||
sendFileFS *fasthttp.FS | ||
sendFileHandler fasthttp.RequestHandler | ||
) | ||
// SendFile defines configuration options when to transfer file with SendFileWithConfig. | ||
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. | ||
// 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"` | ||
} | ||
|
||
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 | ||
} | ||
ReneWerner87 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
// 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 | ||
for _, sf := range c.app.sendfiles { | ||
if sf.compareConfig(cfg) { | ||
fsHandler = sf.handler | ||
cacheControlValue = sf.cacheControlValue | ||
break | ||
} | ||
} | ||
|
||
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 { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I know this is very old line of code, but why are we removing the header? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Shouldnt fasthttp itself be checling the header and not serving compressed content if Compress is disabled in fasthttp.FS ? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not sure but fasthttp also seems to add it to servceuncompressed strAcceptEncodinghttps://github.com/valyala/fasthttp/blob/c7576cc10cabfc9c993317a2d3f8355497bea156/fs.go#L55 |
||
// 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,29 +1578,51 @@ 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) | ||
|
||
// 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() | ||
|
||
// 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) | ||
} | ||
// Check for error | ||
if status != StatusNotFound && fsStatus == StatusNotFound { | ||
return NewError(StatusNotFound, fmt.Sprintf("sendfile: file %s not found", filename)) | ||
|
||
// Apply cache control header | ||
if status != StatusNotFound && status != StatusForbidden { | ||
if len(cacheControlValue) > 0 { | ||
c.Context().Response.Header.Set(HeaderCacheControl, cacheControlValue) | ||
} | ||
|
||
return nil | ||
} | ||
|
||
return nil | ||
} | ||
|
||
|
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Uh oh!
There was an error while loading. Please reload this page.