Previously when rewriting specific template files they were not overwritten properly. This commit updates the behaviour to use the right os.Open flags.
110 lines
3.2 KiB
Go
110 lines
3.2 KiB
Go
package distillery
|
|
|
|
import (
|
|
"io"
|
|
"io/fs"
|
|
"os"
|
|
"regexp"
|
|
"strings"
|
|
|
|
"github.com/pkg/errors"
|
|
"golang.org/x/exp/maps"
|
|
"golang.org/x/exp/slices"
|
|
)
|
|
|
|
var templateRegexp = regexp.MustCompile(`\${[^}]+}`)
|
|
|
|
// InstallTemplates open the resource src, and installs it into dst.
|
|
// the template resource must fit into memory.
|
|
//
|
|
// For each variable ${THING} inside dest, a key 'THING' must exist in context.
|
|
// Extra or missing template keys are an error.
|
|
func InstallTemplate(dst, src string, context map[string]string) error {
|
|
bytes, srcMode, err := doTemplate(src, context)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// determine if we need to create the destination file, or if it already exists
|
|
dstStat, dstErr := os.Stat(dst)
|
|
switch {
|
|
case os.IsNotExist(dstErr):
|
|
case dstErr != nil:
|
|
return errors.Wrapf(dstErr, "Error calling stat on destination %s", dst)
|
|
case dstStat.IsDir():
|
|
return errors.Wrapf(errExpectedFileButGotDirectory, "Error processing destination %s", dst)
|
|
}
|
|
|
|
// open and write the destination file
|
|
dstFile, err := os.OpenFile(dst, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, srcMode)
|
|
if err != nil {
|
|
return errors.Wrapf(err, "Unable to open file %s", dst)
|
|
}
|
|
_, err = dstFile.Write(bytes)
|
|
return errors.Wrapf(err, "Unable to write destination %s", dst)
|
|
}
|
|
|
|
// ReadTemplate is like InstallTemplate, except that it writes template into a byte slice and returns it.
|
|
func ReadTemplate(src string, context map[string]string) ([]byte, error) {
|
|
bytes, _, err := doTemplate(src, context)
|
|
return bytes, err
|
|
}
|
|
|
|
func doTemplate(src string, context map[string]string) (bytes []byte, mode fs.FileMode, err error) {
|
|
// open the source file!
|
|
srcFile, err := resourceEmbed.Open(src)
|
|
if err != nil {
|
|
return nil, mode, errors.Wrapf(err, "Error opening source file %s", src)
|
|
}
|
|
defer srcFile.Close()
|
|
|
|
// stat the source file to install
|
|
srcStat, srcErr := srcFile.Stat()
|
|
if srcErr != nil {
|
|
return nil, mode, errors.Wrapf(srcErr, "Error calling stat on source %s", src)
|
|
}
|
|
|
|
// should not be a directory
|
|
if srcStat.IsDir() {
|
|
return nil, mode, errors.Wrapf(errExpectedFileButGotDirectory, "Error calling stat on source %s", src)
|
|
}
|
|
|
|
// read the template and replace
|
|
templates, err := io.ReadAll(srcFile)
|
|
if err != nil {
|
|
return nil, mode, errors.Wrapf(err, "Unable to read src file %s", src)
|
|
}
|
|
|
|
// keep track of context keys that have not been used
|
|
unuusedContext := make(map[string]struct{}, len(context))
|
|
for key := range context {
|
|
unuusedContext[key] = struct{}{}
|
|
}
|
|
|
|
// replace the template regexp
|
|
// keeping track of unuused errors
|
|
var hadError error
|
|
templates = templateRegexp.ReplaceAllFunc(templates, func(b []byte) []byte {
|
|
name := string(b[2 : len(b)-1]) // remove the leading ${ and trailing }
|
|
delete(unuusedContext, name) // mark the key as having been read
|
|
|
|
value, ok := context[name]
|
|
if hadError != nil && !ok {
|
|
hadError = errors.Errorf("key %s missing in context", name)
|
|
}
|
|
return []byte(value)
|
|
})
|
|
|
|
if hadError != nil {
|
|
return nil, mode, hadError
|
|
}
|
|
|
|
if len(unuusedContext) != 0 {
|
|
keys := maps.Keys(unuusedContext)
|
|
slices.Sort(keys)
|
|
return nil, mode, errors.Errorf("additional keys %s in context", strings.Join(keys, ","))
|
|
}
|
|
|
|
// return the data and the mode!
|
|
return templates, srcStat.Mode(), nil
|
|
}
|