diff --git a/docs/doc.md b/docs/doc.md index 8e6338e49c..303da647a3 100644 --- a/docs/doc.md +++ b/docs/doc.md @@ -1351,6 +1351,8 @@ func main() { router := gin.Default() router.LoadHTMLGlob("templates/*") //router.LoadHTMLFiles("templates/template1.html", "templates/template2.html") + //router.LoadHTMLFS(http.Dir("templates"), "template1.html", "template2.html") + //or //router.LoadHTMLFS(http.FS(templates), "templates/template1.html", "templates/template2.html") router.GET("/index", func(c *gin.Context) { c.HTML(http.StatusOK, "index.tmpl", gin.H{ diff --git a/fs.go b/fs.go index da6ab4b76f..51c3db86fb 100644 --- a/fs.go +++ b/fs.go @@ -5,7 +5,6 @@ package gin import ( - "io/fs" "net/http" "os" ) @@ -26,22 +25,6 @@ func (o OnlyFilesFS) Open(name string) (http.File, error) { 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`. type neutralizedReaddirFile struct { http.File diff --git a/fs_test.go b/fs_test.go index 48cfd6f02f..167ac1afa1 100644 --- a/fs_test.go +++ b/fs_test.go @@ -48,36 +48,6 @@ func TestOnlyFilesFS_Open_err(t *testing.T) { assert.Nil(t, file) } -func TestOnlyHTMLFS_Open(t *testing.T) { - var testFile *os.File - mockFS := &mockFileSystem{ - open: func(name string) (http.File, error) { - return testFile, nil - }, - } - fs := &OnlyHTMLFS{FileSystem: mockFS} - - file, err := fs.Open("foo") - - require.NoError(t, err) - assert.Equal(t, testFile, file) -} - -func TestOnlyHTMLFS_Open_err(t *testing.T) { - testError := errors.New("mock") - mockFS := &mockFileSystem{ - open: func(_ string) (http.File, error) { - return nil, testError - }, - } - fs := &OnlyHTMLFS{FileSystem: mockFS} - - file, err := fs.Open("foo") - - require.ErrorIs(t, err, testError) - assert.Nil(t, file) -} - func Test_neuteredReaddirFile_Readdir(t *testing.T) { n := neutralizedReaddirFile{} diff --git a/gin.go b/gin.go index 85378296e0..10b6ed957a 100644 --- a/gin.go +++ b/gin.go @@ -16,6 +16,7 @@ import ( "sync" "github.com/gin-gonic/gin/internal/bytesconv" + filesystem "github.com/gin-gonic/gin/internal/fs" "github.com/gin-gonic/gin/render" "github.com/quic-go/quic-go/http3" @@ -289,11 +290,12 @@ func (engine *Engine) LoadHTMLFiles(files ...string) { // 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} + engine.HTMLRender = render.HTMLDebug{FileSystem: 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...)) + templ := template.Must(template.New("").Delims(engine.delims.Left, engine.delims.Right).Funcs(engine.FuncMap).ParseFS( + filesystem.FileSystem{FileSystem: fs}, patterns...)) engine.SetHTMLTemplate(templ) } diff --git a/gin_test.go b/gin_test.go index 56115547df..9f81900346 100644 --- a/gin_test.go +++ b/gin_test.go @@ -6,7 +6,6 @@ package gin import ( "crypto/tls" - "embed" "fmt" "html/template" "io" @@ -326,8 +325,7 @@ func TestLoadHTMLFilesFuncMap(t *testing.T) { assert.Equal(t, "Date: 2017/07/01", string(resp)) } -//go:embed testdata/template/*.tmpl -var tmplFS embed.FS +var tmplFS = http.Dir("testdata/template") func TestLoadHTMLFSTestMode(t *testing.T) { ts := setupHTMLFiles( @@ -335,7 +333,7 @@ func TestLoadHTMLFSTestMode(t *testing.T) { TestMode, false, func(router *Engine) { - router.LoadHTMLFS(http.FS(tmplFS), "testdata/template/hello.tmpl", "testdata/template/raw.tmpl") + router.LoadHTMLFS(tmplFS, "hello.tmpl", "raw.tmpl") }, ) defer ts.Close() @@ -355,7 +353,7 @@ func TestLoadHTMLFSDebugMode(t *testing.T) { DebugMode, false, func(router *Engine) { - router.LoadHTMLFS(http.FS(tmplFS), "testdata/template/hello.tmpl", "testdata/template/raw.tmpl") + router.LoadHTMLFS(tmplFS, "hello.tmpl", "raw.tmpl") }, ) defer ts.Close() @@ -375,7 +373,7 @@ func TestLoadHTMLFSReleaseMode(t *testing.T) { ReleaseMode, false, func(router *Engine) { - router.LoadHTMLFS(http.FS(tmplFS), "testdata/template/hello.tmpl", "testdata/template/raw.tmpl") + router.LoadHTMLFS(tmplFS, "hello.tmpl", "raw.tmpl") }, ) defer ts.Close() @@ -395,7 +393,7 @@ func TestLoadHTMLFSUsingTLS(t *testing.T) { TestMode, true, func(router *Engine) { - router.LoadHTMLFS(http.FS(tmplFS), "testdata/template/hello.tmpl", "testdata/template/raw.tmpl") + router.LoadHTMLFS(tmplFS, "hello.tmpl", "raw.tmpl") }, ) defer ts.Close() @@ -422,7 +420,7 @@ func TestLoadHTMLFSFuncMap(t *testing.T) { TestMode, false, func(router *Engine) { - router.LoadHTMLFS(http.FS(tmplFS), "testdata/template/hello.tmpl", "testdata/template/raw.tmpl") + router.LoadHTMLFS(tmplFS, "hello.tmpl", "raw.tmpl") }, ) defer ts.Close() diff --git a/internal/fs/fs.go b/internal/fs/fs.go new file mode 100644 index 0000000000..524ac08b0d --- /dev/null +++ b/internal/fs/fs.go @@ -0,0 +1,22 @@ +package fs + +import ( + "io/fs" + "net/http" +) + +// FileSystem implements an [fs.FS]. +type FileSystem struct { + http.FileSystem +} + +// Open passes `Open` to the upstream implementation and return an [fs.File]. +func (o FileSystem) Open(name string) (fs.File, error) { + f, err := o.FileSystem.Open(name) + + if err != nil { + return nil, err + } + + return fs.File(f), nil +} diff --git a/internal/fs/fs_test.go b/internal/fs/fs_test.go new file mode 100644 index 0000000000..113e92b65c --- /dev/null +++ b/internal/fs/fs_test.go @@ -0,0 +1,49 @@ +package fs + +import ( + "errors" + "net/http" + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +type mockFileSystem struct { + open func(name string) (http.File, error) +} + +func (m *mockFileSystem) Open(name string) (http.File, error) { + return m.open(name) +} + +func TesFileSystem_Open(t *testing.T) { + var testFile *os.File + mockFS := &mockFileSystem{ + open: func(name string) (http.File, error) { + return testFile, nil + }, + } + fs := &FileSystem{mockFS} + + file, err := fs.Open("foo") + + require.NoError(t, err) + assert.Equal(t, testFile, file) +} + +func TestFileSystem_Open_err(t *testing.T) { + testError := errors.New("mock") + mockFS := &mockFileSystem{ + open: func(_ string) (http.File, error) { + return nil, testError + }, + } + fs := &FileSystem{mockFS} + + file, err := fs.Open("foo") + + require.ErrorIs(t, err, testError) + assert.Nil(t, file) +} diff --git a/render/html.go b/render/html.go index bda0a43559..f5e7455a02 100644 --- a/render/html.go +++ b/render/html.go @@ -6,8 +6,9 @@ package render import ( "html/template" - "io/fs" "net/http" + + "github.com/gin-gonic/gin/internal/fs" ) // Delims represents a set of Left and Right delimiters for HTML template rendering. @@ -32,12 +33,12 @@ type HTMLProduction struct { // HTMLDebug contains template delims and pattern and function with file list. type HTMLDebug struct { - Files []string - Glob string - FS fs.FS - Patterns []string - Delims Delims - FuncMap template.FuncMap + Files []string + Glob string + FileSystem http.FileSystem + Patterns []string + Delims Delims + FuncMap template.FuncMap } // HTML contains template reference and its name with given interface object. @@ -76,8 +77,9 @@ func (r HTMLDebug) loadTemplate() *template.Template { if r.Glob != "" { return template.Must(template.New("").Delims(r.Delims.Left, r.Delims.Right).Funcs(r.FuncMap).ParseGlob(r.Glob)) } - 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...)) + if r.FileSystem != nil && len(r.Patterns) > 0 { + return template.Must(template.New("").Delims(r.Delims.Left, r.Delims.Right).Funcs(r.FuncMap).ParseFS( + fs.FileSystem{FileSystem: r.FileSystem}, r.Patterns...)) } panic("the HTML debug render was created without files or glob pattern or file system with patterns") } diff --git a/render/render_test.go b/render/render_test.go index 27a5065be7..6dabb8a8c5 100644 --- a/render/render_test.go +++ b/render/render_test.go @@ -489,10 +489,12 @@ func TestRenderHTMLTemplateEmptyName(t *testing.T) { func TestRenderHTMLDebugFiles(t *testing.T) { w := httptest.NewRecorder() htmlRender := HTMLDebug{ - Files: []string{"../testdata/template/hello.tmpl"}, - Glob: "", - Delims: Delims{Left: "{[{", Right: "}]}"}, - FuncMap: nil, + Files: []string{"../testdata/template/hello.tmpl"}, + Glob: "", + FileSystem: nil, + Patterns: nil, + Delims: Delims{Left: "{[{", Right: "}]}"}, + FuncMap: nil, } instance := htmlRender.Instance("hello.tmpl", map[string]any{ "name": "thinkerou", @@ -508,10 +510,33 @@ func TestRenderHTMLDebugFiles(t *testing.T) { func TestRenderHTMLDebugGlob(t *testing.T) { w := httptest.NewRecorder() htmlRender := HTMLDebug{ - Files: nil, - Glob: "../testdata/template/hello*", - Delims: Delims{Left: "{[{", Right: "}]}"}, - FuncMap: nil, + Files: nil, + Glob: "../testdata/template/hello*", + FileSystem: nil, + Patterns: nil, + Delims: Delims{Left: "{[{", Right: "}]}"}, + FuncMap: nil, + } + instance := htmlRender.Instance("hello.tmpl", map[string]any{ + "name": "thinkerou", + }) + + err := instance.Render(w) + + require.NoError(t, err) + assert.Equal(t, "

Hello thinkerou

", w.Body.String()) + assert.Equal(t, "text/html; charset=utf-8", w.Header().Get("Content-Type")) +} + +func TestRenderHTMLDebugFS(t *testing.T) { + w := httptest.NewRecorder() + htmlRender := HTMLDebug{ + Files: nil, + Glob: "", + FileSystem: http.Dir("../testdata/template"), + Patterns: []string{"hello.tmpl"}, + Delims: Delims{Left: "{[{", Right: "}]}"}, + FuncMap: nil, } instance := htmlRender.Instance("hello.tmpl", map[string]any{ "name": "thinkerou", @@ -526,10 +551,12 @@ func TestRenderHTMLDebugGlob(t *testing.T) { func TestRenderHTMLDebugPanics(t *testing.T) { htmlRender := HTMLDebug{ - Files: nil, - Glob: "", - Delims: Delims{"{{", "}}"}, - FuncMap: nil, + Files: nil, + Glob: "", + FileSystem: nil, + Patterns: nil, + Delims: Delims{"{{", "}}"}, + FuncMap: nil, } assert.Panics(t, func() { htmlRender.Instance("", nil) }) }