Implement loading HTML from http.FileSystem

This commit is contained in:
sunshineplan 2024-09-10 10:11:11 +08:00
parent 28e57f58b1
commit 3df1d433b3
6 changed files with 161 additions and 6 deletions

View File

@ -1341,13 +1341,17 @@ func main() {
### HTML rendering ### HTML rendering
Using LoadHTMLGlob() or LoadHTMLFiles() Using LoadHTMLGlob() or LoadHTMLFiles() or LoadHTMLFS()
```go ```go
//go:embed templates/*
var templates embed.FS
func main() { func main() {
router := gin.Default() router := gin.Default()
router.LoadHTMLGlob("templates/*") router.LoadHTMLGlob("templates/*")
//router.LoadHTMLFiles("templates/template1.html", "templates/template2.html") //router.LoadHTMLFiles("templates/template1.html", "templates/template2.html")
//router.LoadHTMLFS(http.FS(templates), "templates/template1.html", "templates/template2.html")
router.GET("/index", func(c *gin.Context) { router.GET("/index", func(c *gin.Context) {
c.HTML(http.StatusOK, "index.tmpl", gin.H{ c.HTML(http.StatusOK, "index.tmpl", gin.H{
"title": "Main website", "title": "Main website",

17
fs.go
View File

@ -5,6 +5,7 @@
package gin package gin
import ( import (
"io/fs"
"net/http" "net/http"
"os" "os"
) )
@ -25,6 +26,22 @@ func (o OnlyFilesFS) Open(name string) (http.File, error) {
return neutralizedReaddirFile{f}, nil return neutralizedReaddirFile{f}, nil
} }
// OnlyHTMLFS implements an [fs.FS].
type OnlyHTMLFS struct {
FileSystem http.FileSystem
}
// Open passes `Open` to the upstream implementation and return an [fs.File].
func (o OnlyHTMLFS) Open(name string) (fs.File, error) {
f, err := o.FileSystem.Open(name)
if err != nil {
return nil, err
}
return fs.File(f), nil
}
// neutralizedReaddirFile wraps http.File with a specific implementation of `Readdir`. // neutralizedReaddirFile wraps http.File with a specific implementation of `Readdir`.
type neutralizedReaddirFile struct { type neutralizedReaddirFile struct {
http.File http.File

12
gin.go
View File

@ -285,6 +285,18 @@ func (engine *Engine) LoadHTMLFiles(files ...string) {
engine.SetHTMLTemplate(templ) engine.SetHTMLTemplate(templ)
} }
// LoadHTMLFS loads an http.FileSystem and a slice of patterns
// and associates the result with HTML renderer.
func (engine *Engine) LoadHTMLFS(fs http.FileSystem, patterns ...string) {
if IsDebugging() {
engine.HTMLRender = render.HTMLDebug{FS: OnlyHTMLFS{fs}, Patterns: patterns, FuncMap: engine.FuncMap, Delims: engine.delims}
return
}
templ := template.Must(template.New("").Delims(engine.delims.Left, engine.delims.Right).Funcs(engine.FuncMap).ParseFS(OnlyHTMLFS{fs}, patterns...))
engine.SetHTMLTemplate(templ)
}
// SetHTMLTemplate associate a template with HTML renderer. // SetHTMLTemplate associate a template with HTML renderer.
func (engine *Engine) SetHTMLTemplate(templ *template.Template) { func (engine *Engine) SetHTMLTemplate(templ *template.Template) {
if len(engine.trees) > 0 { if len(engine.trees) > 0 {

View File

@ -32,6 +32,11 @@ func LoadHTMLFiles(files ...string) {
engine().LoadHTMLFiles(files...) engine().LoadHTMLFiles(files...)
} }
// LoadHTMLFS is a wrapper for Engine.LoadHTMLFS.
func LoadHTMLFS(fs http.FileSystem, patterns ...string) {
engine().LoadHTMLFS(fs, patterns...)
}
// SetHTMLTemplate is a wrapper for Engine.SetHTMLTemplate. // SetHTMLTemplate is a wrapper for Engine.SetHTMLTemplate.
func SetHTMLTemplate(templ *template.Template) { func SetHTMLTemplate(templ *template.Template) {
engine().SetHTMLTemplate(templ) engine().SetHTMLTemplate(templ)

View File

@ -6,6 +6,7 @@ package gin
import ( import (
"crypto/tls" "crypto/tls"
"embed"
"fmt" "fmt"
"html/template" "html/template"
"io" "io"
@ -325,6 +326,116 @@ func TestLoadHTMLFilesFuncMap(t *testing.T) {
assert.Equal(t, "Date: 2017/07/01", string(resp)) assert.Equal(t, "Date: 2017/07/01", string(resp))
} }
//go:embed testdata/template/*
var htmlFS embed.FS
func TestLoadHTMLFSTestMode(t *testing.T) {
ts := setupHTMLFiles(
t,
TestMode,
false,
func(router *Engine) {
router.LoadHTMLFS(http.FS(htmlFS), "testdata/template/hello.tmpl", "testdata/template/raw.tmpl")
},
)
defer ts.Close()
res, err := http.Get(fmt.Sprintf("%s/test", ts.URL))
if err != nil {
t.Error(err)
}
resp, _ := io.ReadAll(res.Body)
assert.Equal(t, "<h1>Hello world</h1>", string(resp))
}
func TestLoadHTMLFSDebugMode(t *testing.T) {
ts := setupHTMLFiles(
t,
DebugMode,
false,
func(router *Engine) {
router.LoadHTMLFS(http.FS(htmlFS), "testdata/template/hello.tmpl", "testdata/template/raw.tmpl")
},
)
defer ts.Close()
res, err := http.Get(fmt.Sprintf("%s/test", ts.URL))
if err != nil {
t.Error(err)
}
resp, _ := io.ReadAll(res.Body)
assert.Equal(t, "<h1>Hello world</h1>", string(resp))
}
func TestLoadHTMLFSReleaseMode(t *testing.T) {
ts := setupHTMLFiles(
t,
ReleaseMode,
false,
func(router *Engine) {
router.LoadHTMLFS(http.FS(htmlFS), "testdata/template/hello.tmpl", "testdata/template/raw.tmpl")
},
)
defer ts.Close()
res, err := http.Get(fmt.Sprintf("%s/test", ts.URL))
if err != nil {
t.Error(err)
}
resp, _ := io.ReadAll(res.Body)
assert.Equal(t, "<h1>Hello world</h1>", string(resp))
}
func TestLoadHTMLFSUsingTLS(t *testing.T) {
ts := setupHTMLFiles(
t,
TestMode,
true,
func(router *Engine) {
router.LoadHTMLFS(http.FS(htmlFS), "testdata/template/hello.tmpl", "testdata/template/raw.tmpl")
},
)
defer ts.Close()
// Use InsecureSkipVerify for avoiding `x509: certificate signed by unknown authority` error
tr := &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
},
}
client := &http.Client{Transport: tr}
res, err := client.Get(fmt.Sprintf("%s/test", ts.URL))
if err != nil {
t.Error(err)
}
resp, _ := io.ReadAll(res.Body)
assert.Equal(t, "<h1>Hello world</h1>", string(resp))
}
func TestLoadHTMLFSFuncMap(t *testing.T) {
ts := setupHTMLFiles(
t,
TestMode,
false,
func(router *Engine) {
router.LoadHTMLFS(http.FS(htmlFS), "testdata/template/hello.tmpl", "testdata/template/raw.tmpl")
},
)
defer ts.Close()
res, err := http.Get(fmt.Sprintf("%s/raw", ts.URL))
if err != nil {
t.Error(err)
}
resp, _ := io.ReadAll(res.Body)
assert.Equal(t, "Date: 2017/07/01", string(resp))
}
func TestAddRoute(t *testing.T) { func TestAddRoute(t *testing.T) {
router := New() router := New()
router.addRoute("GET", "/", HandlersChain{func(_ *Context) {}}) router.addRoute("GET", "/", HandlersChain{func(_ *Context) {}})

View File

@ -6,6 +6,7 @@ package render
import ( import (
"html/template" "html/template"
"io/fs"
"net/http" "net/http"
) )
@ -31,10 +32,12 @@ type HTMLProduction struct {
// HTMLDebug contains template delims and pattern and function with file list. // HTMLDebug contains template delims and pattern and function with file list.
type HTMLDebug struct { type HTMLDebug struct {
Files []string Files []string
Glob string Glob string
Delims Delims FS fs.FS
FuncMap template.FuncMap Patterns []string
Delims Delims
FuncMap template.FuncMap
} }
// HTML contains template reference and its name with given interface object. // HTML contains template reference and its name with given interface object.
@ -73,7 +76,10 @@ func (r HTMLDebug) loadTemplate() *template.Template {
if r.Glob != "" { if r.Glob != "" {
return template.Must(template.New("").Delims(r.Delims.Left, r.Delims.Right).Funcs(r.FuncMap).ParseGlob(r.Glob)) return template.Must(template.New("").Delims(r.Delims.Left, r.Delims.Right).Funcs(r.FuncMap).ParseGlob(r.Glob))
} }
panic("the HTML debug render was created without files or glob pattern") if r.FS != nil && len(r.Patterns) > 0 {
return template.Must(template.New("").Delims(r.Delims.Left, r.Delims.Right).Funcs(r.FuncMap).ParseFS(r.FS, r.Patterns...))
}
panic("the HTML debug render was created without files or glob pattern or file system with patterns")
} }
// Render (HTML) executes template and writes its result with custom ContentType for response. // Render (HTML) executes template and writes its result with custom ContentType for response.