Closed vomnes closed 9 months ago
Yes, I just found this from https://github.com/gin-gonic/gin/issues/75 It will be great if you can implement this feature. Great thanks.
I have just implemented this feature but I cannot push on my branch
.
Do you know what I should do ? Do I need to be granted on the repository ?
>> git push origin feat-embed-issue-19
remote: Permission to gin-contrib/static.git denied to vomnes.
fatal: unable to access 'https://github.com/gin-contrib/static.git/': The requested URL returned error: 403
I think you need to create a pull request and the admin will decide if they accept it. https://docs.github.com/en/github/collaborating-with-issues-and-pull-requests/creating-a-pull-request
Ah yes, thank you. I remember, I need to create the pull request from a fork.
https://docs.github.com/en/github/collaborating-with-issues-and-pull-requests/creating-a-pull-request-from-a-fork
I also wanted to serve my static files using the new embed feature. I haven't seen anything on the internet apart from this thread discussing it.
For anyone interested, this is how I did it without using this static plugin:
//go:embed web/static/*
var staticFS embed.FS
...
router.GET("/static/*filepath", func(c *gin.Context) {
c.FileFromFS(path.Join("/web/", c.Request.URL.Path), http.FS(staticFS))
})
Maybe you will find this interesting?
You're right, it works well with plain router. But sometimes we must use this plugin to solve the problems like I mentioned above.
@vomnes i think a slight update could be to do fs.Stat istead of e.Open() and then really it could probably be reduced down to a single ServeFileSystem implementation of Filesystem as its not really needed to be seen as only from a embeded filesystem
Added the ability to disable dir listing (indexing), if anyone needs:
(Replace these codes from the first post)
type embedFileSystem struct {
http.FileSystem
indexes bool
}
func (e embedFileSystem) Exists(prefix string, path string) bool {
f, err := e.Open(path)
if err != nil {
return false
}
// check if indexing is allowed
s, _ := f.Stat()
if s.IsDir() && !e.indexes {
return false
}
return true
}
func EmbedFolder(fsEmbed embed.FS, targetPath string, index bool) static.ServeFileSystem {
subFS, err := fs.Sub(fsEmbed, targetPath)
if err != nil {
panic(err)
}
return embedFileSystem{
FileSystem: http.FS(subFS),
indexes: index,
}
}
Will this implementation incur some performance penalties for each static file accessed?
func (e embedFileSystem) Exists(prefix string, path string) bool {
f, err := e.Open(path)
if err != nil {
return false
}
// check if indexing is allowed
s, _ := f.Stat()
if s.IsDir() && !e.indexes {
return false
}
return true
}
Added the ability to disable dir listing (indexing), if anyone needs:
// check if indexing is allowed s, _ := f.Stat() if s.IsDir() && !e.indexes { return false }
You actually can not control if indexing is allowed, because in net/http/fs.go, directory is already omited:
func serveFile(w ResponseWriter, r *Request, fs FileSystem, name string, redirect bool) {
...
if redirect {
// redirect to canonical path: / at end of directory url
// r.URL.Path always begins with /
url := r.URL.Path
if d.IsDir() {
if url[len(url)-1] != '/' {
localRedirect(w, r, path.Base(url)+"/")
return
}
} else {
if url[len(url)-1] == '/' {
localRedirect(w, r, "../"+path.Base(url))
return
}
}
}
...
and github.com/gin-contrib/static.Serve
will call the above with redirect=true
I am trying this approach for a SPA app built using react. My frontend files are in a build folder
build/index.html
build/asset-manifest.json
build/static/css/**
build/static/js/**
build/manifest.json
//go:embed build/*
var reactStatic embed.FS
type embedFileSystem struct {
http.FileSystem
indexes bool
}
func (e embedFileSystem) Exists(prefix string, path string) bool {
f, err := e.Open(path)
if err != nil {
return false
}
// check if indexing is allowed
s, _ := f.Stat()
if s.IsDir() && !e.indexes {
return false
}
return true
}
func EmbedFolder(fsEmbed embed.FS, targetPath string, index bool) static.ServeFileSystem {
subFS, err := fs.Sub(fsEmbed, targetPath)
if err != nil {
panic(err)
}
return embedFileSystem{
FileSystem: http.FS(subFS),
indexes: index,
}
}
func main() {
router := gin.Default()
fs := EmbedFolder(reactStatic, "build", true)
//Serve frontend static files
router.Use(static.Serve("/", fs))
/* THESE ARE MY STATIC URLs FROM THE REACT APP in FRONTEND */
router.Use(static.Serve("/login", fs))
router.Use(static.Serve("/calendar", fs))
router.NoRoute(func(c *gin.Context) {
c.JSON(404, gin.H{
"code": "PAGE_NOT_FOUND", "message": "Page not found",
})
})
setupBaseRoutes(router, database)
httpServerExitDone := &sync.WaitGroup{}
httpServerExitDone.Add(1)
srv, ln := server.StartServer(router, httpServerExitDone)
log.Printf("Starting Server at %s", ln.Addr().String())
quit := make(chan os.Signal)
signal.Notify(quit, os.Interrupt)
<-quit
log.Println("Shutdown Server ...")
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := srv.Shutdown(ctx); err != nil {
log.Fatal("Server Shutdown:", err)
}
log.Println("Server exiting")
}
When the application loads and the page http://localhost:8000/
, opens properly and I can navigate to http://localhost:8000/calendar
using react-navigation.
But when I reload the page http://localhost:8000/calendar
, I get 404 error.
-----------------------------------------------------
[EDIT 14-04-2022]
I managed to find a workaround by renaming build/index.html
to build/index.htm
Before:
package main
import (
"github.com/gin-gonic/gin"
"net/http"
)
func main() {
router := gin.Default()
router.StaticFS("/", http.Dir("public"))
// Listen and serve on 0.0.0.0:8080
router.Run(":8080")
}
After:
package main
import (
"embed"
"io/fs"
"net/http"
)
import "github.com/gin-gonic/gin"
//go:embed public
var staticFS embed.FS
func main() {
router := gin.Default()
router.StaticFS("/", mustFS())
// Listen and serve on 0.0.0.0:8080
router.Run(":8080")
}
func mustFS() http.FileSystem {
sub, err := fs.Sub(staticFS, "public")
if err != nil {
panic(err)
}
return http.FS(sub)
}
Thanks everyone for your examples. I was able to get this working for my SPA, serving from a wwwroot embedded directory with a minor hack in the NoRoute
handler to always return index.html
. I was originally simply trying to do:
//go:embed wwwroot
var app embed.FS
wwwroot := embedFolder(app, "wwwroot")
router.Use(static.Serve("/", wwwroot))
router.NoRoute(func(c *gin.Context) {
c.FileFromFS("index.html", wwwroot)
})
but this doesn't play well with how the http.serveFile function always performs a local redirect to "/"
when the path ends with "/index.html"
. So instead of "index.html"
, I tried ""
, "/"
, "wwwroot"
, and "wwwroot/"
, but all of those failed because that wasn't actually a file in the embedded file system.
My solution was to re-write the request URL to the default empty path and re-use the static.Serve
middleware since it can handle the "/"
path by calling it manually:
wwwroot := embedFolder(app, "wwwroot")
staticServer := static.Serve("/", wwwroot)
router.Use(staticServer)
router.NoRoute(func(c *gin.Context) {
if c.Request.Method == http.MethodGet &&
!strings.ContainsRune(c.Request.URL.Path, '.') &&
!strings.HasPrefix(c.Request.URL.Path, "/api/") {
c.Request.URL.Path = "/"
staticServer(c)
}
})
Note that I'm only doing this for GET
requests that don't contain a '.'
or start with my API prefix so I should still get a 404
error for API routes and files that don't exist, like if I used a bad image path.
My humble version, maybe usefull for somebody
//go:build !prod <--- non prod tag provided - regular "go build"
package utils
import (
"fmt"
"github.com/gin-contrib/static"
"github.com/gin-gonic/gin"
"net/http"
"os"
"path"
"strings"
)
const INDEX = "index.html"
type LocalFileSystem struct {
http.FileSystem
root string
indexes bool
}
func (l *LocalFileSystem) Exists(prefix string, filepath string) bool {
if p := strings.TrimPrefix(filepath, prefix); len(p) < len(filepath) {
name := path.Join(l.root, p)
stats, err := os.Stat(name)
if err != nil {
return false
}
if stats.IsDir() {
if !l.indexes {
index := path.Join(name, INDEX)
_, err := os.Stat(index)
if err != nil {
return false
}
}
}
return true
}
return false
}
func GetFrontendAssets(indexing bool) static.ServeFileSystem {
distDir := fmt.Sprint(os.Getenv("FRONTEND_DIR")) //<--- Get front dir from ENV
return &LocalFileSystem{
FileSystem: gin.Dir(distDir, indexing),
root: distDir,
indexes: indexing,
}
}
//go:build prod <--- go build -tags prod
package utils
import (
"embed"
"github.com/gin-contrib/static"
"io/fs"
"net/http"
"strings"
)
//go:embed front-embedded-dir
var embedFrontend embed.FS
type embedFileSystem struct {
http.FileSystem
indexes bool
}
const INDEX = "index.html"
func GetFrontendAssets(indexing bool) static.ServeFileSystem {
f, err := fs.Sub(embedFrontend, "front-embedded-dir")
if err != nil {
panic(err)
}
return &embedFileSystem{
FileSystem: http.FS(f),
indexes: indexing,
}
}
func (e *embedFileSystem) Exists(prefix string, filepath string) bool {
if p := strings.TrimPrefix(filepath, prefix); len(p) < len(filepath) {
f, err := e.Open(filepath)
if err != nil {
return false
}
stats, _ := f.Stat()
if stats.IsDir() {
if !e.indexes {
_, err = e.FileSystem.Open(INDEX)
if err != nil {
return false
}
}
}
return true
}
return false
}
frontend := utils.GetFrontendAssets(false)
route.Use(static.Serve("/", frontend))
My humble version, maybe usefull for somebody
Achieved a quite similar setup on my side (keeping the embedFs in the struct to re-use it in io/fs.Stat call later) before realising an issue was already opened
something like that :
package static
import (
"log"
"net/http"
"path"
"strings"
iofs "io/fs"
)
type embedFileSystem struct {
http.FileSystem
fs iofs.FS
root string
indexes bool
}
func EmbedFS(root string, indexes bool, fileSystem iofs.FS) *embedFileSystem {
subFS, err := iofs.Sub(fileSystem, root)
if err != nil {
log.Fatal("Failed to create subdirectory object:", err)
}
return &embedFileSystem{
FileSystem: http.FS(subFS),
fs: fileSystem,
root: root,
indexes: indexes,
}
}
func (l *embedFileSystem) Exists(prefix string, filepath string) bool {
if p := strings.TrimPrefix(filepath, prefix); len(p) < len(filepath) {
name := path.Join(l.root, p)
stats, err := iofs.Stat(l.fs, name)
if err != nil {
return false
}
if stats.IsDir() {
if !l.indexes {
index := path.Join(name, INDEX)
_, err := iofs.Stat(l.fs, index)
if err != nil {
return false
}
}
}
return true
}
return false
}
I made a new version based on @vomnes 🍻 and released it. The original interface remains consistent with this project.
so it supports both local files and Go Embed (you can create a single executable file without having to deal with file packaging)
project: https://github.com/soulteary/gin-static
download the middleware:
go get github.com/soulteary/gin-static
use local files:
package main
import (
["log"](https://pkg.go.dev/log)
static "github.com/soulteary/gin-static"
https://github.com/gin-gonic/gin
)
func main() {
r := gin.Default()
// if Allow DirectoryIndex
// r.Use(static.Serve("/", static.LocalFile("./public", true)))
// set prefix
// r.Use(static.Serve("/static", static.LocalFile("./public", true)))
r.Use(static.Serve("/", static.LocalFile("./public", false)))
r.GET("/ping", func(c *gin.Context) {
c.String(200, "test")
})
// Listen and Server in 0.0.0.0:8080
if err := r.Run(":8080"); err != nil {
log.Fatal(err)
}
}
use embed files:
package main
import (
["embed"](https://pkg.go.dev/embed)
["fmt"](https://pkg.go.dev/fmt)
["net/http"](https://pkg.go.dev/net/http)
https://github.com/gin-gonic/gin
)
//go:embed public
var EmbedFS embed.FS
func main() {
r := gin.Default()
// method 1: use as Gin Router
// trim embedfs path `public/page`, and use it as url path `/`
// r.GET("/", static.ServeEmbed("public/page", EmbedFS))
// method 2: use as middleware
// trim embedfs path `public/page`, the embedfs path start with `/`
// r.Use(static.ServeEmbed("public/page", EmbedFS))
// method 2.1: use as middleware
// trim embedfs path `public/page`, the embedfs path start with `/public/page`
// r.Use(static.ServeEmbed("", EmbedFS))
// method 3: use as manual
// trim embedfs path `public/page`, the embedfs path start with `/public/page`
// staticFiles, err := static.EmbedFolder(EmbedFS, "public/page")
// if err != nil {
// log.Fatalln("initialization of embed folder failed:", err)
// } else {
// r.Use(static.Serve("/", staticFiles))
// }
r.GET("/ping", func(c *gin.Context) {
c.String(200, "test")
})
r.NoRoute(func(c *gin.Context) {
fmt.Printf("%s doesn't exists, redirect on /\n", c.Request.URL.Path)
c.Redirect(http.StatusMovedPermanently, "/")
})
// Listen and Server in 0.0.0.0:8080
r.Run(":8080")
}
or both use local and embed file, and as those files as fallback (you can overwrite the routes):
if debugMode {
r.Use(static.Serve("/", static.LocalFile("public", false)))
} else {
r.NoRoute(
// your code ...
func(c *gin.Context) {
if c.Request.URL.Path == "/somewhere/" {
c.Data(http.StatusOK, "text/html; charset=utf-8", []byte("custom as you like"))
c.Abort()
}
},
static.ServeEmbed("public", EmbedFS),
)
// no need to block some request path before request static files
// r.NoRoute(static.ServeEmbed("public", EmbedFS))
}
Support in v1.1.0 version, see the example https://github.com/gin-contrib/static/blob/21b6603afc68fb94b5c7959764f9c198eb9cab52/_example/embed/example.go#L1-L27
The embed feature has been released with
go 1.16
, this is my implement in gin using this plugin.Let me know if there are some
improvement
to do.If people are interested with this feature in static I can do a
pull request
.