static
static copied to clipboard
Embed (release go 1.16) implementation
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.
package main
import (
"embed"
"fmt"
"io/fs"
"net/http"
"github.com/gin-contrib/static"
"github.com/gin-gonic/gin"
)
//go:embed server
var server embed.FS
type embedFileSystem struct {
http.FileSystem
}
func (e embedFileSystem) Exists(prefix string, path string) bool {
_, err := e.Open(path)
if err != nil {
return false
}
return true
}
func EmbedFolder(fsEmbed embed.FS, targetPath string) static.ServeFileSystem {
fsys, err := fs.Sub(fsEmbed, targetPath)
if err != nil {
panic(err)
}
return embedFileSystem{
FileSystem: http.FS(fsys),
}
}
func main() {
r := gin.Default()
r.Use(static.Serve("/", EmbedFolder(server, "server/a")))
r.NoRoute(func (c *gin.Context) {
fmt.Println("%s doesn't exists, redirect on /", c.Request.URL.Path)
c.Redirect(http.StatusMovedPermanently, "/")
})
r.Run()
}
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
see: https://pkg.go.dev/io/fs#Stat
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
Ref https://stackoverflow.com/questions/69462376/serving-react-static-files-in-golang-gin-gonic-using-goembed-giving-404-error-o
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
Dev
- serving from normal filesys for ex. "./vue-front/dist"
- fs_dev.go
//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,
}
}
Prod
- embedding & serving from embed filesys
- fs_prod.go
//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
}
Using like
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