-
-
Notifications
You must be signed in to change notification settings - Fork 1.9k
🔥 feat: Add Support for Serving Embedded Static Files #3448
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
base: v2
Are you sure you want to change the base?
Changes from all commits
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 |
---|---|---|
|
@@ -14,6 +14,7 @@ import ( | |
"encoding/xml" | ||
"errors" | ||
"fmt" | ||
"io/fs" | ||
"net" | ||
"net/http" | ||
"net/http/httputil" | ||
|
@@ -780,6 +781,14 @@ func (app *App) Static(prefix, root string, config ...Static) Router { | |
return app | ||
} | ||
|
||
// StaticFilesystem Supports Serving Static Resources from an Embed.FS File System | ||
// | ||
// The filesystem parameter can directly accept an embed.FS instance. | ||
func (app *App) StaticFilesystem(prefix string, filesystem fs.FS, config ...Static) Router { | ||
app.registerStaticFS(prefix, filesystem, config...) | ||
return app | ||
} | ||
Comment on lines
+784
to
+790
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. 💡 Verification agent 🧩 Analysis chainExpose the new helper through the public Router interface
The omission also means type Router interface {
…
Static(prefix, root string, config ...Static) Router
+ StaticFilesystem(prefix string, filesystem fs.FS, config ...Static) Router
…
} After extending the interface, forwarders for func (g *Group) StaticFilesystem(p string, fs fs.FS, cfg ...Static) Router {
return g.app.StaticFilesystem(getGroupPath(g.Prefix, p), fs, cfg...)
} 🏁 Script executed: #!/bin/bash
set -e
# Locate the Router interface declaration and its methods
rg "type Router interface" -A 20
# Confirm if StaticFilesystem is already declared in router.go
rg "StaticFilesystem" -n router.go || echo "No StaticFilesystem in router.go"
# Inspect Group type and existing Static methods
rg "type Group" -A 10
rg "func .*Group\).*Static" -n Length of output: 1560 Expose StaticFilesystem on the Router interface and Group The new var r fiber.Router = app
r.StaticFilesystem("/assets", myFS) // does not compile Please add: • router.go (within type Router interface {
…
Static(prefix, root string, config ...Static) Router
+ StaticFilesystem(prefix string, filesystem fs.FS, config ...Static) Router
…
} • group.go (alongside func (grp *Group) StaticFilesystem(p string, fs fs.FS, cfg ...Static) Router {
return grp.app.StaticFilesystem(getGroupPath(grp.Prefix, p), fs, cfg...)
} |
||
|
||
// All will register the handler on all HTTP methods | ||
func (app *App) All(path string, handlers ...Handler) Router { | ||
for _, method := range app.config.RequestMethods { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -7,6 +7,7 @@ package fiber | |
import ( | ||
"fmt" | ||
"html" | ||
iofs "io/fs" | ||
"sort" | ||
"strconv" | ||
"strings" | ||
|
@@ -444,6 +445,125 @@ func (app *App) registerStatic(prefix, root string, config ...Static) { | |
app.addRoute(MethodHead, &route) | ||
} | ||
|
||
func (app *App) registerStaticFS(prefix string, filesystem iofs.FS, config ...Static) { | ||
if prefix == "" { | ||
prefix = "/" | ||
} | ||
if prefix[0] != '/' { | ||
prefix = "/" + prefix | ||
} | ||
// in case-sensitive routing, all to lowercase | ||
if !app.config.CaseSensitive { | ||
prefix = utils.ToLower(prefix) | ||
} | ||
|
||
prefixLen := len(prefix) | ||
isStar := strings.Contains(prefix, "*") | ||
if isStar { | ||
prefix = strings.Split(prefix, "*")[0] | ||
prefixLen = len(prefix) | ||
} | ||
isRoot := prefix == "/" | ||
|
||
// add embed fs support | ||
fsHandler := &fasthttp.FS{ | ||
FS: filesystem, | ||
Root: ".", | ||
GenerateIndexPages: false, | ||
AcceptByteRange: false, | ||
Compress: false, | ||
CompressedFileSuffix: app.config.CompressedFileSuffix, | ||
CacheDuration: 10 * time.Second, | ||
IndexNames: []string{"index.html"}, | ||
PathRewrite: func(fctx *fasthttp.RequestCtx) []byte { | ||
path := fctx.Path() | ||
if len(path) >= prefixLen { | ||
if isStar && app.getString(path[0:prefixLen]) == prefix { | ||
path = append(path[0:0], '/') | ||
} else { | ||
path = path[prefixLen:] | ||
if len(path) == 0 || path[len(path)-1] != '/' { | ||
path = append(path, '/') | ||
} | ||
} | ||
} | ||
if len(path) > 0 && path[0] != '/' { | ||
path = append([]byte("/"), path...) | ||
} | ||
return path | ||
}, | ||
PathNotFound: func(fctx *fasthttp.RequestCtx) { | ||
fctx.Response.SetStatusCode(StatusNotFound) | ||
}, | ||
} | ||
|
||
// Set config if provided | ||
var cacheControlValue string | ||
var modifyResponse Handler | ||
if len(config) > 0 { | ||
maxAge := config[0].MaxAge | ||
if maxAge > 0 { | ||
cacheControlValue = "public, max-age=" + strconv.Itoa(maxAge) | ||
} | ||
fsHandler.CacheDuration = config[0].CacheDuration | ||
fsHandler.Compress = config[0].Compress | ||
fsHandler.AcceptByteRange = config[0].ByteRange | ||
fsHandler.GenerateIndexPages = config[0].Browse | ||
if config[0].Index != "" { | ||
fsHandler.IndexNames = []string{config[0].Index} | ||
} | ||
modifyResponse = config[0].ModifyResponse | ||
} | ||
|
||
fileHandler := fsHandler.NewRequestHandler() | ||
handler := func(c *Ctx) error { | ||
// Don't execute middleware if Next returns true | ||
if len(config) != 0 && config[0].Next != nil && config[0].Next(c) { | ||
return c.Next() | ||
} | ||
// Serve file | ||
fileHandler(c.fasthttp) | ||
// Sets the response Content-Disposition header to attachment if the Download option is true | ||
if len(config) > 0 && config[0].Download { | ||
c.Attachment() | ||
} | ||
// Return request if found and not forbidden | ||
status := c.fasthttp.Response.StatusCode() | ||
if status != StatusNotFound && status != StatusForbidden { | ||
if len(cacheControlValue) > 0 { | ||
c.fasthttp.Response.Header.Set(HeaderCacheControl, cacheControlValue) | ||
} | ||
if modifyResponse != nil { | ||
return modifyResponse(c) | ||
} | ||
return nil | ||
} | ||
// Reset response to default | ||
c.fasthttp.SetContentType("") // Issue #420 | ||
c.fasthttp.Response.SetStatusCode(StatusOK) | ||
c.fasthttp.Response.SetBodyString("") | ||
// Next middleware | ||
return c.Next() | ||
} | ||
Comment on lines
+520
to
+547
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. Missing nil-filesystem validation If a caller accidentally passes a nil if filesystem == nil {
panic("StaticFilesystem: filesystem cannot be nil")
} Fail fast with a clear message rather than a runtime panic deep inside |
||
|
||
// Create route metadata without pointer | ||
route := Route{ | ||
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. |
||
use: true, | ||
root: isRoot, | ||
path: prefix, | ||
// Public data | ||
Method: MethodGet, | ||
Path: prefix, | ||
Handlers: []Handler{handler}, | ||
} | ||
Comment on lines
+551
to
+558
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. 🛠️ Refactor suggestion Route metadata sets
Set |
||
// Increment global handler count | ||
atomic.AddUint32(&app.handlersCount, 1) | ||
// Add route to stack | ||
app.addRoute(MethodGet, &route) | ||
// Add HEAD route | ||
app.addRoute(MethodHead, &route) | ||
} | ||
|
||
func (app *App) addRoute(method string, route *Route, isMounted ...bool) { | ||
// Check mounted routes | ||
var mounted bool | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[nitpick] The doc comment should follow Go conventions and include usage examples. For example:
Copilot uses AI. Check for mistakes.