gin-contrib / static

Static middleware
MIT License
290 stars 41 forks source link

Embed (release go 1.16) implementation #19

Closed vomnes closed 9 months ago

vomnes commented 3 years ago

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()
}
AngangGuo commented 3 years 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.

vomnes commented 3 years ago

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
AngangGuo commented 3 years ago

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

vomnes commented 3 years ago

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

hscells commented 3 years ago

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?

AngangGuo commented 3 years ago

You're right, it works well with plain router. But sometimes we must use this plugin to solve the problems like I mentioned above.

zogot commented 3 years ago

@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

Jamesits commented 3 years ago

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,
    }
}
rockmenjack commented 3 years ago

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
}
rockmenjack commented 3 years ago

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

sipian commented 3 years ago

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

YaroslavPodorvanov commented 3 years ago

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)
}
joshstrohminger commented 2 years ago

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.

MERKAT0R commented 1 year ago

My humble version, maybe usefull for somebody

Dev

//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

//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))
itzwam commented 1 year ago

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
}
soulteary commented 10 months ago

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))
}
appleboy commented 9 months ago

Support in v1.1.0 version, see the example https://github.com/gin-contrib/static/blob/21b6603afc68fb94b5c7959764f9c198eb9cab52/_example/embed/example.go#L1-L27