Skip to content
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

✨ v3 (feature): add configuration support to c.SendFile() #3017

Merged
merged 16 commits into from
Jun 30, 2024
5 changes: 5 additions & 0 deletions app.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
efectn marked this conversation as resolved.
Show resolved Hide resolved
Comment on lines +126 to +127
Copy link
Contributor

@coderabbitai coderabbitai bot Jun 27, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Documentation for sendfiles field still needed.

The sendfiles field lacks documentation. Proper documentation is crucial for maintainability and understanding the purpose of the field, especially for public or exported fields.

+	// sendfiles stores configurations for handling ctx.SendFile operations.
	sendfiles []*sendFileStore
Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// sendfiles stores configurations for handling ctx.SendFile operations
sendfiles []*sendFileStore
// sendfiles stores configurations for handling ctx.SendFile operations.
sendfiles []*sendFileStore

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's available above the ctx.SendFile method

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After reviewing the provided content of app.go, it appears that there isn't a direct reference to the sendfiles field in the vicinity of the ctx.SendFile method. It's possible there might be some confusion or miscommunication regarding the location of the documentation. Could you please double-check or clarify where the documentation for the sendfiles field is located? If it's in a different part of the file or in another file, I can assist in locating and reviewing it.

// sendfilesMutex is a mutex used for sendfile operations
sendfilesMutex sync.RWMutex
efectn marked this conversation as resolved.
Show resolved Hide resolved
}

// Config is a struct holding the server settings.
Expand Down Expand Up @@ -440,6 +444,7 @@ func New(config ...Config) *App {
getString: utils.UnsafeString,
latestRoute: &Route{},
customBinders: []CustomBinder{},
sendfiles: []*sendFileStore{},
}

// Create Ctx pool
Expand Down
188 changes: 164 additions & 24 deletions ctx.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"errors"
"fmt"
"io"
"io/fs"
"mime/multipart"
"net"
"net/http"
Expand Down Expand Up @@ -69,6 +70,84 @@
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"`
}
efectn marked this conversation as resolved.
Show resolved Hide resolved

// 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

Check warning on line 129 in ctx.go

View check run for this annotation

Codecov / codecov/patch

ctx.go#L129

Added line #L129 was not covered by tests
}

if sf.config.ByteRange != cfg.ByteRange {
return false

Check warning on line 133 in ctx.go

View check run for this annotation

Codecov / codecov/patch

ctx.go#L133

Added line #L133 was not covered by tests
}

if sf.config.Download != cfg.Download {
return false
}

if sf.config.CacheDuration != cfg.CacheDuration {
return false

Check warning on line 141 in ctx.go

View check run for this annotation

Codecov / codecov/patch

ctx.go#L141

Added line #L141 was not covered by tests
}

if sf.config.MaxAge != cfg.MaxAge {
return false

Check warning on line 145 in ctx.go

View check run for this annotation

Codecov / codecov/patch

ctx.go#L145

Added line #L145 was not covered by tests
}

return true
}

// TLSHandler object
type TLSHandler struct {
clientHelloInfo *tls.ClientHelloInfo
Expand Down Expand Up @@ -1414,48 +1493,87 @@
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 {
Copy link
Member

Choose a reason for hiding this comment

The 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?

Copy link
Member

Choose a reason for hiding this comment

The 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 ?

Copy link
Member Author

Choose a reason for hiding this comment

The 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] == '\\')

Expand All @@ -1468,29 +1586,51 @@
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
}

Expand Down
2 changes: 1 addition & 1 deletion ctx_interface_gen.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading