@@ -11,6 +11,7 @@ import (
11
11
"errors"
12
12
"fmt"
13
13
"io"
14
+ "io/fs"
14
15
"mime/multipart"
15
16
"net"
16
17
"net/http"
@@ -69,6 +70,84 @@ type DefaultCtx struct {
69
70
redirectionMessages []string // Messages of the previous redirect
70
71
}
71
72
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
+
72
151
// TLSHandler object
73
152
type TLSHandler struct {
74
153
clientHelloInfo * tls.ClientHelloInfo
@@ -1414,48 +1493,87 @@ func (c *DefaultCtx) Send(body []byte) error {
1414
1493
return nil
1415
1494
}
1416
1495
1417
- var (
1418
- sendFileOnce sync.Once
1419
- sendFileFS * fasthttp.FS
1420
- sendFileHandler fasthttp.RequestHandler
1421
- )
1422
-
1423
1496
// SendFile transfers the file from the given path.
1424
1497
// The file is not compressed by default, enable this by passing a 'true' argument
1425
1498
// 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 {
1427
1500
// Save the filename, we will need it in the error message if the file isn't found
1428
1501
filename := file
1429
1502
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 {
1434
1527
Root : "" ,
1528
+ FS : cfg .FS ,
1435
1529
AllowEmptyRoot : true ,
1436
1530
GenerateIndexPages : false ,
1437
- AcceptByteRange : true ,
1438
- Compress : true ,
1439
- CompressBrotli : true ,
1531
+ AcceptByteRange : cfg . ByteRange ,
1532
+ Compress : cfg . Compress ,
1533
+ CompressBrotli : cfg . Compress ,
1440
1534
CompressedFileSuffixes : c .app .config .CompressedFileSuffixes ,
1441
- CacheDuration : cacheDuration ,
1535
+ CacheDuration : cfg .CacheDuration ,
1536
+ SkipCache : cfg .CacheDuration < 0 ,
1442
1537
IndexNames : []string {"index.html" },
1443
1538
PathNotFound : func (ctx * fasthttp.RequestCtx ) {
1444
1539
ctx .Response .SetStatusCode (StatusNotFound )
1445
1540
},
1446
1541
}
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
+ }
1449
1565
1450
1566
// Keep original path for mutable params
1451
1567
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 {
1454
1571
// https://github.com/valyala/fasthttp/blob/7cc6f4c513f9e0d3686142e0a1a5aa2f76b3194a/fs.go#L55
1455
1572
c .fasthttp .Request .Header .Del (HeaderAcceptEncoding )
1456
1573
}
1574
+
1457
1575
// 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 ) {
1459
1577
// extend relative path to absolute path
1460
1578
hasTrailingSlash := len (file ) > 0 && (file [len (file )- 1 ] == '/' || file [len (file )- 1 ] == '\\' )
1461
1579
@@ -1468,29 +1586,51 @@ func (c *DefaultCtx) SendFile(file string, compress ...bool) error {
1468
1586
file += "/"
1469
1587
}
1470
1588
}
1589
+
1471
1590
// convert the path to forward slashes regardless the OS in order to set the URI properly
1472
1591
// the handler will convert back to OS path separator before opening the file
1473
1592
file = filepath .ToSlash (file )
1474
1593
1475
1594
// Restore the original requested URL
1476
1595
originalURL := utils .CopyString (c .OriginalURL ())
1477
1596
defer c .fasthttp .Request .SetRequestURI (originalURL )
1597
+
1478
1598
// Set new URI for fileHandler
1479
1599
c .fasthttp .Request .SetRequestURI (file )
1600
+
1480
1601
// Save status code
1481
1602
status := c .fasthttp .Response .StatusCode ()
1603
+
1482
1604
// 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
+
1484
1612
// Get the status code which is set by fasthttp
1485
1613
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
+
1486
1620
// Set the status code set by the user if it is different from the fasthttp status code and 200
1487
1621
if status != fsStatus && status != StatusOK {
1488
1622
c .Status (status )
1489
1623
}
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
1493
1632
}
1633
+
1494
1634
return nil
1495
1635
}
1496
1636
0 commit comments