Skip to content

Commit 21ede59

Browse files
efectngaby
andauthored
✨ 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 <[email protected]>
1 parent 83731ce commit 21ede59

File tree

6 files changed

+545
-34
lines changed

6 files changed

+545
-34
lines changed

app.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,10 @@ type App struct {
123123
configured Config
124124
// customConstraints is a list of external constraints
125125
customConstraints []CustomConstraint
126+
// sendfiles stores configurations for handling ctx.SendFile operations
127+
sendfiles []*sendFileStore
128+
// sendfilesMutex is a mutex used for sendfile operations
129+
sendfilesMutex sync.RWMutex
126130
}
127131

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

445450
// Create Ctx pool

ctx.go

Lines changed: 164 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"errors"
1212
"fmt"
1313
"io"
14+
"io/fs"
1415
"mime/multipart"
1516
"net"
1617
"net/http"
@@ -69,6 +70,84 @@ type DefaultCtx struct {
6970
redirectionMessages []string // Messages of the previous redirect
7071
}
7172

73+
// SendFile defines configuration options when to transfer file with SendFile.
74+
type SendFile struct {
75+
// FS is the file system to serve the static files from.
76+
// You can use interfaces compatible with fs.FS like embed.FS, os.DirFS etc.
77+
//
78+
// Optional. Default: nil
79+
FS fs.FS
80+
81+
// When set to true, the server tries minimizing CPU usage by caching compressed files.
82+
// This works differently than the github.com/gofiber/compression middleware.
83+
// You have to set Content-Encoding header to compress the file.
84+
// Available compression methods are gzip, br, and zstd.
85+
//
86+
// Optional. Default value false
87+
Compress bool `json:"compress"`
88+
89+
// When set to true, enables byte range requests.
90+
//
91+
// Optional. Default value false
92+
ByteRange bool `json:"byte_range"`
93+
94+
// When set to true, enables direct download.
95+
//
96+
// Optional. Default: false.
97+
Download bool `json:"download"`
98+
99+
// Expiration duration for inactive file handlers.
100+
// Use a negative time.Duration to disable it.
101+
//
102+
// Optional. Default value 10 * time.Second.
103+
CacheDuration time.Duration `json:"cache_duration"`
104+
105+
// The value for the Cache-Control HTTP-header
106+
// that is set on the file response. MaxAge is defined in seconds.
107+
//
108+
// Optional. Default value 0.
109+
MaxAge int `json:"max_age"`
110+
}
111+
112+
// sendFileStore is used to keep the SendFile configuration and the handler.
113+
type sendFileStore struct {
114+
handler fasthttp.RequestHandler
115+
config SendFile
116+
cacheControlValue string
117+
}
118+
119+
// compareConfig compares the current SendFile config with the new one
120+
// and returns true if they are different.
121+
//
122+
// Here we don't use reflect.DeepEqual because it is quite slow compared to manual comparison.
123+
func (sf *sendFileStore) compareConfig(cfg SendFile) bool {
124+
if sf.config.FS != cfg.FS {
125+
return false
126+
}
127+
128+
if sf.config.Compress != cfg.Compress {
129+
return false
130+
}
131+
132+
if sf.config.ByteRange != cfg.ByteRange {
133+
return false
134+
}
135+
136+
if sf.config.Download != cfg.Download {
137+
return false
138+
}
139+
140+
if sf.config.CacheDuration != cfg.CacheDuration {
141+
return false
142+
}
143+
144+
if sf.config.MaxAge != cfg.MaxAge {
145+
return false
146+
}
147+
148+
return true
149+
}
150+
72151
// TLSHandler object
73152
type TLSHandler struct {
74153
clientHelloInfo *tls.ClientHelloInfo
@@ -1414,48 +1493,87 @@ func (c *DefaultCtx) Send(body []byte) error {
14141493
return nil
14151494
}
14161495

1417-
var (
1418-
sendFileOnce sync.Once
1419-
sendFileFS *fasthttp.FS
1420-
sendFileHandler fasthttp.RequestHandler
1421-
)
1422-
14231496
// SendFile transfers the file from the given path.
14241497
// The file is not compressed by default, enable this by passing a 'true' argument
14251498
// Sets the Content-Type response HTTP header field based on the filenames extension.
1426-
func (c *DefaultCtx) SendFile(file string, compress ...bool) error {
1499+
func (c *DefaultCtx) SendFile(file string, config ...SendFile) error {
14271500
// Save the filename, we will need it in the error message if the file isn't found
14281501
filename := file
14291502

1430-
// https://github.com/valyala/fasthttp/blob/c7576cc10cabfc9c993317a2d3f8355497bea156/fs.go#L129-L134
1431-
sendFileOnce.Do(func() {
1432-
const cacheDuration = 10 * time.Second
1433-
sendFileFS = &fasthttp.FS{
1503+
var cfg SendFile
1504+
if len(config) > 0 {
1505+
cfg = config[0]
1506+
}
1507+
1508+
if cfg.CacheDuration == 0 {
1509+
cfg.CacheDuration = 10 * time.Second
1510+
}
1511+
1512+
var fsHandler fasthttp.RequestHandler
1513+
var cacheControlValue string
1514+
1515+
c.app.sendfilesMutex.RLock()
1516+
for _, sf := range c.app.sendfiles {
1517+
if sf.compareConfig(cfg) {
1518+
fsHandler = sf.handler
1519+
cacheControlValue = sf.cacheControlValue
1520+
break
1521+
}
1522+
}
1523+
c.app.sendfilesMutex.RUnlock()
1524+
1525+
if fsHandler == nil {
1526+
fasthttpFS := &fasthttp.FS{
14341527
Root: "",
1528+
FS: cfg.FS,
14351529
AllowEmptyRoot: true,
14361530
GenerateIndexPages: false,
1437-
AcceptByteRange: true,
1438-
Compress: true,
1439-
CompressBrotli: true,
1531+
AcceptByteRange: cfg.ByteRange,
1532+
Compress: cfg.Compress,
1533+
CompressBrotli: cfg.Compress,
14401534
CompressedFileSuffixes: c.app.config.CompressedFileSuffixes,
1441-
CacheDuration: cacheDuration,
1535+
CacheDuration: cfg.CacheDuration,
1536+
SkipCache: cfg.CacheDuration < 0,
14421537
IndexNames: []string{"index.html"},
14431538
PathNotFound: func(ctx *fasthttp.RequestCtx) {
14441539
ctx.Response.SetStatusCode(StatusNotFound)
14451540
},
14461541
}
1447-
sendFileHandler = sendFileFS.NewRequestHandler()
1448-
})
1542+
1543+
if cfg.FS != nil {
1544+
fasthttpFS.Root = "."
1545+
}
1546+
1547+
sf := &sendFileStore{
1548+
config: cfg,
1549+
handler: fasthttpFS.NewRequestHandler(),
1550+
}
1551+
1552+
maxAge := cfg.MaxAge
1553+
if maxAge > 0 {
1554+
sf.cacheControlValue = "public, max-age=" + strconv.Itoa(maxAge)
1555+
}
1556+
1557+
// set vars
1558+
fsHandler = sf.handler
1559+
cacheControlValue = sf.cacheControlValue
1560+
1561+
c.app.sendfilesMutex.Lock()
1562+
c.app.sendfiles = append(c.app.sendfiles, sf)
1563+
c.app.sendfilesMutex.Unlock()
1564+
}
14491565

14501566
// Keep original path for mutable params
14511567
c.pathOriginal = utils.CopyString(c.pathOriginal)
1452-
// Disable compression
1453-
if len(compress) == 0 || !compress[0] {
1568+
1569+
// Delete the Accept-Encoding header if compression is disabled
1570+
if !cfg.Compress {
14541571
// https://github.com/valyala/fasthttp/blob/7cc6f4c513f9e0d3686142e0a1a5aa2f76b3194a/fs.go#L55
14551572
c.fasthttp.Request.Header.Del(HeaderAcceptEncoding)
14561573
}
1574+
14571575
// copy of https://github.com/valyala/fasthttp/blob/7cc6f4c513f9e0d3686142e0a1a5aa2f76b3194a/fs.go#L103-L121 with small adjustments
1458-
if len(file) == 0 || !filepath.IsAbs(file) {
1576+
if len(file) == 0 || (!filepath.IsAbs(file) && cfg.FS == nil) {
14591577
// extend relative path to absolute path
14601578
hasTrailingSlash := len(file) > 0 && (file[len(file)-1] == '/' || file[len(file)-1] == '\\')
14611579

@@ -1468,29 +1586,51 @@ func (c *DefaultCtx) SendFile(file string, compress ...bool) error {
14681586
file += "/"
14691587
}
14701588
}
1589+
14711590
// convert the path to forward slashes regardless the OS in order to set the URI properly
14721591
// the handler will convert back to OS path separator before opening the file
14731592
file = filepath.ToSlash(file)
14741593

14751594
// Restore the original requested URL
14761595
originalURL := utils.CopyString(c.OriginalURL())
14771596
defer c.fasthttp.Request.SetRequestURI(originalURL)
1597+
14781598
// Set new URI for fileHandler
14791599
c.fasthttp.Request.SetRequestURI(file)
1600+
14801601
// Save status code
14811602
status := c.fasthttp.Response.StatusCode()
1603+
14821604
// Serve file
1483-
sendFileHandler(c.fasthttp)
1605+
fsHandler(c.fasthttp)
1606+
1607+
// Sets the response Content-Disposition header to attachment if the Download option is true
1608+
if cfg.Download {
1609+
c.Attachment()
1610+
}
1611+
14841612
// Get the status code which is set by fasthttp
14851613
fsStatus := c.fasthttp.Response.StatusCode()
1614+
1615+
// Check for error
1616+
if status != StatusNotFound && fsStatus == StatusNotFound {
1617+
return NewError(StatusNotFound, fmt.Sprintf("sendfile: file %s not found", filename))
1618+
}
1619+
14861620
// Set the status code set by the user if it is different from the fasthttp status code and 200
14871621
if status != fsStatus && status != StatusOK {
14881622
c.Status(status)
14891623
}
1490-
// Check for error
1491-
if status != StatusNotFound && fsStatus == StatusNotFound {
1492-
return NewError(StatusNotFound, fmt.Sprintf("sendfile: file %s not found", filename))
1624+
1625+
// Apply cache control header
1626+
if status != StatusNotFound && status != StatusForbidden {
1627+
if len(cacheControlValue) > 0 {
1628+
c.Context().Response.Header.Set(HeaderCacheControl, cacheControlValue)
1629+
}
1630+
1631+
return nil
14931632
}
1633+
14941634
return nil
14951635
}
14961636

ctx_interface_gen.go

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)