Mono repo -> Master (#22553)
Combines the following repositories into one: https://github.com/mattermost/mattermost-server https://github.com/mattermost/mattermost-webapp https://github.com/mattermost/focalboard https://github.com/mattermost/mattermost-plugin-playbooks
Этот коммит содержится в:
155
server/platform/shared/templates/templates.go
Обычный файл
155
server/platform/shared/templates/templates.go
Обычный файл
@@ -0,0 +1,155 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package templates
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"html/template"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
|
||||
"github.com/fsnotify/fsnotify"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/server/channels/utils/fileutils"
|
||||
)
|
||||
|
||||
// Container represents a set of templates that can be render
|
||||
type Container struct {
|
||||
templates *template.Template
|
||||
mutex sync.RWMutex
|
||||
stop chan struct{}
|
||||
stopped chan struct{}
|
||||
watch bool
|
||||
}
|
||||
|
||||
// Data contains the data used to populate the template variables, it has Props
|
||||
// that can be of any type and HTML that only can be `template.HTML` types.
|
||||
type Data struct {
|
||||
Props map[string]any
|
||||
HTML map[string]template.HTML
|
||||
}
|
||||
|
||||
func GetTemplateDirectory() (string, bool) {
|
||||
templatesDir := "templates"
|
||||
if mattermostPath := os.Getenv("MM_SERVER_PATH"); mattermostPath != "" {
|
||||
templatesDir = filepath.Join(mattermostPath, templatesDir)
|
||||
}
|
||||
|
||||
return fileutils.FindDir(templatesDir)
|
||||
}
|
||||
|
||||
// NewFromTemplates creates a new templates container using a
|
||||
// `template.Template` object
|
||||
func NewFromTemplate(templates *template.Template) *Container {
|
||||
return &Container{templates: templates}
|
||||
}
|
||||
|
||||
// New creates a new templates container scanning a directory.
|
||||
func New(directory string) (*Container, error) {
|
||||
c := &Container{}
|
||||
|
||||
htmlTemplates, err := template.ParseGlob(filepath.Join(directory, "*.html"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c.templates = htmlTemplates
|
||||
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// NewWithWatcher creates a new templates container scanning a directory and
|
||||
// watch the directory filesystem changes to apply them to the loaded
|
||||
// templates. This function returns the container and an errors channel to pass
|
||||
// all errors that can happen during the watch process, or an regular error if
|
||||
// we fail to create the templates or the watcher. The caller must consume the
|
||||
// returned errors channel to ensure not blocking the watch process.
|
||||
func NewWithWatcher(directory string) (*Container, <-chan error, error) {
|
||||
htmlTemplates, err := template.ParseGlob(filepath.Join(directory, "*.html"))
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
watcher, err := fsnotify.NewWatcher()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
err = watcher.Add(directory)
|
||||
if err != nil {
|
||||
watcher.Close()
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
c := &Container{
|
||||
templates: htmlTemplates,
|
||||
watch: true,
|
||||
stop: make(chan struct{}),
|
||||
stopped: make(chan struct{}),
|
||||
}
|
||||
errors := make(chan error)
|
||||
|
||||
go func() {
|
||||
defer close(errors)
|
||||
defer close(c.stopped)
|
||||
defer watcher.Close()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-c.stop:
|
||||
return
|
||||
case event := <-watcher.Events:
|
||||
if event.Op&fsnotify.Write == fsnotify.Write {
|
||||
if htmlTemplates, err := template.ParseGlob(filepath.Join(directory, "*.html")); err != nil {
|
||||
errors <- err
|
||||
} else {
|
||||
c.mutex.Lock()
|
||||
c.templates = htmlTemplates
|
||||
c.mutex.Unlock()
|
||||
}
|
||||
}
|
||||
case err := <-watcher.Errors:
|
||||
errors <- err
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return c, errors, nil
|
||||
}
|
||||
|
||||
// Close stops the templates watcher of the container in case you have created
|
||||
// it with watch parameter set to true
|
||||
func (c *Container) Close() {
|
||||
c.mutex.RLock()
|
||||
defer c.mutex.RUnlock()
|
||||
if c.watch {
|
||||
close(c.stop)
|
||||
<-c.stopped
|
||||
}
|
||||
}
|
||||
|
||||
// RenderToString renders the template referenced with the template name using
|
||||
// the data provided and return a string with the result
|
||||
func (c *Container) RenderToString(templateName string, data Data) (string, error) {
|
||||
var text bytes.Buffer
|
||||
if err := c.Render(&text, templateName, data); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return text.String(), nil
|
||||
}
|
||||
|
||||
// RenderToString renders the template referenced with the template name using
|
||||
// the data provided and write it to the writer provided
|
||||
func (c *Container) Render(w io.Writer, templateName string, data Data) error {
|
||||
c.mutex.RLock()
|
||||
htmlTemplates := c.templates
|
||||
c.mutex.RUnlock()
|
||||
|
||||
if err := htmlTemplates.ExecuteTemplate(w, templateName, data); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
114
server/platform/shared/templates/templates_test.go
Обычный файл
114
server/platform/shared/templates/templates_test.go
Обычный файл
@@ -0,0 +1,114 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package templates
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"html/template"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestHTMLTemplateWatcher(t *testing.T) {
|
||||
dir, err := os.MkdirTemp("", "")
|
||||
require.NoError(t, err)
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
require.NoError(t, os.Mkdir(filepath.Join(dir, "templates"), 0700))
|
||||
require.NoError(t, os.WriteFile(filepath.Join(dir, "templates", "foo.html"), []byte(`{{ define "foo" }}foo{{ end }}`), 0600))
|
||||
|
||||
prevDir, err := os.Getwd()
|
||||
require.NoError(t, err)
|
||||
defer os.Chdir(prevDir)
|
||||
os.Chdir(dir)
|
||||
|
||||
watcher, errChan, err := NewWithWatcher("templates")
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, watcher)
|
||||
select {
|
||||
case msg := <-errChan:
|
||||
err = msg
|
||||
default:
|
||||
err = nil
|
||||
}
|
||||
require.NoError(t, err)
|
||||
defer watcher.Close()
|
||||
|
||||
text, err := watcher.RenderToString("foo", Data{})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "foo", text)
|
||||
|
||||
require.NoError(t, os.WriteFile(filepath.Join(dir, "templates", "foo.html"), []byte(`{{ define "foo" }}bar{{ end }}`), 0600))
|
||||
|
||||
require.Eventually(t, func() bool {
|
||||
text, err := watcher.RenderToString("foo", Data{})
|
||||
return text == "bar" && err == nil
|
||||
}, time.Millisecond*1000, time.Millisecond*50)
|
||||
}
|
||||
|
||||
func TestNewWithWatcher_BadDirectory(t *testing.T) {
|
||||
watcher, errChan, err := NewWithWatcher("notarealdirectory")
|
||||
require.Error(t, err)
|
||||
assert.Nil(t, watcher)
|
||||
assert.Nil(t, errChan)
|
||||
}
|
||||
|
||||
func TestNew_BadDirectory(t *testing.T) {
|
||||
watcher, err := New("notarealdirectory")
|
||||
assert.Nil(t, watcher)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestRender(t *testing.T) {
|
||||
tpl := template.New("test")
|
||||
_, err := tpl.Parse(`{{ define "foo" }}foo{{ .Props.Bar }}{{ end }}`)
|
||||
require.NoError(t, err)
|
||||
mt := NewFromTemplate(tpl)
|
||||
|
||||
data := Data{
|
||||
Props: map[string]any{
|
||||
"Bar": "bar",
|
||||
},
|
||||
}
|
||||
text, err := mt.RenderToString("foo", data)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "foobar", text)
|
||||
|
||||
buf := &bytes.Buffer{}
|
||||
require.NoError(t, mt.Render(buf, "foo", data))
|
||||
assert.Equal(t, "foobar", buf.String())
|
||||
}
|
||||
|
||||
func TestRenderError(t *testing.T) {
|
||||
tpl := template.New("test")
|
||||
_, err := tpl.Parse(`{{ define "foo" }}foo{{ .Foo.Bar }}bar{{ end }}`)
|
||||
require.NoError(t, err)
|
||||
mt := NewFromTemplate(tpl)
|
||||
|
||||
text, err := mt.RenderToString("foo", Data{})
|
||||
require.Error(t, err)
|
||||
assert.Equal(t, "", text)
|
||||
|
||||
buf := &bytes.Buffer{}
|
||||
assert.Error(t, mt.Render(buf, "foo", Data{}))
|
||||
assert.Equal(t, "foo", buf.String())
|
||||
}
|
||||
|
||||
func TestRenderUnknownTemplate(t *testing.T) {
|
||||
tpl := template.New("")
|
||||
mt := NewFromTemplate(tpl)
|
||||
|
||||
text, err := mt.RenderToString("foo", Data{})
|
||||
require.Error(t, err)
|
||||
assert.Equal(t, "", text)
|
||||
|
||||
buf := &bytes.Buffer{}
|
||||
assert.Error(t, mt.Render(buf, "foo", Data{}))
|
||||
assert.Equal(t, "", buf.String())
|
||||
}
|
||||
Ссылка в новой задаче
Block a user