summaryrefslogtreecommitdiff
path: root/template.go
diff options
context:
space:
mode:
authorDave Henderson <dhenderson@gmail.com>2022-05-29 15:03:51 -0400
committerDave Henderson <dhenderson@gmail.com>2022-05-30 14:06:32 -0400
commitc0b93d7ebdfd27badbb41eb62ca0bd77b0252308 (patch)
tree2f3b3e17023f4819cf69dee8953ea44463588bae /template.go
parent9ae9a6a5182342f775383646058807222947f483 (diff)
Support URLs for nested templates
Signed-off-by: Dave Henderson <dhenderson@gmail.com>
Diffstat (limited to 'template.go')
-rw-r--r--template.go136
1 files changed, 117 insertions, 19 deletions
diff --git a/template.go b/template.go
index e161a800..81ecbf14 100644
--- a/template.go
+++ b/template.go
@@ -4,11 +4,15 @@ import (
"context"
"fmt"
"io"
+ "io/fs"
"io/ioutil"
"os"
+ "path"
"path/filepath"
+ "strings"
"text/template"
+ "github.com/hairyhenderson/go-fsimpl"
"github.com/hairyhenderson/gomplate/v3/internal/config"
"github.com/hairyhenderson/gomplate/v3/internal/iohelpers"
"github.com/hairyhenderson/gomplate/v3/tmpl"
@@ -21,7 +25,7 @@ import (
const gomplateignore = ".gomplateignore"
// for overriding in tests
-var fs = afero.NewOsFs()
+var aferoFS = afero.NewOsFs()
// tplate - models a gomplate template file...
type tplate struct {
@@ -53,6 +57,23 @@ func copyFuncMap(funcMap template.FuncMap) template.FuncMap {
return newFuncMap
}
+var fsProviderCtxKey = struct{}{}
+
+// ContextWithFSProvider returns a context with the given FSProvider. Should
+// only be used in tests.
+func ContextWithFSProvider(ctx context.Context, fsp fsimpl.FSProvider) context.Context {
+ return context.WithValue(ctx, fsProviderCtxKey, fsp)
+}
+
+// FSProviderFromContext returns the FSProvider from the context, if any
+func FSProviderFromContext(ctx context.Context) fsimpl.FSProvider {
+ if fsp, ok := ctx.Value(fsProviderCtxKey).(fsimpl.FSProvider); ok {
+ return fsp
+ }
+
+ return nil
+}
+
// toGoTemplate - parses t.contents as a Go template named t.name with the
// configured funcMap, delimiters, and nested templates.
func (t *tplate) toGoTemplate(ctx context.Context, g *gomplate) (tmpl *template.Template, err error) {
@@ -69,24 +90,101 @@ func (t *tplate) toGoTemplate(ctx context.Context, g *gomplate) (tmpl *template.
if err != nil {
return nil, err
}
- for alias, path := range g.nestedTemplates {
- // nolint: gosec
- b, err := ioutil.ReadFile(path)
+
+ err = parseNestedTemplates(ctx, g.nestedTemplates, tmpl)
+ if err != nil {
+ return nil, fmt.Errorf("parse nested templates: %w", err)
+ }
+
+ return tmpl, nil
+}
+
+func parseNestedTemplates(ctx context.Context, nested config.Templates, tmpl *template.Template) error {
+ fsp := FSProviderFromContext(ctx)
+
+ for alias, n := range nested {
+ u := *n.URL
+
+ fname := path.Base(u.Path)
+ if strings.HasSuffix(u.Path, "/") {
+ fname = "."
+ }
+
+ u.Path = path.Dir(u.Path)
+
+ fsys, err := fsp.New(&u)
if err != nil {
- return nil, err
+ return fmt.Errorf("filesystem provider for %q unavailable: %w", &u, err)
}
- _, err = tmpl.New(alias).Parse(string(b))
+
+ // inject context & header in case they're useful...
+ fsys = fsimpl.WithContextFS(ctx, fsys)
+ fsys = fsimpl.WithHeaderFS(n.Header, fsys)
+
+ // valid fs.FS paths have no trailing slash
+ fname = strings.TrimRight(fname, "/")
+
+ // first determine if the template path is a directory, in which case we
+ // need to load all the files in the directory (but not recursively)
+ fi, err := fs.Stat(fsys, fname)
if err != nil {
- return nil, err
+ return fmt.Errorf("stat %q: %w", fname, err)
+ }
+
+ if fi.IsDir() {
+ err = parseNestedTemplateDir(ctx, fsys, alias, fname, tmpl)
+ } else {
+ err = parseNestedTemplate(ctx, fsys, alias, fname, tmpl)
+ }
+
+ if err != nil {
+ return err
}
}
- return tmpl, nil
+
+ return nil
+}
+
+func parseNestedTemplateDir(ctx context.Context, fsys fs.FS, alias, fname string, tmpl *template.Template) error {
+ files, err := fs.ReadDir(fsys, fname)
+ if err != nil {
+ return fmt.Errorf("readDir %q: %w", fname, err)
+ }
+
+ for _, f := range files {
+ if !f.IsDir() {
+ err = parseNestedTemplate(ctx, fsys,
+ path.Join(alias, f.Name()),
+ path.Join(fname, f.Name()),
+ tmpl,
+ )
+ if err != nil {
+ return err
+ }
+ }
+ }
+
+ return nil
+}
+
+func parseNestedTemplate(ctx context.Context, fsys fs.FS, alias, fname string, tmpl *template.Template) error {
+ b, err := fs.ReadFile(fsys, fname)
+ if err != nil {
+ return fmt.Errorf("readFile %q: %w", fname, err)
+ }
+
+ _, err = tmpl.New(alias).Parse(string(b))
+ if err != nil {
+ return fmt.Errorf("parse nested template %q: %w", fname, err)
+ }
+
+ return nil
}
// loadContents - reads the template
func (t *tplate) loadContents(in io.Reader) ([]byte, error) {
if in == nil {
- f, err := fs.OpenFile(t.name, os.O_RDONLY, 0)
+ f, err := aferoFS.OpenFile(t.name, os.O_RDONLY, 0)
if err != nil {
return nil, fmt.Errorf("failed to open %s: %w", t.name, err)
}
@@ -153,14 +251,14 @@ func gatherTemplates(ctx context.Context, cfg *config.Config, outFileNamer func(
func walkDir(ctx context.Context, cfg *config.Config, dir string, outFileNamer func(context.Context, string) (string, error), excludeGlob []string, mode os.FileMode, modeOverride bool) ([]*tplate, error) {
dir = filepath.Clean(dir)
- dirStat, err := fs.Stat(dir)
+ dirStat, err := aferoFS.Stat(dir)
if err != nil {
return nil, fmt.Errorf("couldn't stat %s: %w", dir, err)
}
dirMode := dirStat.Mode()
templates := make([]*tplate, 0)
- matcher := xignore.NewMatcher(fs)
+ matcher := xignore.NewMatcher(aferoFS)
// work around bug in xignore - a basedir of '.' doesn't work
basedir := dir
@@ -191,7 +289,7 @@ func walkDir(ctx context.Context, cfg *config.Config, dir string, outFileNamer f
}
// Ensure file parent dirs
- if err = fs.MkdirAll(filepath.Dir(outFile), dirMode); err != nil {
+ if err = aferoFS.MkdirAll(filepath.Dir(outFile), dirMode); err != nil {
return nil, err
}
@@ -213,7 +311,7 @@ func fileToTemplates(cfg *config.Config, inFile, outFile string, mode os.FileMod
source = string(b)
} else {
- si, err := fs.Stat(inFile)
+ si, err := aferoFS.Stat(inFile)
if err != nil {
return nil, err
}
@@ -223,7 +321,7 @@ func fileToTemplates(cfg *config.Config, inFile, outFile string, mode os.FileMod
// we read the file and store in memory immediately, to prevent leaking
// file descriptors.
- f, err := fs.OpenFile(inFile, os.O_RDONLY, 0)
+ f, err := aferoFS.OpenFile(inFile, os.O_RDONLY, 0)
if err != nil {
return nil, fmt.Errorf("failed to open %s: %w", inFile, err)
}
@@ -285,7 +383,7 @@ func openOutFile(filename string, dirMode, mode os.FileMode, modeOverride bool,
func createOutFile(filename string, dirMode, mode os.FileMode, modeOverride bool) (out io.WriteCloser, err error) {
mode = iohelpers.NormalizeFileMode(mode.Perm())
if modeOverride {
- err = fs.Chmod(filename, mode)
+ err = aferoFS.Chmod(filename, mode)
if err != nil && !os.IsNotExist(err) {
return nil, fmt.Errorf("failed to chmod output file '%s' with mode %q: %w", filename, mode, err)
}
@@ -293,11 +391,11 @@ func createOutFile(filename string, dirMode, mode os.FileMode, modeOverride bool
open := func() (out io.WriteCloser, err error) {
// Ensure file parent dirs
- if err = fs.MkdirAll(filepath.Dir(filename), dirMode); err != nil {
+ if err = aferoFS.MkdirAll(filepath.Dir(filename), dirMode); err != nil {
return nil, err
}
- out, err = fs.OpenFile(filename, os.O_RDWR|os.O_CREATE|os.O_TRUNC, mode)
+ out, err = aferoFS.OpenFile(filename, os.O_RDWR|os.O_CREATE|os.O_TRUNC, mode)
if err != nil {
return out, fmt.Errorf("failed to open output file '%s' for writing: %w", filename, err)
}
@@ -306,7 +404,7 @@ func createOutFile(filename string, dirMode, mode os.FileMode, modeOverride bool
}
// if the output file already exists, we'll use a SameSkipper
- fi, err := fs.Stat(filename)
+ fi, err := aferoFS.Stat(filename)
if err != nil {
// likely means the file just doesn't exist - further errors will be more useful
return iohelpers.LazyWriteCloser(open), nil
@@ -317,7 +415,7 @@ func createOutFile(filename string, dirMode, mode os.FileMode, modeOverride bool
}
out = iohelpers.SameSkipper(iohelpers.LazyReadCloser(func() (io.ReadCloser, error) {
- return fs.OpenFile(filename, os.O_RDONLY, mode)
+ return aferoFS.OpenFile(filename, os.O_RDONLY, mode)
}), open)
return out, err