diff --git a/HISTORY.md b/HISTORY.md index 1a00bdf1..76c66fe6 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -29,10 +29,10 @@ The codebase for Dependency Injection, Internationalization and localization and ## Fixes and Improvements - Support of embedded [locale files](https://github.com/kataras/iris/blob/master/_examples/i18n/template-embedded/main.go) using standard `embed.FS` with the new `LoadFS` function. -- Support of direct embedded view templates with `embed.FS` or `fs.FS` (in addition to `string` and `http.FileSystem` types). +- Support of direct embedded view engines (`HTML, Blocks, Django, Handlebars, Pug, Amber, Jet` and `Ace`) with `embed.FS` or `fs.FS` (in addition to `string` and `http.FileSystem` types). +- Add support for `embed.FS` and `fs.FS` on `app.HandleDir`. - Add `iris.Patches()` package-level function to customize Iris Request Context REST (and more to come) behavior. -- Add support for `embed.FS` and `fs.FS` on `app.HandleDir`. - Minor fixes. - Enable setting a custom "go-redis" client through `SetClient` go redis driver method or `Client` struct field on sessions/database/redis driver as requested at [chat](https://chat.iris-go.com). diff --git a/_examples/i18n/template-embedded/locales/el-GR/other.ini b/_examples/i18n/template-embedded/embedded/locales/el-GR/other.ini similarity index 100% rename from _examples/i18n/template-embedded/locales/el-GR/other.ini rename to _examples/i18n/template-embedded/embedded/locales/el-GR/other.ini diff --git a/_examples/i18n/template-embedded/locales/el-GR/user.ini b/_examples/i18n/template-embedded/embedded/locales/el-GR/user.ini similarity index 100% rename from _examples/i18n/template-embedded/locales/el-GR/user.ini rename to _examples/i18n/template-embedded/embedded/locales/el-GR/user.ini diff --git a/_examples/i18n/template-embedded/locales/en-US/other.ini b/_examples/i18n/template-embedded/embedded/locales/en-US/other.ini similarity index 100% rename from _examples/i18n/template-embedded/locales/en-US/other.ini rename to _examples/i18n/template-embedded/embedded/locales/en-US/other.ini diff --git a/_examples/i18n/template-embedded/locales/en-US/user.ini b/_examples/i18n/template-embedded/embedded/locales/en-US/user.ini similarity index 100% rename from _examples/i18n/template-embedded/locales/en-US/user.ini rename to _examples/i18n/template-embedded/embedded/locales/en-US/user.ini diff --git a/_examples/i18n/template-embedded/main.go b/_examples/i18n/template-embedded/main.go index fd45e57c..bf8f8359 100644 --- a/_examples/i18n/template-embedded/main.go +++ b/_examples/i18n/template-embedded/main.go @@ -8,8 +8,8 @@ import ( "github.com/kataras/iris/v12" ) -//go:embed locales/* -var filesystem embed.FS +//go:embed embedded/locales/* +var embeddedFS embed.FS func main() { app := newApp() @@ -31,7 +31,7 @@ func newApp() *iris.Application { // Instead of: // err := app.I18n.Load("./locales/*/*.ini", "en-US", "el-GR") // Apply the below in order to build with embedded locales inside your executable binary. - err := app.I18n.LoadFS(filesystem, ".", "en-US", "el-GR") + err := app.I18n.LoadFS(embeddedFS, "./embedded/locales/*/*.ini", "en-US", "el-GR") if err != nil { panic(err) } diff --git a/_examples/view/context-view-engine/views/admin/layouts/main.html b/_examples/view/context-view-engine/views/admin/layouts/main.html index d83d5062..b4b52356 100644 --- a/_examples/view/context-view-engine/views/admin/layouts/main.html +++ b/_examples/view/context-view-engine/views/admin/layouts/main.html @@ -9,6 +9,6 @@ {{ template "content" .}} -

Copyright © 2020 Admin

+

Copyright © 2022 Admin

\ No newline at end of file diff --git a/_examples/view/embedding-templates-into-app/templates/layouts/layout.html b/_examples/view/embedding-templates-into-app/embedded/templates/layouts/layout.html similarity index 93% rename from _examples/view/embedding-templates-into-app/templates/layouts/layout.html rename to _examples/view/embedding-templates-into-app/embedded/templates/layouts/layout.html index eb543f0e..69b545ec 100644 --- a/_examples/view/embedding-templates-into-app/templates/layouts/layout.html +++ b/_examples/view/embedding-templates-into-app/embedded/templates/layouts/layout.html @@ -1,12 +1,12 @@ - - -Layout - - - -

This is the global layout

-
- - {{ yield }} - - + + +Layout + + + +

This is the global layout

+
+ + {{ yield }} + + diff --git a/_examples/view/embedding-templates-into-app/templates/layouts/mylayout.html b/_examples/view/embedding-templates-into-app/embedded/templates/layouts/mylayout.html similarity index 94% rename from _examples/view/embedding-templates-into-app/templates/layouts/mylayout.html rename to _examples/view/embedding-templates-into-app/embedded/templates/layouts/mylayout.html index d87575d3..d22426fe 100644 --- a/_examples/view/embedding-templates-into-app/templates/layouts/mylayout.html +++ b/_examples/view/embedding-templates-into-app/embedded/templates/layouts/mylayout.html @@ -1,12 +1,12 @@ - - -my Layout - - - -

This is the layout for the /my/ and /my/other routes only

-
- - {{ yield }} - - + + +my Layout + + + +

This is the layout for the /my/ and /my/other routes only

+
+ + {{ yield }} + + diff --git a/_examples/view/embedding-templates-into-app/templates/page1.html b/_examples/view/embedding-templates-into-app/embedded/templates/page1.html similarity index 95% rename from _examples/view/embedding-templates-into-app/templates/page1.html rename to _examples/view/embedding-templates-into-app/embedded/templates/page1.html index 22bd16a1..6f63f7b3 100644 --- a/_examples/view/embedding-templates-into-app/templates/page1.html +++ b/_examples/view/embedding-templates-into-app/embedded/templates/page1.html @@ -1,7 +1,7 @@ -
- -

Page 1 {{ greet "iris developer"}}

- - {{ render "partials/page1_partial1.html"}} - -
+
+ +

Page 1 {{ greet "iris developer"}}

+ + {{ render "partials/page1_partial1.html"}} + +
diff --git a/_examples/view/embedding-templates-into-app/templates/partials/page1_partial1.html b/_examples/view/embedding-templates-into-app/embedded/templates/partials/page1_partial1.html similarity index 96% rename from _examples/view/embedding-templates-into-app/templates/partials/page1_partial1.html rename to _examples/view/embedding-templates-into-app/embedded/templates/partials/page1_partial1.html index 8af006d2..66ba9266 100644 --- a/_examples/view/embedding-templates-into-app/templates/partials/page1_partial1.html +++ b/_examples/view/embedding-templates-into-app/embedded/templates/partials/page1_partial1.html @@ -1,3 +1,3 @@ -
-

Page 1's Partial 1

-
+
+

Page 1's Partial 1

+
diff --git a/_examples/view/embedding-templates-into-app/main.go b/_examples/view/embedding-templates-into-app/main.go index fca9a2ee..2fc45ed6 100644 --- a/_examples/view/embedding-templates-into-app/main.go +++ b/_examples/view/embedding-templates-into-app/main.go @@ -7,13 +7,14 @@ import ( "github.com/kataras/iris/v12/x/errors" ) -//go:embed templates/* -var embeddedTemplatesFS embed.FS +//go:embed embedded/* +var embeddedFS embed.FS func main() { app := iris.New() - tmpl := iris.HTML(embeddedTemplatesFS, ".html") + tmpl := iris.HTML(embeddedFS, ".html").RootDir("embedded/templates") + tmpl.Layout("layouts/layout.html") tmpl.AddFunc("greet", func(s string) string { return "Greetings " + s + "!" diff --git a/aliases.go b/aliases.go index 7d68f146..fc08c106 100644 --- a/aliases.go +++ b/aliases.go @@ -1,6 +1,7 @@ package iris import ( + "io/fs" "net/http" "net/url" "path" @@ -805,10 +806,15 @@ func (cp *ContextPatches) SetCookieKVExpiration(patch time.Duration) { context.SetCookieKVExpiration = patch } -// ResolveFS modifies the default way to resolve a filesystem by any type of value. -// It affects the view engine filesystem resolver -// and the Application's API Builder's `HandleDir` method. -func (cp *ContextPatches) ResolveFS(patchFunc func(fsOrDir interface{}) http.FileSystem) { +// ResolveHTTPFS modifies the default way to resolve a filesystem by any type of value. +// It affects the Application's API Builder's `HandleDir` method. +func (cp *ContextPatches) ResolveHTTPFS(patchFunc func(fsOrDir interface{}) http.FileSystem) { + context.ResolveHTTPFS = patchFunc +} + +// ResolveHTTPFS modifies the default way to resolve a filesystem by any type of value. +// It affects the view engine's filesystem resolver. +func (cp *ContextPatches) ResolveFS(patchFunc func(fsOrDir interface{}) fs.FS) { context.ResolveFS = patchFunc } diff --git a/context/context_fs.go b/context/context_fs.go index 7a8f423a..4b5dcc21 100644 --- a/context/context_fs.go +++ b/context/context_fs.go @@ -5,17 +5,102 @@ import ( "fmt" "io/fs" "net/http" + "os" "path" + "path/filepath" ) // ResolveFS accepts a single input argument of any type -// and tries to cast it to http.FileSystem. +// and tries to cast it to fs.FS. // -// It affects the view engine filesystem resolver -// and the Application's API Builder's `HandleDir` method. +// It affects the view engine's filesystem resolver. // // This package-level variable can be modified on initialization. -var ResolveFS = func(fsOrDir interface{}) http.FileSystem { +var ResolveFS = func(fsOrDir interface{}) fs.FS { + if fsOrDir == nil { + return noOpFS{} + } + + switch v := fsOrDir.(type) { + case string: + if v == "" { + return noOpFS{} + } + return os.DirFS(v) + case fs.FS: + return v + case http.FileSystem: // handles go-bindata. + return &httpFS{v} + default: + panic(fmt.Errorf(`unexpected "fsOrDir" argument type of %T (string or fs.FS or embed.FS or http.FileSystem)`, v)) + } +} + +type noOpFS struct{} + +func (fileSystem noOpFS) Open(name string) (fs.File, error) { return nil, nil } + +// IsNoOpFS reports whether the given "fileSystem" is a no operation fs. +func IsNoOpFS(fileSystem fs.FS) bool { + _, ok := fileSystem.(noOpFS) + return ok +} + +type httpFS struct { + fs http.FileSystem +} + +func (f *httpFS) Open(name string) (fs.File, error) { + if name == "." { + name = "/" + } + + return f.fs.Open(filepath.ToSlash(name)) +} + +func (f *httpFS) ReadDir(name string) ([]fs.DirEntry, error) { + name = filepath.ToSlash(name) + if name == "." { + name = "/" + } + + file, err := f.fs.Open(name) + if err != nil { + return nil, err + } + defer file.Close() + + infos, err := file.Readdir(-1) + if err != nil { + return nil, err + } + + entries := make([]fs.DirEntry, 0, len(infos)) + for _, info := range infos { + if info.IsDir() { // http file's does not return the whole tree, so read it. + sub, err := f.ReadDir(info.Name()) + if err != nil { + return nil, err + } + + entries = append(entries, sub...) + continue + } + + entry := fs.FileInfoToDirEntry(info) + entries = append(entries, entry) + } + + return entries, nil +} + +// ResolveHTTPFS accepts a single input argument of any type +// and tries to cast it to http.FileSystem. +// +// It affects the Application's API Builder's `HandleDir` method. +// +// This package-level variable can be modified on initialization. +var ResolveHTTPFS = func(fsOrDir interface{}) http.FileSystem { var fileSystem http.FileSystem switch v := fsOrDir.(type) { case string: diff --git a/core/router/api_builder.go b/core/router/api_builder.go index b9d801e8..af4f5fae 100644 --- a/core/router/api_builder.go +++ b/core/router/api_builder.go @@ -635,7 +635,7 @@ func (api *APIBuilder) HandleDir(requestPath string, fsOrDir interface{}, opts . options = opts[0] } - fs := context.ResolveFS(fsOrDir) + fs := context.ResolveHTTPFS(fsOrDir) h := FileServer(fs, options) description := "file server" if d, ok := fs.(http.Dir); ok { diff --git a/go.mod b/go.mod index 117c0f4e..dc858c86 100644 --- a/go.mod +++ b/go.mod @@ -22,7 +22,7 @@ require ( github.com/iris-contrib/jade v1.1.4 github.com/iris-contrib/schema v0.0.6 github.com/json-iterator/go v1.1.12 - github.com/kataras/blocks v0.0.6 + github.com/kataras/blocks v0.0.7 github.com/kataras/golog v0.1.7 github.com/kataras/jwt v0.1.8 github.com/kataras/neffos v0.0.20 diff --git a/go.sum b/go.sum index f93b24be..51cc84ed 100644 --- a/go.sum +++ b/go.sum @@ -105,8 +105,8 @@ github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnr github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= -github.com/kataras/blocks v0.0.6 h1:UQI+2AMxhUoe5WcyAT0AdPHTSMcrPy+ALAgvYj2vPwo= -github.com/kataras/blocks v0.0.6/go.mod h1:UK+Iwk0Oxpc0GdoJja7sEildotAUKK1LYeYcVF0COWc= +github.com/kataras/blocks v0.0.7 h1:cF3RDY/vxnSRezc7vLFlQFTYXG/yAr1o7WImJuZbzC4= +github.com/kataras/blocks v0.0.7/go.mod h1:UJIU97CluDo0f+zEjbnbkeMRlvYORtmc1304EeyXf4I= github.com/kataras/golog v0.1.7 h1:0TY5tHn5L5DlRIikepcaRR/6oInIr9AiWsxzt0vvlBE= github.com/kataras/golog v0.1.7/go.mod h1:jOSQ+C5fUqsNSwurB/oAHq1IFSb0KI3l6GMa7xB6dZA= github.com/kataras/jwt v0.1.8 h1:u71baOsYD22HWeSOg32tCHbczPjdCk7V4MMeJqTtmGk= diff --git a/i18n/i18n.go b/i18n/i18n.go index 29ca9fa5..514aec48 100644 --- a/i18n/i18n.go +++ b/i18n/i18n.go @@ -4,6 +4,7 @@ package i18n import ( "fmt" + "io/fs" "net/http" "os" "strings" @@ -142,12 +143,12 @@ func (i *I18n) LoadAssets(assetNames func() []string, asset func(string) ([]byte // LoadFS is a method shortcut to load files using `embed.FS` or `fs.FS` or // `http.FileSystem` or `string` (local directory). -// With this method, all the embedded files into "sub" MUST be locale files. +// The "pattern" is a classic glob pattern. // // See `New` and `FS` package-level functions for more. // Example: https://github.com/kataras/iris/blob/master/_examples/i18n/template-embedded/main.go. -func (i *I18n) LoadFS(fsOrDir interface{}, sub string, languages ...string) error { - loader, err := FS(fsOrDir, sub, i.Loader) +func (i *I18n) LoadFS(fileSystem fs.FS, pattern string, languages ...string) error { + loader, err := FS(fileSystem, pattern, i.Loader) if err != nil { return err } diff --git a/i18n/loader.go b/i18n/loader.go index a38b7156..e418e22b 100644 --- a/i18n/loader.go +++ b/i18n/loader.go @@ -4,11 +4,11 @@ import ( "encoding/json" "fmt" "io" + "io/fs" "os" "path/filepath" "strings" - "github.com/kataras/iris/v12/context" "github.com/kataras/iris/v12/i18n/internal" "github.com/BurntSushi/toml" @@ -50,17 +50,13 @@ func Assets(assetNames func() []string, asset func(string) ([]byte, error), opti // LoadFS loads the files using embed.FS or fs.FS or // http.FileSystem or string (local directory). -// With this method, all the embedded files into "sub" MUST be locale files. +// The "pattern" is a classic glob pattern. // // See `Glob`, `Assets`, `New` and `LoaderConfig` too. -func FS(fsOrDir interface{}, sub string, options LoaderConfig) (Loader, error) { - if sub == "" { - sub = "." - } +func FS(fileSystem fs.FS, pattern string, options LoaderConfig) (Loader, error) { + pattern = strings.TrimPrefix(pattern, "./") - fileSystem := context.ResolveFS(fsOrDir) - - assetNames, err := context.FindNames(fileSystem, sub) + assetNames, err := fs.Glob(fileSystem, pattern) if err != nil { return nil, err } diff --git a/view/README.md b/view/README.md index 3e488348..3bc9e7cb 100644 --- a/view/README.md +++ b/view/README.md @@ -154,7 +154,7 @@ func hi(ctx iris.Context) { } ``` -A real example can be found here: https://github.com/kataras/iris/tree/master/_examples/view/embedding-templates-into-app. +Examples can be found here: https://github.com/kataras/iris/tree/master/_examples/view/embedding-templates-into-app and https://github.com/kataras/iris/tree/master/_examples/view/embedding-templates-into-app-bindata. ## Reload diff --git a/view/ace.go b/view/ace.go index 2cbaf31e..e6cf90f9 100644 --- a/view/ace.go +++ b/view/ace.go @@ -34,7 +34,7 @@ func (s *AceEngine) SetIndent(indent string) *AceEngine { // Usage: // Ace("./views", ".ace") or // Ace(iris.Dir("./views"), ".ace") or -// Ace(AssetFile(), ".ace") for embedded data. +// Ace(embed.FS, ".ace") or Ace(AssetFile(), ".ace") for embedded data. func Ace(fs interface{}, extension string) *AceEngine { s := &AceEngine{HTMLEngine: HTML(fs, extension), indent: ""} s.name = "Ace" diff --git a/view/amber.go b/view/amber.go index 60aec742..309d98ff 100644 --- a/view/amber.go +++ b/view/amber.go @@ -5,6 +5,7 @@ import ( "fmt" "html/template" "io" + "io/fs" "net/http" "os" "path/filepath" @@ -17,7 +18,7 @@ import ( // AmberEngine contains the amber view engine structure. type AmberEngine struct { - fs http.FileSystem + fs fs.FS // files configuration rootDir string extension string @@ -43,15 +44,15 @@ var amberOnce = new(uint32) // Usage: // Amber("./views", ".amber") or // Amber(iris.Dir("./views"), ".amber") or -// Amber(AssetFile(), ".amber") for embedded data. -func Amber(fs interface{}, extension string) *AmberEngine { +// Amber(embed.FS, ".amber") or Amber(AssetFile(), ".amber") for embedded data. +func Amber(dirOrFS interface{}, extension string) *AmberEngine { if atomic.LoadUint32(amberOnce) > 0 { panic("Amber: cannot be registered twice as its internal implementation share the same template functions across instances.") } else { atomic.StoreUint32(amberOnce, 1) } - fileSystem := getFS(fs) + fileSystem := getFS(dirOrFS) s := &AmberEngine{ fs: fileSystem, rootDir: "/", @@ -60,7 +61,7 @@ func Amber(fs interface{}, extension string) *AmberEngine { Options: amber.Options{ PrettyPrint: false, LineNumbers: false, - VirtualFilesystem: fileSystem, + VirtualFilesystem: http.FS(fileSystem), }, bufPool: &sync.Pool{New: func() interface{} { return new(bytes.Buffer) @@ -84,6 +85,15 @@ func Amber(fs interface{}, extension string) *AmberEngine { // RootDir sets the directory to be used as a starting point // to load templates from the provided file system. func (s *AmberEngine) RootDir(root string) *AmberEngine { + if s.fs != nil && root != "" && root != "/" && root != "." && root != s.rootDir { + sub, err := fs.Sub(s.fs, s.rootDir) + if err != nil { + panic(err) + } + + s.fs = sub // here so the "middleware" can work. + } + s.rootDir = filepath.ToSlash(root) return s } @@ -142,7 +152,7 @@ func (s *AmberEngine) AddFunc(funcName string, funcBody interface{}) { // // Returns an error if something bad happens, user is responsible to catch it. func (s *AmberEngine) Load() error { - return walk(s.fs, s.rootDir, func(path string, info os.FileInfo, err error) error { + return walk(s.fs, "", func(path string, info os.FileInfo, err error) error { if err != nil { return err } diff --git a/view/blocks.go b/view/blocks.go index 9c351e0b..b85e602e 100644 --- a/view/blocks.go +++ b/view/blocks.go @@ -43,7 +43,7 @@ func WrapBlocks(v *blocks.Blocks) *BlocksEngine { // Usage: // Blocks("./views", ".html") or // Blocks(iris.Dir("./views"), ".html") or -// Blocks(AssetFile(), ".html") for embedded data. +// Blocks(embed.FS, ".html") or Blocks(AssetFile(), ".html") for embedded data. func Blocks(fs interface{}, extension string) *BlocksEngine { return WrapBlocks(blocks.New(fs).Extension(extension)) } @@ -54,7 +54,7 @@ func (s *BlocksEngine) Name() string { } // RootDir sets the directory to use as the root one inside the provided File System. -func (s *BlocksEngine) RootDir(root string) *BlocksEngine { +func (s *BlocksEngine) RootDir(root string) *BlocksEngine { // TODO: update blocks for the new fs.FS interface and use it for Sub. s.Engine.RootDir(root) return s } diff --git a/view/django.go b/view/django.go index 3e1205d5..0dd6e768 100644 --- a/view/django.go +++ b/view/django.go @@ -3,7 +3,7 @@ package view import ( "bytes" "io" - "net/http" + "io/fs" "os" stdPath "path" "path/filepath" @@ -62,7 +62,7 @@ var AsSafeValue = pongo2.AsSafeValue type tDjangoAssetLoader struct { rootDir string - fs http.FileSystem + fs fs.FS } // Abs calculates the path to a given template. Whenever a path must be resolved @@ -91,7 +91,7 @@ func (l *tDjangoAssetLoader) Get(path string) (io.Reader, error) { // DjangoEngine contains the django view engine structure. type DjangoEngine struct { - fs http.FileSystem + fs fs.FS // files configuration rootDir string extension string @@ -117,7 +117,7 @@ var ( // Usage: // Django("./views", ".html") or // Django(iris.Dir("./views"), ".html") or -// Django(AssetFile(), ".html") for embedded data. +// Django(embed.FS, ".html") or Django(AssetFile(), ".html") for embedded data. func Django(fs interface{}, extension string) *DjangoEngine { s := &DjangoEngine{ fs: getFS(fs), @@ -134,6 +134,15 @@ func Django(fs interface{}, extension string) *DjangoEngine { // RootDir sets the directory to be used as a starting point // to load templates from the provided file system. func (s *DjangoEngine) RootDir(root string) *DjangoEngine { + if s.fs != nil && root != "" && root != "/" && root != "." && root != s.rootDir { + sub, err := fs.Sub(s.fs, s.rootDir) + if err != nil { + panic(err) + } + + s.fs = sub // here so the "middleware" can work. + } + s.rootDir = filepath.ToSlash(root) return s } @@ -213,7 +222,7 @@ func (s *DjangoEngine) RegisterTag(tagName string, fn TagParser) error { // // Returns an error if something bad happens, user is responsible to catch it. func (s *DjangoEngine) Load() error { - return walk(s.fs, s.rootDir, func(path string, info os.FileInfo, err error) error { + return walk(s.fs, "", func(path string, info os.FileInfo, err error) error { if err != nil { return err } diff --git a/view/fs.go b/view/fs.go index 8fb503c9..94827872 100644 --- a/view/fs.go +++ b/view/fs.go @@ -2,125 +2,54 @@ package view import ( "fmt" - "io" - "net/http" - "path" + "io/fs" "path/filepath" - "sort" "github.com/kataras/iris/v12/context" ) -// walk recursively in "fs" descends "root" path, calling "walkFn". -func walk(fs http.FileSystem, root string, walkFn filepath.WalkFunc) error { - names, err := assetNames(fs, root) - if err != nil { - return fmt.Errorf("%s: %w", root, err) +// walk recursively in "fileSystem" descends "root" path, calling "walkFn". +func walk(fileSystem fs.FS, root string, walkFn filepath.WalkFunc) error { + if root != "" && root != "/" && root != "." { + sub, err := fs.Sub(fileSystem, root) + if err != nil { + return err + } + + fileSystem = sub } - for _, name := range names { - fullpath := path.Join(root, name) - f, err := fs.Open(fullpath) + if root == "" { + root = "." + } + + return fs.WalkDir(fileSystem, root, func(path string, d fs.DirEntry, err error) error { if err != nil { - return fmt.Errorf("%s: %w", fullpath, err) + return fmt.Errorf("%s: %w", path, err) } - stat, err := f.Stat() - err = walkFn(fullpath, stat, err) + + info, err := d.Info() if err != nil { if err != filepath.SkipDir { - return fmt.Errorf("%s: %w", fullpath, err) + return fmt.Errorf("%s: %w", path, err) } - continue + return nil } - if stat.IsDir() { - if err := walk(fs, fullpath, walkFn); err != nil { - return fmt.Errorf("%s: %w", fullpath, err) - } + if info.IsDir() { + return nil } - } - return nil + return walkFn(path, info, err) + }) + } -// assetNames returns the first-level directories and file, sorted, names. -func assetNames(fs http.FileSystem, name string) ([]string, error) { - f, err := fs.Open(name) - if err != nil { - return nil, err - } - - if f == nil { - return nil, nil - } - - infos, err := f.Readdir(-1) - f.Close() - if err != nil { - return nil, err - } - - names := make([]string, 0, len(infos)) - for _, info := range infos { - // note: go-bindata fs returns full names whether - // the http.Dir returns the base part, so - // we only work with their base names. - name := filepath.ToSlash(info.Name()) - name = path.Base(name) - - names = append(names, name) - } - - sort.Strings(names) - return names, nil +func asset(fileSystem fs.FS, name string) ([]byte, error) { + return fs.ReadFile(fileSystem, name) } -func asset(fs http.FileSystem, name string) ([]byte, error) { - f, err := fs.Open(name) - if err != nil { - return nil, err - } - - contents, err := io.ReadAll(f) - f.Close() - return contents, err -} - -func getFS(fsOrDir interface{}) (fs http.FileSystem) { - if fsOrDir == nil { - return noOpFS{} - } - - if v, ok := fsOrDir.(string); ok { - if v == "" { - fs = noOpFS{} - } else { - fs = httpDirWrapper{http.Dir(v)} - } - } else { - fs = context.ResolveFS(fsOrDir) - } - - return -} - -type noOpFS struct{} - -func (fs noOpFS) Open(name string) (http.File, error) { return nil, nil } - -func isNoOpFS(fs http.FileSystem) bool { - _, ok := fs.(noOpFS) - return ok -} - -// fixes: "invalid character in file path" -// on amber engine (it uses the virtual fs directly -// and it uses filepath instead of the path package...). -type httpDirWrapper struct { - http.Dir -} - -func (fs httpDirWrapper) Open(name string) (http.File, error) { - return fs.Dir.Open(filepath.ToSlash(name)) +func getFS(fsOrDir interface{}) fs.FS { + return context.ResolveFS(fsOrDir) } diff --git a/view/handlebars.go b/view/handlebars.go index bdb9ed78..91899058 100644 --- a/view/handlebars.go +++ b/view/handlebars.go @@ -4,7 +4,7 @@ import ( "fmt" "html/template" "io" - "net/http" + "io/fs" "os" "path/filepath" "strings" @@ -15,7 +15,7 @@ import ( // HandlebarsEngine contains the handlebars view engine structure. type HandlebarsEngine struct { - fs http.FileSystem + fs fs.FS // files configuration rootDir string extension string @@ -41,7 +41,7 @@ var ( // Usage: // Handlebars("./views", ".html") or // Handlebars(iris.Dir("./views"), ".html") or -// Handlebars(AssetFile(), ".html") for embedded data. +// Handlebars(embed.FS, ".html") or Handlebars(AssetFile(), ".html") for embedded data. func Handlebars(fs interface{}, extension string) *HandlebarsEngine { s := &HandlebarsEngine{ fs: getFS(fs), @@ -66,6 +66,15 @@ func Handlebars(fs interface{}, extension string) *HandlebarsEngine { // RootDir sets the directory to be used as a starting point // to load templates from the provided file system. func (s *HandlebarsEngine) RootDir(root string) *HandlebarsEngine { + if s.fs != nil && root != "" && root != "/" && root != "." && root != s.rootDir { + sub, err := fs.Sub(s.fs, s.rootDir) + if err != nil { + panic(err) + } + + s.fs = sub // here so the "middleware" can work. + } + s.rootDir = filepath.ToSlash(root) return s } @@ -125,7 +134,7 @@ func (s *HandlebarsEngine) AddGlobalFunc(funcName string, funcBody interface{}) // // Returns an error if something bad happens, user is responsible to catch it. func (s *HandlebarsEngine) Load() error { - return walk(s.fs, s.rootDir, func(path string, info os.FileInfo, _ error) error { + return walk(s.fs, "", func(path string, info os.FileInfo, _ error) error { if info == nil || info.IsDir() { return nil } diff --git a/view/html.go b/view/html.go index 6e875eb1..bcae343e 100644 --- a/view/html.go +++ b/view/html.go @@ -5,7 +5,7 @@ import ( "fmt" "html/template" "io" - "net/http" + "io/fs" "os" "path/filepath" "strings" @@ -16,7 +16,7 @@ import ( type HTMLEngine struct { name string // the view engine's name, can be HTML, Ace or Pug. // the file system to load from. - fs http.FileSystem + fs fs.FS // files configuration rootDir string extension string @@ -80,11 +80,11 @@ var emptyFuncs = template.FuncMap{ // Usage: // HTML("./views", ".html") or // HTML(iris.Dir("./views"), ".html") or -// HTML(AssetFile(), ".html") for embedded data. -func HTML(fs interface{}, extension string) *HTMLEngine { +// HTML(embed.FS, ".html") or HTML(AssetFile(), ".html") for embedded data. +func HTML(dirOrFS interface{}, extension string) *HTMLEngine { s := &HTMLEngine{ name: "HTML", - fs: getFS(fs), + fs: getFS(dirOrFS), rootDir: "/", extension: extension, reload: false, @@ -104,6 +104,15 @@ func HTML(fs interface{}, extension string) *HTMLEngine { // RootDir sets the directory to be used as a starting point // to load templates from the provided file system. func (s *HTMLEngine) RootDir(root string) *HTMLEngine { + if s.fs != nil && root != "" && root != "/" && root != "." && root != s.rootDir { + sub, err := fs.Sub(s.fs, root) + if err != nil { + panic(err) + } + + s.fs = sub // here so the "middleware" can work. + } + s.rootDir = filepath.ToSlash(root) return s } @@ -246,7 +255,7 @@ func (s *HTMLEngine) load() error { return err } - return walk(s.fs, s.rootDir, func(path string, info os.FileInfo, err error) error { + err := walk(s.fs, "", func(path string, info os.FileInfo, err error) error { if err != nil { return err } @@ -268,6 +277,16 @@ func (s *HTMLEngine) load() error { return s.parseTemplate(path, buf, nil) }) + + if err != nil { + return err + } + + if s.Templates == nil { + return fmt.Errorf("no templates found") + } + + return nil } func (s *HTMLEngine) reloadCustomTemplates() error { diff --git a/view/jet.go b/view/jet.go index 13d06a58..3f1379e2 100644 --- a/view/jet.go +++ b/view/jet.go @@ -3,7 +3,7 @@ package view import ( "fmt" "io" - "net/http" + "io/fs" "os" "path/filepath" "reflect" @@ -19,7 +19,7 @@ const jetEngineName = "jet" // JetEngine is the jet template parser's view engine. type JetEngine struct { - fs http.FileSystem + fs fs.FS rootDir string extension string left, right string @@ -59,8 +59,8 @@ var jetExtensions = [...]string{ // Usage: // Jet("./views", ".jet") or // Jet(iris.Dir("./views"), ".jet") or -// Jet(AssetFile(), ".jet") for embedded data. -func Jet(fs interface{}, extension string) *JetEngine { +// Jet(embed.FS, ".jet") or Jet(AssetFile(), ".jet") for embedded data. +func Jet(dirOrFS interface{}, extension string) *JetEngine { extOK := false for _, ext := range jetExtensions { if ext == extension { @@ -74,10 +74,10 @@ func Jet(fs interface{}, extension string) *JetEngine { } s := &JetEngine{ - fs: getFS(fs), + fs: getFS(dirOrFS), rootDir: "/", extension: extension, - loader: &jetLoader{fs: getFS(fs)}, + loader: &jetLoader{fs: getFS(dirOrFS)}, jetDataContextKey: "_jet", } @@ -92,6 +92,15 @@ func (s *JetEngine) String() string { // RootDir sets the directory to be used as a starting point // to load templates from the provided file system. func (s *JetEngine) RootDir(root string) *JetEngine { + if s.fs != nil && root != "" && root != "/" && root != "." && root != s.rootDir { + sub, err := fs.Sub(s.fs, s.rootDir) + if err != nil { + panic(err) + } + + s.fs = sub + } + s.rootDir = filepath.ToSlash(root) return s } @@ -199,7 +208,7 @@ func (s *JetEngine) SetLoader(loader jet.Loader) *JetEngine { } type jetLoader struct { - fs http.FileSystem + fs fs.FS } var _ jet.Loader = (*jetLoader)(nil) @@ -217,7 +226,7 @@ func (l *jetLoader) Exists(name string) bool { // Load should load the templates from a physical system directory or by an embedded one (assets/go-bindata). func (s *JetEngine) Load() error { - return walk(s.fs, s.rootDir, func(path string, info os.FileInfo, err error) error { + return walk(s.fs, "", func(path string, info os.FileInfo, err error) error { if err != nil { return err } @@ -256,7 +265,7 @@ func (s *JetEngine) initSet() { var opts = []jet.Option{ jet.WithDelims(s.left, s.right), } - if s.developmentMode && !isNoOpFS(s.fs) { + if s.developmentMode && !context.IsNoOpFS(s.fs) { // this check is made to avoid jet's fs lookup on noOp fs (nil passed by the developer). // This can be produced when nil fs passed // and only `ParseTemplate` is used. diff --git a/view/pug.go b/view/pug.go index 9e57dc74..dd0cf9fc 100644 --- a/view/pug.go +++ b/view/pug.go @@ -16,7 +16,7 @@ import ( // Usage: // Pug("./views", ".pug") or // Pug(iris.Dir("./views"), ".pug") or -// Pug(AssetFile(), ".pug") for embedded data. +// Pug(embed.FS, ".pug") or Pug(AssetFile(), ".pug") for embedded data. // // Examples: // https://github.com/kataras/iris/tree/master/_examples/view/template_pug_0