Skip to content
Permalink
Browse files
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.
  • Loading branch information
Dmitry Verkhoturov committed Jul 29, 2022
1 parent 76f5ce3 commit 86d059bf9970bd136b71a246babd4f907790b70f
Show file tree
Hide file tree
Showing 21 changed files with 39 additions and 137 deletions.
@@ -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
@@ -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/

@@ -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 {
@@ -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
@@ -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 {
@@ -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",
},
},
{
@@ -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{
@@ -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,22 +504,19 @@ 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
}

log.Printf("[DEBUG] unsubscribe user %s", userID)

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
@@ -48,17 +48,17 @@ 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 {
panic(err)
}
}
MustRead := func(path string) string {
file, e := t.ReadFile(path)
file, e := templates.Read(path)
if e != nil {
panic(e)
}
@@ -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)
@@ -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"
File renamed without changes.
File renamed without changes.
@@ -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
}

// FileReader describes methods of filesystem
type FileReader interface {
ReadFile(path string) ([]byte, error)
}
//go:embed static
var templateFS embed.FS

// 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))
}
@@ -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)
}
@@ -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:

0 comments on commit 86d059b

Please sign in to comment.