move templates from rakyll/statik to go:embed

There is no need for the rakyll/statik package starting with Go 1.16,
which provides us with tools for embedding files
without third-party libraries.
This commit is contained in:
Dmitry Verkhoturov
2022-07-29 19:07:49 +02:00
parent 76f5ce32ca
commit 86d059bf99
21 changed files with 39 additions and 137 deletions
-1
View File
@@ -100,7 +100,6 @@ ADD backend/scripts/import.sh /usr/local/bin/import
RUN chmod +x /entrypoint.sh /usr/local/bin/backup /usr/local/bin/restore /usr/local/bin/import
COPY --from=build-backend /build/backend/remark42 /srv/remark42
COPY --from=build-backend /build/backend/templates /srv
COPY --from=build-frontend /srv/frontend/apps/remark42/public/ /srv/web/
COPY docker-init.sh /srv/init.sh
RUN chown -R app:app /srv
-2
View File
@@ -36,8 +36,6 @@ RUN \
export WEB_ROOT=/build/backend/web && \
find . -regex '.*\.\(html\|js\|mjs\)$' -print -exec sed -i "s|{% REMARK_URL %}|http://127.0.0.1:8080|g" {} \; && \
statik --src=${WEB_ROOT} --dest=/build/backend/app/rest -p api -f && \
statik --src=/build/backend/templates --dest=/build/backend/app -p templates -ns templates -f && \
ls -la /build/backend/app/templates/statik.go && \
ls -la /build/backend/app/rest/api/statik.go && \
ls -la /build/backend/web/
+2 -23
View File
@@ -877,11 +877,11 @@ func (s *ServerCommand) addAuthProviders(authenticator *auth.Service) error {
ContentType: s.Auth.Email.ContentType,
}
sndr := sender.NewEmailClient(params, log.Default())
tmpl, err := s.loadEmailTemplate()
tmpl, err := templates.Read(s.Auth.Email.MsgTemplate)
if err != nil {
return err
}
authenticator.AddVerifProvider("email", tmpl, sndr)
authenticator.AddVerifProvider("email", string(tmpl), sndr)
}
if s.Auth.Anonymous {
@@ -925,27 +925,6 @@ func (s *ServerCommand) addAuthProviders(authenticator *auth.Service) error {
return nil
}
// loadEmailTemplate trying to get template from statik
func (s *ServerCommand) loadEmailTemplate() (string, error) {
var file []byte
var err error
if s.Auth.Email.MsgTemplate == "email_confirmation_login.html.tmpl" {
fs := templates.NewFS()
file, err = fs.ReadFile(s.Auth.Email.MsgTemplate)
} else {
// deprecated loading from an external file, should be removed before v1.9.0
file, err = os.ReadFile(s.Auth.Email.MsgTemplate)
log.Printf("[INFO] template %s will be read from disk", s.Auth.Email.MsgTemplate)
}
if err != nil {
return "", fmt.Errorf("failed to read file %s: %w", s.Auth.Email.MsgTemplate, err)
}
return string(file), nil
}
// creates and registers telegram auth, which we need separately from other auth providers
func (s *ServerCommand) makeTelegramAuth(authenticator *auth.Service) providers.TGUpdatesReceiver {
if s.Auth.Telegram {
-13
View File
@@ -667,19 +667,6 @@ func TestServerAuthHooks(t *testing.T) {
client.CloseIdleConnections()
}
func TestServer_loadEmailTemplate(t *testing.T) {
cmd := ServerCommand{}
cmd.Auth.Email.MsgTemplate = "testdata/email.tmpl"
r, err := cmd.loadEmailTemplate()
assert.NoError(t, err)
assert.Equal(t, "The token is {{.Token}}", r)
cmd.Auth.Email.MsgTemplate = "badpath.tmpl"
r, err = cmd.loadEmailTemplate()
assert.EqualError(t, err, "failed to read file badpath.tmpl: open badpath.tmpl: no such file or directory")
assert.Equal(t, r, "")
}
func TestServerCommand_parseSameSite(t *testing.T) {
tbl := []struct {
inp string
+2 -3
View File
@@ -100,7 +100,6 @@ func NewEmail(emailParams EmailParams, smtpParams ntf.SMTPParams) (*Email, error
func (e *Email) setTemplates() error {
var err error
var msgTmplFile, verifyTmplFile []byte
fs := templates.NewFS()
if e.VerificationTemplatePath == "" {
e.VerificationTemplatePath = defaultEmailVerificationTemplatePath
@@ -110,10 +109,10 @@ func (e *Email) setTemplates() error {
e.MsgTemplatePath = defaultEmailTemplatePath
}
if msgTmplFile, err = fs.ReadFile(e.MsgTemplatePath); err != nil {
if msgTmplFile, err = templates.Read(e.MsgTemplatePath); err != nil {
return fmt.Errorf("can't read message template: %w", err)
}
if verifyTmplFile, err = fs.ReadFile(e.VerificationTemplatePath); err != nil {
if verifyTmplFile, err = templates.Read(e.VerificationTemplatePath); err != nil {
return fmt.Errorf("can't read verification template: %w", err)
}
if e.msgTmpl, err = template.New("msgTmpl").Parse(string(msgTmplFile)); err != nil {
+3 -17
View File
@@ -55,32 +55,18 @@ func Test_initTemplatesErr(t *testing.T) {
errText string
emailParams EmailParams
}{
{
name: "with wrong (default, working in prod) path to reply template",
errText: "can't read message template: open email_reply.html.tmpl: no such file or directory",
emailParams: EmailParams{},
},
{
name: "with wrong (default, working in prod) path to verification template",
errText: "can't read verification template: open email_confirmation_subscription.html.tmpl: no such file or directory",
emailParams: EmailParams{
MsgTemplatePath: "testdata/msg.html.tmpl",
},
},
{
name: "with wrong path to verification template",
errText: "can't read verification template: open notfound.tmpl: no such file or directory",
errText: "notfound.tmpl: file does not exist",
emailParams: EmailParams{
VerificationTemplatePath: "notfound.tmpl",
MsgTemplatePath: "testdata/msg.html.tmpl",
},
},
{
name: "with wrong path to message template",
errText: "can't read message template: open notfound.tmpl: no such file or directory",
errText: "notfound.tmpl: file does not exist",
emailParams: EmailParams{
VerificationTemplatePath: "testdata/verification.html.tmpl",
MsgTemplatePath: "notfound.tmpl",
MsgTemplatePath: "notfound.tmpl",
},
},
{
-2
View File
@@ -31,7 +31,6 @@ import (
"github.com/umputun/remark42/backend/app/store"
"github.com/umputun/remark42/backend/app/store/image"
"github.com/umputun/remark42/backend/app/store/service"
"github.com/umputun/remark42/backend/app/templates"
)
// Rest is a rest access server
@@ -377,7 +376,6 @@ func (s *Rest) controllerGroups() (public, private, admin, rss) {
telegramService: s.TelegramService,
remarkURL: s.RemarkURL,
anonVote: s.AnonVote,
templates: templates.NewFS(),
}
admGrp := admin{
+9 -16
View File
@@ -43,7 +43,6 @@ type private struct {
telegramService telegramService
remarkURL string
anonVote bool
templates templates.FileReader
}
// telegramService is a subset of Telegram service used for setting up user telegram notifications
@@ -473,29 +472,26 @@ func (s *private) setConfirmedEmailCtrl(w http.ResponseWriter, r *http.Request)
func (s *private) emailUnsubscribeCtrl(w http.ResponseWriter, r *http.Request) {
tkn := r.URL.Query().Get("tkn")
if tkn == "" {
rest.SendErrorHTML(w, r, http.StatusBadRequest,
fmt.Errorf("missing parameter"), "token parameter is required", rest.ErrInternal, s.templates)
rest.SendErrorHTML(w, r, http.StatusBadRequest, fmt.Errorf("missing parameter"), "token parameter is required", rest.ErrInternal)
return
}
siteID := r.URL.Query().Get("site")
confClaims, err := s.authenticator.TokenService().Parse(tkn)
if err != nil {
rest.SendErrorHTML(w, r, http.StatusForbidden, err, "failed to verify confirmation token", rest.ErrInternal, s.templates)
rest.SendErrorHTML(w, r, http.StatusForbidden, err, "failed to verify confirmation token", rest.ErrInternal)
return
}
if s.authenticator.TokenService().IsExpired(confClaims) {
rest.SendErrorHTML(w, r, http.StatusForbidden,
fmt.Errorf("expired"), "failed to verify confirmation token", rest.ErrInternal, s.templates)
rest.SendErrorHTML(w, r, http.StatusForbidden, fmt.Errorf("expired"), "failed to verify confirmation token", rest.ErrInternal)
return
}
// Handshake.ID is user.ID + "::" + address
elems := strings.Split(confClaims.Handshake.ID, "::")
if len(elems) != 2 {
rest.SendErrorHTML(w, r, http.StatusBadRequest,
fmt.Errorf("%s", confClaims.Handshake.ID), "invalid handshake token", rest.ErrInternal, s.templates)
rest.SendErrorHTML(w, r, http.StatusBadRequest, fmt.Errorf("%s", confClaims.Handshake.ID), "invalid handshake token", rest.ErrInternal)
return
}
userID := elems[0]
@@ -508,14 +504,11 @@ func (s *private) emailUnsubscribeCtrl(w http.ResponseWriter, r *http.Request) {
log.Printf("[WARN] can't read email for %s, %v", userID, err)
}
if existingAddress == "" {
rest.SendErrorHTML(w, r, http.StatusConflict,
fmt.Errorf("user is not subscribed"), "user does not have active email subscription", rest.ErrInternal, s.templates)
rest.SendErrorHTML(w, r, http.StatusConflict, fmt.Errorf("user is not subscribed"), "user does not have active email subscription", rest.ErrInternal)
return
}
if address != existingAddress {
rest.SendErrorHTML(w, r, http.StatusBadRequest,
fmt.Errorf("wrong email unsubscription"), "email address in request does not match known for this user",
rest.ErrInternal, s.templates)
rest.SendErrorHTML(w, r, http.StatusBadRequest, fmt.Errorf("wrong email unsubscription"), "email address in request does not match known for this user", rest.ErrInternal)
return
}
@@ -523,7 +516,7 @@ func (s *private) emailUnsubscribeCtrl(w http.ResponseWriter, r *http.Request) {
if err = s.dataService.DeleteUserDetail(siteID, userID, engine.UserEmail); err != nil {
code := parseError(err, rest.ErrInternal)
rest.SendErrorHTML(w, r, http.StatusBadRequest, err, "can't delete email for user", code, s.templates)
rest.SendErrorHTML(w, r, http.StatusBadRequest, err, "can't delete email for user", code)
return
}
// clean User.Email from the token, if user has the token
@@ -534,7 +527,7 @@ func (s *private) emailUnsubscribeCtrl(w http.ResponseWriter, r *http.Request) {
if claims.User != nil && claims.User.Email != "" {
claims.User.Email = ""
if _, err = s.authenticator.TokenService().Set(w, claims); err != nil {
rest.SendErrorHTML(w, r, http.StatusInternalServerError, err, "failed to set token", rest.ErrInternal, s.templates)
rest.SendErrorHTML(w, r, http.StatusInternalServerError, err, "failed to set token", rest.ErrInternal)
return
}
}
@@ -546,7 +539,7 @@ func (s *private) emailUnsubscribeCtrl(w http.ResponseWriter, r *http.Request) {
}
}
MustRead := func(path string) string {
file, err := s.templates.ReadFile(path)
file, err := templates.Read(path)
if err != nil {
panic(err)
}
@@ -600,17 +600,10 @@ func TestRest_AnonVote(t *testing.T) {
assert.Equal(t, map[string]store.VotedIPInfo(nil), cr.VotedIPs)
}
type MockFS struct{}
func (fs *MockFS) ReadFile(path string) ([]byte, error) {
return []byte(fmt.Sprintf("template %s", path)), nil
}
func TestRest_EmailAndTelegram(t *testing.T) {
ts, srv, teardown := startupT(t)
defer teardown()
srv.privRest.templates = &MockFS{}
srv.privRest.telegramService = &mockTelegram{site: "remark42"}
// issue good token
+3 -3
View File
@@ -48,9 +48,9 @@ type errTmplData struct {
Details string
}
// SendErrorHTML makes html body with provided template and responds with provided http status code,
// SendErrorHTML makes html body from error_response.html.tmpl template and responds with provided http status code,
// error code is not included in render as it is intended for UI developers and not for the users
func SendErrorHTML(w http.ResponseWriter, r *http.Request, httpStatusCode int, err error, details string, errCode int, t templates.FileReader) {
func SendErrorHTML(w http.ResponseWriter, r *http.Request, httpStatusCode int, err error, details string, errCode int) {
// MustExecute behaves like template.Execute, but panics if an error occurs.
MustExecute := func(tmpl *template.Template, wr io.Writer, data interface{}) {
if err = tmpl.Execute(wr, data); err != nil {
@@ -58,7 +58,7 @@ func SendErrorHTML(w http.ResponseWriter, r *http.Request, httpStatusCode int, e
}
}
MustRead := func(path string) string {
file, e := t.ReadFile(path)
file, e := templates.Read(path)
if e != nil {
panic(e)
}
+1 -8
View File
@@ -36,18 +36,11 @@ func TestSendErrorJSON(t *testing.T) {
assert.Equal(t, `{"code":123,"details":"error details 123456","error":"error 500"}`+"\n", string(body))
}
type MockFS struct{}
func (fs *MockFS) ReadFile(path string) ([]byte, error) {
return []byte(fmt.Sprintf("{{.Error}}{{.Details}} %s", path)), nil
}
func TestSendErrorHTML(t *testing.T) {
fs := &MockFS{}
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/error" {
t.Log("http err request", r.URL)
SendErrorHTML(w, r, 500, fmt.Errorf("error 500"), "error details 123456", 987, fs)
SendErrorHTML(w, r, 500, fmt.Errorf("error 500"), "error details 123456", 987)
return
}
w.WriteHeader(404)
+1 -1
View File
@@ -4,7 +4,7 @@ import (
"net/url"
"strings"
bfchroma "github.com/Depado/bfchroma/v2"
"github.com/Depado/bfchroma/v2"
"github.com/PuerkitoBio/goquery"
"github.com/alecthomas/chroma/v2/formatters/html"
bf "github.com/russross/blackfriday/v2"
+13 -27
View File
@@ -1,38 +1,24 @@
package templates
import (
"net/http"
"embed"
"io/fs"
"os"
"path/filepath"
log "github.com/go-pkgz/lgr"
"github.com/rakyll/statik/fs"
)
// FS stores link to statikFS if it exists
type FS struct {
statik http.FileSystem
}
//go:embed static
var templateFS embed.FS
// FileReader describes methods of filesystem
type FileReader interface {
ReadFile(path string) ([]byte, error)
}
// NewFS returns new FS instance, which will read from statik if it's available and from fs otherwise
func NewFS() *FS {
f := &FS{}
if statikFS, err := fs.NewWithNamespace("templates"); err == nil {
log.Printf("[INFO] templates will be read from statik")
f.statik = statikFS
// Read reads either template from disk if it exists, or from embedded template
func Read(path string) ([]byte, error) {
if _, err := os.Stat(filepath.Clean(path)); err == nil {
return os.ReadFile(filepath.Clean(path))
}
return f
}
// ReadFile depends on statik achieve exists
func (f *FS) ReadFile(path string) ([]byte, error) {
if f.statik != nil {
return fs.ReadFile(f.statik, filepath.Join("/", path)) //nolint:gocritic // root folder is a requirement for statik
// remove "static/" prefix from path
var contentFS, err = fs.Sub(templateFS, "static")
if err != nil {
return nil, err
}
return os.ReadFile(filepath.Clean(path))
return fs.ReadFile(contentFS, filepath.Clean(path))
}
+3 -10
View File
@@ -6,19 +6,12 @@ import (
"github.com/stretchr/testify/assert"
)
func TestNewFS(t *testing.T) {
fs := NewFS()
assert.NotNil(t, &fs)
}
func TestFS_ReadFile(t *testing.T) {
fs := NewFS()
file, err := fs.ReadFile("testdata/template.html.tmpl")
func Test_Read(t *testing.T) {
file, err := Read("testdata/template.html.tmpl")
assert.NoError(t, err)
assert.Equal(t, []byte("template\n"), file)
file, err = fs.ReadFile("testdata/bad_path.html.tmpl")
file, err = Read("testdata/bad_path.html.tmpl")
assert.Error(t, err)
assert.Nil(t, file)
}
+1 -1
View File
@@ -190,7 +190,7 @@ After you set `SMTP_` variables, you can allow email authentication by setting t
## HTML templates for emails and error messages
Remark42 uses golang templates for email templating. Templates are located in `backend/templates` and embedded into binary by statik
Remark42 uses golang templates for email templating. Templates are located in `backend/app/templates/static` and embedded into binary by `go:embed` [directive](https://pkg.go.dev/embed).
Now we have the following templates:
+1 -3
View File
@@ -30,7 +30,7 @@ Run tests in your IDE, and re-run `make rundev` each time you want to see how yo
You have to [install](https://golang.org/doc/install) the latest stable `go` toolchain to run the backend locally.
In order to have working Remark42 installation you need once to copy frontend static files to `./backend/web` directory from `master` docker image, and also copy files from `./templates` to the `./backend` as they are expected to be where application starts:
In order to have working Remark42 installation you need once to copy frontend static files to `./backend/web` directory from `master` docker image, as it is expected to be where application compiles:
```shell
# frontend files
@@ -38,8 +38,6 @@ docker pull umputun/remark42:master
docker create -ti --name remark42files umputun/remark42:master sh
docker cp remark42files:/srv/web/ ./backend/
docker rm -f remark42files
# template files
cp ./backend/templates/* ./backend
# fix frontend files to point to the right URL
## Mac version
find -E ./backend/web -regex '.*\.(html|js|mjs)$' -print -exec sed -i '' "s|{% REMARK_URL %}|http://127.0.0.1:8080|g" {} \;