Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 5 additions & 6 deletions cmd/web.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
"code.gitea.io/gitea/modules/process"
"code.gitea.io/gitea/modules/public"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/templates"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/routers"
"code.gitea.io/gitea/routers/install"
Expand Down Expand Up @@ -249,12 +250,6 @@ func servePprof() {
}

func runWeb(ctx context.Context, cmd *cli.Command) error {
defer func() {
if panicked := recover(); panicked != nil {
log.Fatal("PANIC: %v\n%s", panicked, log.Stack(2))
}
}()

if subCmdName, valid := isValidDefaultSubCommand(cmd); !valid {
return fmt.Errorf("unknown command: %s", subCmdName)
}
Expand All @@ -274,6 +269,10 @@ func runWeb(ctx context.Context, cmd *cli.Command) error {
createPIDFile(cmd.String("pid"))
}

// init the HTML renderer and load templates, if error happens, it will report the error immediately and exit with error log
// in dev mode, it won't exit, but watch the template files for changes
_ = templates.PageRenderer()
Comment thread
wxiaoguang marked this conversation as resolved.

if !setting.InstallLock {
if err := serveInstall(cmd); err != nil {
return err
Expand Down
50 changes: 21 additions & 29 deletions modules/assetfs/layered.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,7 @@ package assetfs
import (
"context"
"fmt"
"io"
"io/fs"
"net/http"
"os"
"path/filepath"
"sort"
Expand All @@ -25,7 +23,7 @@ import (
// Layer represents a layer in a layered asset file-system. It has a name and works like http.FileSystem
type Layer struct {
name string
fs http.FileSystem
fs fs.FS
localPath string
}

Expand All @@ -34,7 +32,7 @@ func (l *Layer) Name() string {
}

// Open opens the named file. The caller is responsible for closing the file.
func (l *Layer) Open(name string) (http.File, error) {
func (l *Layer) Open(name string) (fs.File, error) {
return l.fs.Open(name)
}

Expand All @@ -48,12 +46,12 @@ func Local(name, base string, sub ...string) *Layer {
panic(fmt.Sprintf("Unable to get absolute path for %q: %v", base, err))
}
root := util.FilePathJoinAbs(base, sub...)
return &Layer{name: name, fs: http.Dir(root), localPath: root}
return &Layer{name: name, fs: os.DirFS(root), localPath: root}
}

// Bindata returns a new Layer with the given name, it serves files from the given bindata asset.
func Bindata(name string, fs fs.FS) *Layer {
return &Layer{name: name, fs: http.FS(fs)}
return &Layer{name: name, fs: fs}
}

// LayeredFS is a layered asset file-system. It works like http.FileSystem, but it can have multiple layers.
Expand All @@ -69,7 +67,7 @@ func Layered(layers ...*Layer) *LayeredFS {
}

// Open opens the named file. The caller is responsible for closing the file.
func (l *LayeredFS) Open(name string) (http.File, error) {
func (l *LayeredFS) Open(name string) (fs.File, error) {
for _, layer := range l.layers {
f, err := layer.Open(name)
if err == nil || !os.IsNotExist(err) {
Expand All @@ -89,40 +87,34 @@ func (l *LayeredFS) ReadFile(elems ...string) ([]byte, error) {
func (l *LayeredFS) ReadLayeredFile(elems ...string) ([]byte, string, error) {
name := util.PathJoinRel(elems...)
for _, layer := range l.layers {
f, err := layer.Open(name)
bs, err := fs.ReadFile(layer, name)
if os.IsNotExist(err) {
continue
} else if err != nil {
return nil, layer.name, err
}
bs, err := io.ReadAll(f)
_ = f.Close()
return bs, layer.name, err
}
return nil, "", fs.ErrNotExist
}

func shouldInclude(info fs.FileInfo, fileMode ...bool) bool {
if util.IsCommonHiddenFileName(info.Name()) {
func shouldInclude(dirEntry fs.DirEntry, fileMode ...bool) bool {
if util.IsCommonHiddenFileName(dirEntry.Name()) {
return false
}
if len(fileMode) == 0 {
return true
} else if len(fileMode) == 1 {
return fileMode[0] == !info.Mode().IsDir()
return fileMode[0] == !dirEntry.IsDir()
}
panic("too many arguments for fileMode in shouldInclude")
}

func readDir(layer *Layer, name string) ([]fs.FileInfo, error) {
f, err := layer.Open(name)
if os.IsNotExist(err) {
func readDirOptional(layer *Layer, name string) (entries []fs.DirEntry, err error) {
if entries, err = fs.ReadDir(layer, name); os.IsNotExist(err) {
return nil, nil
} else if err != nil {
return nil, err
}
defer f.Close()
return f.Readdir(-1)
return entries, err
}

// ListFiles lists files/directories in the given directory. The fileMode controls the returned files.
Expand All @@ -133,13 +125,13 @@ func readDir(layer *Layer, name string) ([]fs.FileInfo, error) {
func (l *LayeredFS) ListFiles(name string, fileMode ...bool) ([]string, error) {
fileSet := make(container.Set[string])
for _, layer := range l.layers {
infos, err := readDir(layer, name)
entries, err := readDirOptional(layer, name)
if err != nil {
return nil, err
}
for _, info := range infos {
if shouldInclude(info, fileMode...) {
fileSet.Add(info.Name())
for _, entry := range entries {
if shouldInclude(entry, fileMode...) {
fileSet.Add(entry.Name())
}
}
}
Expand All @@ -163,16 +155,16 @@ func listAllFiles(layers []*Layer, name string, fileMode ...bool) ([]string, err
var list func(dir string) error
list = func(dir string) error {
for _, layer := range layers {
infos, err := readDir(layer, dir)
entries, err := readDirOptional(layer, dir)
if err != nil {
return err
}
for _, info := range infos {
path := util.PathJoinRelX(dir, info.Name())
if shouldInclude(info, fileMode...) {
for _, entry := range entries {
path := util.PathJoinRelX(dir, entry.Name())
if shouldInclude(entry, fileMode...) {
fileSet.Add(path)
}
if info.IsDir() {
if entry.IsDir() {
if err = list(path); err != nil {
return err
}
Expand Down
2 changes: 1 addition & 1 deletion modules/public/public.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ func FileHandlerFunc() http.HandlerFunc {
resp.WriteHeader(http.StatusMethodNotAllowed)
return
}
handleRequest(resp, req, assetFS, req.URL.Path)
handleRequest(resp, req, http.FS(assetFS), req.URL.Path)
}
}

Expand Down
23 changes: 0 additions & 23 deletions modules/templates/base.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,6 @@
package templates

import (
"slices"
"strings"

"code.gitea.io/gitea/modules/assetfs"
"code.gitea.io/gitea/modules/setting"
)
Expand All @@ -18,23 +15,3 @@ func AssetFS() *assetfs.LayeredFS {
func CustomAssets() *assetfs.Layer {
return assetfs.Local("custom", setting.CustomPath, "templates")
}

func ListWebTemplateAssetNames(assets *assetfs.LayeredFS) ([]string, error) {
files, err := assets.ListAllFiles(".", true)
if err != nil {
return nil, err
}
return slices.DeleteFunc(files, func(file string) bool {
return strings.HasPrefix(file, "mail/") || !strings.HasSuffix(file, ".tmpl")
}), nil
}

func ListMailTemplateAssetNames(assets *assetfs.LayeredFS) ([]string, error) {
files, err := assets.ListAllFiles(".", true)
if err != nil {
return nil, err
}
return slices.DeleteFunc(files, func(file string) bool {
return !strings.HasPrefix(file, "mail/") || !strings.HasSuffix(file, ".tmpl")
}), nil
}
2 changes: 0 additions & 2 deletions modules/templates/helper.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,6 @@ import (
// NewFuncMap returns functions for injecting to templates
func NewFuncMap() template.FuncMap {
return map[string]any{
"ctx": func() any { return nil }, // template context function

"DumpVar": dumpVar,
"NIL": func() any { return nil },

Expand Down
107 changes: 19 additions & 88 deletions modules/templates/htmlrenderer.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,18 @@ package templates
import (
"bufio"
"bytes"
"context"
"errors"
"fmt"
"html/template"
"io"
"net/http"
"path/filepath"
"regexp"
"strconv"
"strings"
"sync"
"sync/atomic"
texttemplate "text/template"

"code.gitea.io/gitea/modules/assetfs"
"code.gitea.io/gitea/modules/graceful"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/templates/scopedtmpl"
Expand All @@ -31,58 +28,27 @@ type TemplateExecutor scopedtmpl.TemplateExecutor

type TplName string

type HTMLRender struct {
type tmplRender struct {
templates atomic.Pointer[scopedtmpl.ScopedTemplate]
}

var (
htmlRender *HTMLRender
htmlRenderOnce sync.Once
)

var ErrTemplateNotInitialized = errors.New("template system is not initialized, check your log for errors")

func (h *HTMLRender) HTML(w io.Writer, status int, tplName TplName, data any, ctx context.Context) error { //nolint:revive // we don't use ctx, only pass it to the template executor
name := string(tplName)
if respWriter, ok := w.(http.ResponseWriter); ok {
if respWriter.Header().Get("Content-Type") == "" {
respWriter.Header().Set("Content-Type", "text/html; charset=utf-8")
}
respWriter.WriteHeader(status)
}
t, err := h.TemplateLookup(name, ctx)
if err != nil {
return texttemplate.ExecError{Name: name, Err: err}
}
return t.Execute(w, data)
collectTemplateNames func() ([]string, error)
readTemplateContent func(name string) ([]byte, error)
}

func (h *HTMLRender) TemplateLookup(name string, ctx context.Context) (TemplateExecutor, error) { //nolint:revive // we don't use ctx, only pass it to the template executor
tmpls := h.templates.Load()
if tmpls == nil {
return nil, ErrTemplateNotInitialized
}
m := NewFuncMap()
m["ctx"] = func() any { return ctx }
return tmpls.Executor(name, m)
func (h *tmplRender) Templates() *scopedtmpl.ScopedTemplate {
return h.templates.Load()
}

func (h *HTMLRender) CompileTemplates() error {
assets := AssetFS()
extSuffix := ".tmpl"
func (h *tmplRender) recompileTemplates(dummyFuncMap template.FuncMap) error {
tmpls := scopedtmpl.NewScopedTemplate()
tmpls.Funcs(NewFuncMap())
files, err := ListWebTemplateAssetNames(assets)
tmpls.Funcs(dummyFuncMap)
names, err := h.collectTemplateNames()
if err != nil {
return nil
return err
}
for _, file := range files {
if !strings.HasSuffix(file, extSuffix) {
continue
}
name := strings.TrimSuffix(file, extSuffix)
for _, name := range names {
tmpl := tmpls.New(filepath.ToSlash(name))
buf, err := assets.ReadFile(file)
buf, err := h.readTemplateContent(name)
if err != nil {
return err
}
Expand All @@ -95,55 +61,20 @@ func (h *HTMLRender) CompileTemplates() error {
return nil
}

// HTMLRenderer init once and returns the globally shared html renderer
func HTMLRenderer() *HTMLRender {
htmlRenderOnce.Do(initHTMLRenderer)
return htmlRender
}

func ReloadHTMLTemplates() error {
log.Trace("Reloading HTML templates")
if err := htmlRender.CompileTemplates(); err != nil {
log.Error("Template error: %v\n%s", err, log.Stack(2))
return err
}
return nil
}

func initHTMLRenderer() {
rendererType := "static"
if !setting.IsProd {
rendererType = "auto-reloading"
}
log.Debug("Creating %s HTML Renderer", rendererType)

htmlRender = &HTMLRender{}
if err := htmlRender.CompileTemplates(); err != nil {
p := &templateErrorPrettier{assets: AssetFS()}
wrapTmplErrMsg(p.handleFuncNotDefinedError(err))
wrapTmplErrMsg(p.handleUnexpectedOperandError(err))
wrapTmplErrMsg(p.handleExpectedEndError(err))
wrapTmplErrMsg(p.handleGenericTemplateError(err))
wrapTmplErrMsg(fmt.Sprintf("CompileTemplates error: %v", err))
}

if !setting.IsProd {
go AssetFS().WatchLocalChanges(graceful.GetManager().ShutdownContext(), func() {
_ = ReloadHTMLTemplates()
})
}
func ReloadAllTemplates() error {
return errors.Join(PageRendererReload(), MailRendererReload())
}

func wrapTmplErrMsg(msg string) {
if msg == "" {
func processStartupTemplateError(err error) {
if err == nil {
return
}
if setting.IsProd {
if setting.IsProd || setting.IsInTesting {
// in prod mode, Gitea must have correct templates to run
log.Fatal("Gitea can't run with template errors: %s", msg)
log.Fatal("Gitea can't run with template errors: %v", err)
}
// in dev mode, do not need to really exit, because the template errors could be fixed by developer soon and the templates get reloaded
log.Error("There are template errors but Gitea continues to run in dev mode: %s", msg)
log.Error("There are template errors but Gitea continues to run in dev mode: %v", err)
}

type templateErrorPrettier struct {
Expand Down
Loading