diff --git a/cmd/embedded.go b/cmd/embedded.go
index 30fc7103d..ffdc3d6a6 100644
--- a/cmd/embedded.go
+++ b/cmd/embedded.go
@@ -123,7 +123,7 @@ func initEmbeddedExtractor(c *cli.Context) error {
sections["public"] = §ion{Path: "public", Names: public.AssetNames, IsDir: public.AssetIsDir, Asset: public.Asset}
sections["options"] = §ion{Path: "options", Names: options.AssetNames, IsDir: options.AssetIsDir, Asset: options.Asset}
- sections["templates"] = §ion{Path: "templates", Names: templates.AssetNames, IsDir: templates.AssetIsDir, Asset: templates.Asset}
+ sections["templates"] = §ion{Path: "templates", Names: templates.BuiltinAssetNames, IsDir: templates.BuiltinAssetIsDir, Asset: templates.BuiltinAsset}
for _, sec := range sections {
assets = append(assets, buildAssetList(sec, pats, c)...)
diff --git a/cmd/web.go b/cmd/web.go
index 43f106f78..e09560bb8 100644
--- a/cmd/web.go
+++ b/cmd/web.go
@@ -126,8 +126,10 @@ func runWeb(ctx *cli.Context) error {
return err
}
}
- c := install.Routes()
+ installCtx, cancel := context.WithCancel(graceful.GetManager().HammerContext())
+ c := install.Routes(installCtx)
err := listen(c, false)
+ cancel()
if err != nil {
log.Critical("Unable to open listener for installer. Is Gitea already running?")
graceful.GetManager().DoGracefulShutdown()
@@ -175,7 +177,7 @@ func runWeb(ctx *cli.Context) error {
}
// Set up Chi routes
- c := routers.NormalRoutes()
+ c := routers.NormalRoutes(graceful.GetManager().HammerContext())
err := listen(c, true)
<-graceful.GetManager().Done()
log.Info("PID: %d Gitea Web Finished", os.Getpid())
diff --git a/contrib/pr/checkout.go b/contrib/pr/checkout.go
index 65762a91e..900b44d16 100644
--- a/contrib/pr/checkout.go
+++ b/contrib/pr/checkout.go
@@ -27,6 +27,7 @@ import (
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/unittest"
gitea_git "code.gitea.io/gitea/modules/git"
+ "code.gitea.io/gitea/modules/graceful"
"code.gitea.io/gitea/modules/markup"
"code.gitea.io/gitea/modules/markup/external"
repo_module "code.gitea.io/gitea/modules/repository"
@@ -117,7 +118,7 @@ func runPR() {
// routers.GlobalInit()
external.RegisterRenderers()
markup.Init()
- c := routers.NormalRoutes()
+ c := routers.NormalRoutes(graceful.GetManager().HammerContext())
log.Printf("[PR] Ready for testing !\n")
log.Printf("[PR] Login with user1, user2, user3, ... with pass: password\n")
diff --git a/go.mod b/go.mod
index fa6fb911d..d578e145c 100644
--- a/go.mod
+++ b/go.mod
@@ -28,6 +28,7 @@ require (
github.com/emirpasic/gods v1.18.1
github.com/ethantkoenig/rupture v1.0.1
github.com/felixge/fgprof v0.9.2
+ github.com/fsnotify/fsnotify v1.5.4
github.com/gliderlabs/ssh v0.3.4
github.com/go-ap/activitypub v0.0.0-20220615144428-48208c70483b
github.com/go-ap/jsonld v0.0.0-20220615144122-1d862b15410d
@@ -161,7 +162,6 @@ require (
github.com/envoyproxy/protoc-gen-validate v0.6.2 // indirect
github.com/felixge/httpsnoop v1.0.2 // indirect
github.com/form3tech-oss/jwt-go v3.2.3+incompatible // indirect
- github.com/fsnotify/fsnotify v1.5.4 // indirect
github.com/fullstorydev/grpcurl v1.8.1 // indirect
github.com/fxamacker/cbor/v2 v2.4.0 // indirect
github.com/go-ap/errors v0.0.0-20220615144307-e8bc4a40ae9f // indirect
diff --git a/integrations/api_activitypub_person_test.go b/integrations/api_activitypub_person_test.go
index e19da4086..c0548df0b 100644
--- a/integrations/api_activitypub_person_test.go
+++ b/integrations/api_activitypub_person_test.go
@@ -23,10 +23,10 @@ import (
func TestActivityPubPerson(t *testing.T) {
setting.Federation.Enabled = true
- c = routers.NormalRoutes()
+ c = routers.NormalRoutes(context.TODO())
defer func() {
setting.Federation.Enabled = false
- c = routers.NormalRoutes()
+ c = routers.NormalRoutes(context.TODO())
}()
onGiteaRun(t, func(*testing.T, *url.URL) {
@@ -60,10 +60,10 @@ func TestActivityPubPerson(t *testing.T) {
func TestActivityPubMissingPerson(t *testing.T) {
setting.Federation.Enabled = true
- c = routers.NormalRoutes()
+ c = routers.NormalRoutes(context.TODO())
defer func() {
setting.Federation.Enabled = false
- c = routers.NormalRoutes()
+ c = routers.NormalRoutes(context.TODO())
}()
onGiteaRun(t, func(*testing.T, *url.URL) {
@@ -75,10 +75,10 @@ func TestActivityPubMissingPerson(t *testing.T) {
func TestActivityPubPersonInbox(t *testing.T) {
setting.Federation.Enabled = true
- c = routers.NormalRoutes()
+ c = routers.NormalRoutes(context.TODO())
defer func() {
setting.Federation.Enabled = false
- c = routers.NormalRoutes()
+ c = routers.NormalRoutes(context.TODO())
}()
srv := httptest.NewServer(c)
diff --git a/integrations/api_nodeinfo_test.go b/integrations/api_nodeinfo_test.go
index cf9ff4da1..bbb791207 100644
--- a/integrations/api_nodeinfo_test.go
+++ b/integrations/api_nodeinfo_test.go
@@ -5,6 +5,7 @@
package integrations
import (
+ "context"
"net/http"
"net/url"
"testing"
@@ -18,10 +19,10 @@ import (
func TestNodeinfo(t *testing.T) {
setting.Federation.Enabled = true
- c = routers.NormalRoutes()
+ c = routers.NormalRoutes(context.TODO())
defer func() {
setting.Federation.Enabled = false
- c = routers.NormalRoutes()
+ c = routers.NormalRoutes(context.TODO())
}()
onGiteaRun(t, func(*testing.T, *url.URL) {
diff --git a/integrations/create_no_session_test.go b/integrations/create_no_session_test.go
index 49234c1e9..017fe1d35 100644
--- a/integrations/create_no_session_test.go
+++ b/integrations/create_no_session_test.go
@@ -5,6 +5,7 @@
package integrations
import (
+ "context"
"net/http"
"net/http/httptest"
"os"
@@ -57,7 +58,7 @@ func TestSessionFileCreation(t *testing.T) {
oldSessionConfig := setting.SessionConfig.ProviderConfig
defer func() {
setting.SessionConfig.ProviderConfig = oldSessionConfig
- c = routers.NormalRoutes()
+ c = routers.NormalRoutes(context.TODO())
}()
var config session.Options
@@ -82,7 +83,7 @@ func TestSessionFileCreation(t *testing.T) {
setting.SessionConfig.ProviderConfig = string(newConfigBytes)
- c = routers.NormalRoutes()
+ c = routers.NormalRoutes(context.TODO())
t.Run("NoSessionOnViewIssue", func(t *testing.T) {
defer PrintCurrentTest(t)()
diff --git a/integrations/integration_test.go b/integrations/integration_test.go
index 3c379f5c8..a506c6a82 100644
--- a/integrations/integration_test.go
+++ b/integrations/integration_test.go
@@ -89,7 +89,7 @@ func TestMain(m *testing.M) {
defer cancel()
initIntegrationTest()
- c = routers.NormalRoutes()
+ c = routers.NormalRoutes(context.TODO())
// integration test settings...
if setting.Cfg != nil {
diff --git a/modules/charset/escape_test.go b/modules/charset/escape_test.go
index 8063e1154..a7232a465 100644
--- a/modules/charset/escape_test.go
+++ b/modules/charset/escape_test.go
@@ -133,11 +133,18 @@ then resh (ר), and finally heh (ה) (which should appear leftmost).`,
},
}
+type nullLocale struct{}
+
+func (nullLocale) Language() string { return "" }
+func (nullLocale) Tr(key string, _ ...interface{}) string { return key }
+func (nullLocale) TrN(cnt interface{}, key1, keyN string, args ...interface{}) string { return "" }
+
+var _ (translation.Locale) = nullLocale{}
+
func TestEscapeControlString(t *testing.T) {
for _, tt := range escapeControlTests {
t.Run(tt.name, func(t *testing.T) {
- locale := translation.NewLocale("en_US")
- status, result := EscapeControlString(tt.text, locale)
+ status, result := EscapeControlString(tt.text, nullLocale{})
if !reflect.DeepEqual(*status, tt.status) {
t.Errorf("EscapeControlString() status = %v, wanted= %v", status, tt.status)
}
@@ -173,7 +180,7 @@ func TestEscapeControlReader(t *testing.T) {
t.Run(tt.name, func(t *testing.T) {
input := strings.NewReader(tt.text)
output := &strings.Builder{}
- status, err := EscapeControlReader(input, output, translation.NewLocale("en_US"))
+ status, err := EscapeControlReader(input, output, nullLocale{})
result := output.String()
if err != nil {
t.Errorf("EscapeControlReader(): err = %v", err)
@@ -195,5 +202,5 @@ func TestEscapeControlReader_panic(t *testing.T) {
for i := 0; i < 6826; i++ {
bs = append(bs, []byte("—")...)
}
- _, _ = EscapeControlString(string(bs), translation.NewLocale("en_US"))
+ _, _ = EscapeControlString(string(bs), nullLocale{})
}
diff --git a/modules/context/context.go b/modules/context/context.go
index 45f1978e9..4b6a21b21 100644
--- a/modules/context/context.go
+++ b/modules/context/context.go
@@ -658,8 +658,8 @@ func Auth(authMethod auth.Method) func(*Context) {
}
// Contexter initializes a classic context for a request.
-func Contexter() func(next http.Handler) http.Handler {
- rnd := templates.HTMLRenderer()
+func Contexter(ctx context.Context) func(next http.Handler) http.Handler {
+ _, rnd := templates.HTMLRenderer(ctx)
csrfOpts := getCsrfOpts()
if !setting.IsProd {
CsrfTokenRegenerationInterval = 5 * time.Second // in dev, re-generate the tokens more aggressively for debug purpose
diff --git a/modules/context/package.go b/modules/context/package.go
index 92a97831d..ad06f4d63 100644
--- a/modules/context/package.go
+++ b/modules/context/package.go
@@ -5,6 +5,7 @@
package context
import (
+ gocontext "context"
"fmt"
"net/http"
@@ -14,6 +15,7 @@ import (
"code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/structs"
+ "code.gitea.io/gitea/modules/templates"
)
// Package contains owner, access mode and optional the package descriptor
@@ -118,12 +120,14 @@ func packageAssignment(ctx *Context, errCb func(int, string, interface{})) {
}
// PackageContexter initializes a package context for a request.
-func PackageContexter() func(next http.Handler) http.Handler {
+func PackageContexter(ctx gocontext.Context) func(next http.Handler) http.Handler {
+ _, rnd := templates.HTMLRenderer(ctx)
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
ctx := Context{
- Resp: NewResponse(resp),
- Data: map[string]interface{}{},
+ Resp: NewResponse(resp),
+ Data: map[string]interface{}{},
+ Render: rnd,
}
defer ctx.Close()
diff --git a/modules/options/base.go b/modules/options/base.go
new file mode 100644
index 000000000..e1d6efa7f
--- /dev/null
+++ b/modules/options/base.go
@@ -0,0 +1,40 @@
+// Copyright 2022 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package options
+
+import (
+ "fmt"
+ "io/fs"
+ "os"
+ "path/filepath"
+
+ "code.gitea.io/gitea/modules/util"
+)
+
+func walkAssetDir(root string, callback func(path, name string, d fs.DirEntry, err error) error) error {
+ if err := filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error {
+ // name is the path relative to the root
+ name := path[len(root):]
+ if len(name) > 0 && name[0] == '/' {
+ name = name[1:]
+ }
+ if err != nil {
+ if os.IsNotExist(err) {
+ return callback(path, name, d, err)
+ }
+ return err
+ }
+ if util.CommonSkip(d.Name()) {
+ if d.IsDir() {
+ return fs.SkipDir
+ }
+ return nil
+ }
+ return callback(path, name, d, err)
+ }); err != nil && !os.IsNotExist(err) {
+ return fmt.Errorf("unable to get files for assets in %s: %w", root, err)
+ }
+ return nil
+}
diff --git a/modules/options/dynamic.go b/modules/options/dynamic.go
index 5fea337e4..eeef11e8d 100644
--- a/modules/options/dynamic.go
+++ b/modules/options/dynamic.go
@@ -8,8 +8,10 @@ package options
import (
"fmt"
+ "io/fs"
"os"
"path"
+ "path/filepath"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
@@ -45,7 +47,7 @@ func Dir(name string) ([]string, error) {
isDir, err = util.IsDir(staticDir)
if err != nil {
- return []string{}, fmt.Errorf("Unabe to check if static directory %s is a directory. %v", staticDir, err)
+ return []string{}, fmt.Errorf("unable to check if static directory %s is a directory. %v", staticDir, err)
}
if isDir {
files, err := util.StatDir(staticDir, true)
@@ -64,6 +66,18 @@ func Locale(name string) ([]byte, error) {
return fileFromDir(path.Join("locale", name))
}
+// WalkLocales reads the content of a specific locale from static or custom path.
+func WalkLocales(callback func(path, name string, d fs.DirEntry, err error) error) error {
+ if err := walkAssetDir(filepath.Join(setting.StaticRootPath, "options", "locale"), callback); err != nil && !os.IsNotExist(err) {
+ return fmt.Errorf("failed to walk locales. Error: %w", err)
+ }
+
+ if err := walkAssetDir(filepath.Join(setting.CustomPath, "options", "locale"), callback); err != nil && !os.IsNotExist(err) {
+ return fmt.Errorf("failed to walk locales. Error: %w", err)
+ }
+ return nil
+}
+
// Readme reads the content of a specific readme from static or custom path.
func Readme(name string) ([]byte, error) {
return fileFromDir(path.Join("readme", name))
diff --git a/modules/options/static.go b/modules/options/static.go
index 6cad88cb6..d9a6c8366 100644
--- a/modules/options/static.go
+++ b/modules/options/static.go
@@ -9,8 +9,10 @@ package options
import (
"fmt"
"io"
+ "io/fs"
"os"
"path"
+ "path/filepath"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
@@ -74,6 +76,14 @@ func Locale(name string) ([]byte, error) {
return fileFromDir(path.Join("locale", name))
}
+// WalkLocales reads the content of a specific locale from static or custom path.
+func WalkLocales(callback func(path, name string, d fs.DirEntry, err error) error) error {
+ if err := walkAssetDir(filepath.Join(setting.CustomPath, "options", "locale"), callback); err != nil && !os.IsNotExist(err) {
+ return fmt.Errorf("failed to walk locales. Error: %w", err)
+ }
+ return nil
+}
+
// Readme reads the content of a specific readme from bindata or custom path.
func Readme(name string) ([]byte, error) {
return fileFromDir(path.Join("readme", name))
diff --git a/modules/templates/base.go b/modules/templates/base.go
index 9563650e1..d234d531f 100644
--- a/modules/templates/base.go
+++ b/modules/templates/base.go
@@ -5,15 +5,16 @@
package templates
import (
+ "fmt"
+ "io/fs"
"os"
+ "path/filepath"
"strings"
"time"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
-
- "github.com/unrolled/render"
)
// Vars represents variables to be render in golang templates
@@ -47,8 +48,16 @@ func BaseVars() Vars {
}
}
-func getDirAssetNames(dir string) []string {
+func getDirTemplateAssetNames(dir string) []string {
+ return getDirAssetNames(dir, false)
+}
+
+func getDirAssetNames(dir string, mailer bool) []string {
var tmpls []string
+
+ if mailer {
+ dir += filepath.Join(dir, "mail")
+ }
f, err := os.Stat(dir)
if err != nil {
if os.IsNotExist(err) {
@@ -67,8 +76,13 @@ func getDirAssetNames(dir string) []string {
log.Warn("Failed to read %s templates dir. %v", dir, err)
return tmpls
}
+
+ prefix := "templates/"
+ if mailer {
+ prefix += "mail/"
+ }
for _, filePath := range files {
- if strings.HasPrefix(filePath, "mail/") {
+ if !mailer && strings.HasPrefix(filePath, "mail/") {
continue
}
@@ -76,20 +90,39 @@ func getDirAssetNames(dir string) []string {
continue
}
- tmpls = append(tmpls, "templates/"+filePath)
+ tmpls = append(tmpls, prefix+filePath)
}
return tmpls
}
-// HTMLRenderer returns a render.
-func HTMLRenderer() *render.Render {
- return render.New(render.Options{
- Extensions: []string{".tmpl"},
- Directory: "templates",
- Funcs: NewFuncMap(),
- Asset: GetAsset,
- AssetNames: GetAssetNames,
- IsDevelopment: !setting.IsProd,
- DisableHTTPErrorRendering: true,
- })
+func walkAssetDir(root string, skipMail bool, callback func(path, name string, d fs.DirEntry, err error) error) error {
+ mailRoot := filepath.Join(root, "mail")
+ if err := filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error {
+ name := path[len(root):]
+ if len(name) > 0 && name[0] == '/' {
+ name = name[1:]
+ }
+ if err != nil {
+ if os.IsNotExist(err) {
+ return callback(path, name, d, err)
+ }
+ return err
+ }
+ if skipMail && path == mailRoot && d.IsDir() {
+ return fs.SkipDir
+ }
+ if util.CommonSkip(d.Name()) {
+ if d.IsDir() {
+ return fs.SkipDir
+ }
+ return nil
+ }
+ if strings.HasSuffix(d.Name(), ".tmpl") || d.IsDir() {
+ return callback(path, name, d, err)
+ }
+ return nil
+ }); err != nil && !os.IsNotExist(err) {
+ return fmt.Errorf("unable to get files for template assets in %s: %w", root, err)
+ }
+ return nil
}
diff --git a/modules/templates/dynamic.go b/modules/templates/dynamic.go
index de6968c31..4896580f6 100644
--- a/modules/templates/dynamic.go
+++ b/modules/templates/dynamic.go
@@ -8,15 +8,12 @@ package templates
import (
"html/template"
+ "io/fs"
"os"
- "path"
"path/filepath"
- "strings"
texttmpl "text/template"
- "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/modules/util"
)
var (
@@ -36,77 +33,42 @@ func GetAsset(name string) ([]byte, error) {
return os.ReadFile(filepath.Join(setting.StaticRootPath, name))
}
-// GetAssetNames returns assets list
-func GetAssetNames() []string {
- tmpls := getDirAssetNames(filepath.Join(setting.CustomPath, "templates"))
- tmpls2 := getDirAssetNames(filepath.Join(setting.StaticRootPath, "templates"))
+// walkTemplateFiles calls a callback for each template asset
+func walkTemplateFiles(callback func(path, name string, d fs.DirEntry, err error) error) error {
+ if err := walkAssetDir(filepath.Join(setting.CustomPath, "templates"), true, callback); err != nil && !os.IsNotExist(err) {
+ return err
+ }
+ if err := walkAssetDir(filepath.Join(setting.StaticRootPath, "templates"), true, callback); err != nil && !os.IsNotExist(err) {
+ return err
+ }
+ return nil
+}
+
+// GetTemplateAssetNames returns list of template names
+func GetTemplateAssetNames() []string {
+ tmpls := getDirTemplateAssetNames(filepath.Join(setting.CustomPath, "templates"))
+ tmpls2 := getDirTemplateAssetNames(filepath.Join(setting.StaticRootPath, "templates"))
return append(tmpls, tmpls2...)
}
-// Mailer provides the templates required for sending notification mails.
-func Mailer() (*texttmpl.Template, *template.Template) {
- for _, funcs := range NewTextFuncMap() {
- subjectTemplates.Funcs(funcs)
+func walkMailerTemplates(callback func(path, name string, d fs.DirEntry, err error) error) error {
+ if err := walkAssetDir(filepath.Join(setting.StaticRootPath, "templates", "mail"), false, callback); err != nil && !os.IsNotExist(err) {
+ return err
}
- for _, funcs := range NewFuncMap() {
- bodyTemplates.Funcs(funcs)
+ if err := walkAssetDir(filepath.Join(setting.CustomPath, "templates", "mail"), false, callback); err != nil && !os.IsNotExist(err) {
+ return err
}
-
- staticDir := path.Join(setting.StaticRootPath, "templates", "mail")
-
- isDir, err := util.IsDir(staticDir)
- if err != nil {
- log.Warn("Unable to check if templates dir %s is a directory. Error: %v", staticDir, err)
- }
- if isDir {
- files, err := util.StatDir(staticDir)
-
- if err != nil {
- log.Warn("Failed to read %s templates dir. %v", staticDir, err)
- } else {
- for _, filePath := range files {
- if !strings.HasSuffix(filePath, ".tmpl") {
- continue
- }
-
- content, err := os.ReadFile(path.Join(staticDir, filePath))
- if err != nil {
- log.Warn("Failed to read static %s template. %v", filePath, err)
- continue
- }
-
- buildSubjectBodyTemplate(subjectTemplates, bodyTemplates, strings.TrimSuffix(filePath, ".tmpl"), content)
- }
- }
- }
-
- customDir := path.Join(setting.CustomPath, "templates", "mail")
-
- isDir, err = util.IsDir(customDir)
- if err != nil {
- log.Warn("Unable to check if templates dir %s is a directory. Error: %v", customDir, err)
- }
- if isDir {
- files, err := util.StatDir(customDir)
-
- if err != nil {
- log.Warn("Failed to read %s templates dir. %v", customDir, err)
- } else {
- for _, filePath := range files {
- if !strings.HasSuffix(filePath, ".tmpl") {
- continue
- }
-
- content, err := os.ReadFile(path.Join(customDir, filePath))
- if err != nil {
- log.Warn("Failed to read custom %s template. %v", filePath, err)
- continue
- }
-
- buildSubjectBodyTemplate(subjectTemplates, bodyTemplates, strings.TrimSuffix(filePath, ".tmpl"), content)
- }
- }
- }
-
- return subjectTemplates, bodyTemplates
+ return nil
+}
+
+// BuiltinAsset will read the provided asset from the embedded assets
+// (This always returns os.ErrNotExist)
+func BuiltinAsset(name string) ([]byte, error) {
+ return nil, os.ErrNotExist
+}
+
+// BuiltinAssetNames returns the names of the embedded assets
+// (This always returns nil)
+func BuiltinAssetNames() []string {
+ return nil
}
diff --git a/modules/templates/htmlrenderer.go b/modules/templates/htmlrenderer.go
new file mode 100644
index 000000000..210bb5e73
--- /dev/null
+++ b/modules/templates/htmlrenderer.go
@@ -0,0 +1,52 @@
+// Copyright 2022 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package templates
+
+import (
+ "context"
+
+ "code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/modules/watcher"
+
+ "github.com/unrolled/render"
+)
+
+var rendererKey interface{} = "templatesHtmlRendereer"
+
+// HTMLRenderer returns the current html renderer for the context or creates and stores one within the context for future use
+func HTMLRenderer(ctx context.Context) (context.Context, *render.Render) {
+ rendererInterface := ctx.Value(rendererKey)
+ if rendererInterface != nil {
+ renderer, ok := rendererInterface.(*render.Render)
+ if ok {
+ return ctx, renderer
+ }
+ }
+
+ rendererType := "static"
+ if !setting.IsProd {
+ rendererType = "auto-reloading"
+ }
+ log.Log(1, log.DEBUG, "Creating "+rendererType+" HTML Renderer")
+
+ renderer := render.New(render.Options{
+ Extensions: []string{".tmpl"},
+ Directory: "templates",
+ Funcs: NewFuncMap(),
+ Asset: GetAsset,
+ AssetNames: GetTemplateAssetNames,
+ UseMutexLock: !setting.IsProd,
+ IsDevelopment: false,
+ DisableHTTPErrorRendering: true,
+ })
+ if !setting.IsProd {
+ watcher.CreateWatcher(ctx, "HTML Templates", &watcher.CreateWatcherOpts{
+ PathsCallback: walkTemplateFiles,
+ BetweenCallback: renderer.CompileTemplates,
+ })
+ }
+ return context.WithValue(ctx, rendererKey, renderer), renderer
+}
diff --git a/modules/templates/mailer.go b/modules/templates/mailer.go
new file mode 100644
index 000000000..0cac1280f
--- /dev/null
+++ b/modules/templates/mailer.go
@@ -0,0 +1,92 @@
+// Copyright 2022 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package templates
+
+import (
+ "context"
+ "html/template"
+ "io/fs"
+ "os"
+ "strings"
+ texttmpl "text/template"
+
+ "code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/modules/watcher"
+)
+
+// Mailer provides the templates required for sending notification mails.
+func Mailer(ctx context.Context) (*texttmpl.Template, *template.Template) {
+ for _, funcs := range NewTextFuncMap() {
+ subjectTemplates.Funcs(funcs)
+ }
+ for _, funcs := range NewFuncMap() {
+ bodyTemplates.Funcs(funcs)
+ }
+
+ refreshTemplates := func() {
+ for _, assetPath := range BuiltinAssetNames() {
+ if !strings.HasPrefix(assetPath, "mail/") {
+ continue
+ }
+
+ if !strings.HasSuffix(assetPath, ".tmpl") {
+ continue
+ }
+
+ content, err := BuiltinAsset(assetPath)
+ if err != nil {
+ log.Warn("Failed to read embedded %s template. %v", assetPath, err)
+ continue
+ }
+
+ assetName := strings.TrimPrefix(strings.TrimSuffix(assetPath, ".tmpl"), "mail/")
+
+ log.Trace("Adding built-in mailer template for %s", assetName)
+ buildSubjectBodyTemplate(subjectTemplates,
+ bodyTemplates,
+ assetName,
+ content)
+ }
+
+ if err := walkMailerTemplates(func(path, name string, d fs.DirEntry, err error) error {
+ if err != nil {
+ return err
+ }
+ if d.IsDir() {
+ return nil
+ }
+
+ content, err := os.ReadFile(path)
+ if err != nil {
+ log.Warn("Failed to read custom %s template. %v", path, err)
+ return nil
+ }
+
+ assetName := strings.TrimSuffix(name, ".tmpl")
+ log.Trace("Adding mailer template for %s from %q", assetName, path)
+ buildSubjectBodyTemplate(subjectTemplates,
+ bodyTemplates,
+ assetName,
+ content)
+ return nil
+ }); err != nil && !os.IsNotExist(err) {
+ log.Warn("Error whilst walking mailer templates directories. %v", err)
+ }
+ }
+
+ refreshTemplates()
+
+ if !setting.IsProd {
+ // Now subjectTemplates and bodyTemplates are both synchronized
+ // thus it is safe to call refresh from a different goroutine
+ watcher.CreateWatcher(ctx, "Mailer Templates", &watcher.CreateWatcherOpts{
+ PathsCallback: walkMailerTemplates,
+ BetweenCallback: refreshTemplates,
+ })
+ }
+
+ return subjectTemplates, bodyTemplates
+}
diff --git a/modules/templates/static.go b/modules/templates/static.go
index 351e48b4d..3265bd9cf 100644
--- a/modules/templates/static.go
+++ b/modules/templates/static.go
@@ -9,6 +9,7 @@ package templates
import (
"html/template"
"io"
+ "io/fs"
"os"
"path"
"path/filepath"
@@ -16,10 +17,8 @@ import (
texttmpl "text/template"
"time"
- "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/timeutil"
- "code.gitea.io/gitea/modules/util"
)
var (
@@ -40,95 +39,42 @@ func GetAsset(name string) ([]byte, error) {
} else if err == nil {
return bs, nil
}
- return Asset(strings.TrimPrefix(name, "templates/"))
+ return BuiltinAsset(strings.TrimPrefix(name, "templates/"))
}
-// GetAssetNames only for chi
-func GetAssetNames() []string {
+// GetFiles calls a callback for each template asset
+func walkTemplateFiles(callback func(path, name string, d fs.DirEntry, err error) error) error {
+ if err := walkAssetDir(filepath.Join(setting.CustomPath, "templates"), true, callback); err != nil && !os.IsNotExist(err) {
+ return err
+ }
+ return nil
+}
+
+// GetTemplateAssetNames only for chi
+func GetTemplateAssetNames() []string {
realFS := Assets.(vfsgen۰FS)
tmpls := make([]string, 0, len(realFS))
for k := range realFS {
+ if strings.HasPrefix(k, "/mail/") {
+ continue
+ }
tmpls = append(tmpls, "templates/"+k[1:])
}
customDir := path.Join(setting.CustomPath, "templates")
- customTmpls := getDirAssetNames(customDir)
+ customTmpls := getDirTemplateAssetNames(customDir)
return append(tmpls, customTmpls...)
}
-// Mailer provides the templates required for sending notification mails.
-func Mailer() (*texttmpl.Template, *template.Template) {
- for _, funcs := range NewTextFuncMap() {
- subjectTemplates.Funcs(funcs)
+func walkMailerTemplates(callback func(path, name string, d fs.DirEntry, err error) error) error {
+ if err := walkAssetDir(filepath.Join(setting.CustomPath, "templates", "mail"), false, callback); err != nil && !os.IsNotExist(err) {
+ return err
}
- for _, funcs := range NewFuncMap() {
- bodyTemplates.Funcs(funcs)
- }
-
- for _, assetPath := range AssetNames() {
- if !strings.HasPrefix(assetPath, "mail/") {
- continue
- }
-
- if !strings.HasSuffix(assetPath, ".tmpl") {
- continue
- }
-
- content, err := Asset(assetPath)
- if err != nil {
- log.Warn("Failed to read embedded %s template. %v", assetPath, err)
- continue
- }
-
- buildSubjectBodyTemplate(subjectTemplates,
- bodyTemplates,
- strings.TrimPrefix(
- strings.TrimSuffix(
- assetPath,
- ".tmpl",
- ),
- "mail/",
- ),
- content)
- }
-
- customDir := path.Join(setting.CustomPath, "templates", "mail")
- isDir, err := util.IsDir(customDir)
- if err != nil {
- log.Warn("Failed to check if custom directory %s is a directory. %v", err)
- }
- if isDir {
- files, err := util.StatDir(customDir)
-
- if err != nil {
- log.Warn("Failed to read %s templates dir. %v", customDir, err)
- } else {
- for _, filePath := range files {
- if !strings.HasSuffix(filePath, ".tmpl") {
- continue
- }
-
- content, err := os.ReadFile(path.Join(customDir, filePath))
- if err != nil {
- log.Warn("Failed to read custom %s template. %v", filePath, err)
- continue
- }
-
- buildSubjectBodyTemplate(subjectTemplates,
- bodyTemplates,
- strings.TrimSuffix(
- filePath,
- ".tmpl",
- ),
- content)
- }
- }
- }
-
- return subjectTemplates, bodyTemplates
+ return nil
}
-func Asset(name string) ([]byte, error) {
+// BuiltinAsset reads the provided asset from the builtin embedded assets
+func BuiltinAsset(name string) ([]byte, error) {
f, err := Assets.Open("/" + name)
if err != nil {
return nil, err
@@ -137,7 +83,8 @@ func Asset(name string) ([]byte, error) {
return io.ReadAll(f)
}
-func AssetNames() []string {
+// BuiltinAssetNames returns the names of the built-in embedded assets
+func BuiltinAssetNames() []string {
realFS := Assets.(vfsgen۰FS)
results := make([]string, 0, len(realFS))
for k := range realFS {
@@ -146,7 +93,8 @@ func AssetNames() []string {
return results
}
-func AssetIsDir(name string) (bool, error) {
+// BuiltinAssetIsDir returns if a provided asset is a directory
+func BuiltinAssetIsDir(name string) (bool, error) {
if f, err := Assets.Open("/" + name); err != nil {
return false, err
} else {
diff --git a/modules/timeutil/since_test.go b/modules/timeutil/since_test.go
index dfcf9cb01..9350b5e96 100644
--- a/modules/timeutil/since_test.go
+++ b/modules/timeutil/since_test.go
@@ -5,6 +5,7 @@
package timeutil
import (
+ "context"
"fmt"
"os"
"testing"
@@ -31,7 +32,7 @@ func TestMain(m *testing.M) {
setting.Names = []string{"english"}
setting.Langs = []string{"en-US"}
// setup
- translation.InitLocales()
+ translation.InitLocales(context.Background())
BaseDate = time.Date(2000, time.January, 1, 0, 0, 0, 0, time.UTC)
// run the tests
diff --git a/modules/translation/i18n/errors.go b/modules/translation/i18n/errors.go
new file mode 100644
index 000000000..b485badd1
--- /dev/null
+++ b/modules/translation/i18n/errors.go
@@ -0,0 +1,12 @@
+// Copyright 2022 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package i18n
+
+import "errors"
+
+var (
+ ErrLocaleAlreadyExist = errors.New("lang already exists")
+ ErrUncertainArguments = errors.New("arguments to i18n should not contain uncertain slices")
+)
diff --git a/modules/translation/i18n/format.go b/modules/translation/i18n/format.go
new file mode 100644
index 000000000..3fb9e6d6d
--- /dev/null
+++ b/modules/translation/i18n/format.go
@@ -0,0 +1,42 @@
+// Copyright 2022 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package i18n
+
+import (
+ "fmt"
+ "reflect"
+)
+
+// Format formats provided arguments for a given translated message
+func Format(format string, args ...interface{}) (msg string, err error) {
+ if len(args) == 0 {
+ return format, nil
+ }
+
+ fmtArgs := make([]interface{}, 0, len(args))
+ for _, arg := range args {
+ val := reflect.ValueOf(arg)
+ if val.Kind() == reflect.Slice {
+ // Previously, we would accept Tr(lang, key, a, [b, c], d, [e, f]) as Sprintf(msg, a, b, c, d, e, f)
+ // but this is an unstable behavior.
+ //
+ // So we restrict the accepted arguments to either:
+ //
+ // 1. Tr(lang, key, [slice-items]) as Sprintf(msg, items...)
+ // 2. Tr(lang, key, args...) as Sprintf(msg, args...)
+ if len(args) == 1 {
+ for i := 0; i < val.Len(); i++ {
+ fmtArgs = append(fmtArgs, val.Index(i).Interface())
+ }
+ } else {
+ err = ErrUncertainArguments
+ break
+ }
+ } else {
+ fmtArgs = append(fmtArgs, arg)
+ }
+ }
+ return fmt.Sprintf(format, fmtArgs...), err
+}
diff --git a/modules/translation/i18n/i18n.go b/modules/translation/i18n/i18n.go
index bb906f3c0..23b4e23c7 100644
--- a/modules/translation/i18n/i18n.go
+++ b/modules/translation/i18n/i18n.go
@@ -5,297 +5,48 @@
package i18n
import (
- "errors"
- "fmt"
- "os"
- "reflect"
- "sync"
- "time"
-
- "code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/modules/setting"
-
- "gopkg.in/ini.v1"
+ "io"
)
-var (
- ErrLocaleAlreadyExist = errors.New("lang already exists")
+var DefaultLocales = NewLocaleStore()
- DefaultLocales = NewLocaleStore(true)
-)
-
-type locale struct {
- // This mutex will be set if we have live-reload enabled (e.g. dev mode)
- reloadMu *sync.RWMutex
-
- store *LocaleStore
- langName string
-
- idxToMsgMap map[int]string // the map idx is generated by store's trKeyToIdxMap
-
- sourceFileName string
- sourceFileInfo os.FileInfo
- lastReloadCheckTime time.Time
+type Locale interface {
+ // Tr translates a given key and arguments for a language
+ Tr(trKey string, trArgs ...interface{}) string
+ // Has reports if a locale has a translation for a given key
+ Has(trKey string) bool
}
-type LocaleStore struct {
- // This mutex will be set if we have live-reload enabled (e.g. dev mode)
- reloadMu *sync.RWMutex
+// LocaleStore provides the functions common to all locale stores
+type LocaleStore interface {
+ io.Closer
- langNames []string
- langDescs []string
- localeMap map[string]*locale
-
- // this needs to be locked when live-reloading
- trKeyToIdxMap map[string]int
-
- defaultLang string
-}
-
-func NewLocaleStore(isProd bool) *LocaleStore {
- store := &LocaleStore{localeMap: make(map[string]*locale), trKeyToIdxMap: make(map[string]int)}
- if !isProd {
- store.reloadMu = &sync.RWMutex{}
- }
- return store
-}
-
-// AddLocaleByIni adds locale by ini into the store
-// if source is a string, then the file is loaded. In dev mode, this file will be checked for live-reloading
-// if source is a []byte, then the content is used
-// Note: this is not concurrent safe
-func (store *LocaleStore) AddLocaleByIni(langName, langDesc string, source interface{}) error {
- if _, ok := store.localeMap[langName]; ok {
- return ErrLocaleAlreadyExist
- }
-
- l := &locale{store: store, langName: langName}
- if store.reloadMu != nil {
- l.reloadMu = &sync.RWMutex{}
- l.reloadMu.Lock() // Arguably this is not necessary as AddLocaleByIni isn't concurrent safe - but for consistency we do this
- defer l.reloadMu.Unlock()
- }
-
- if fileName, ok := source.(string); ok {
- l.sourceFileName = fileName
- l.sourceFileInfo, _ = os.Stat(fileName) // live-reload only works for regular files. the error can be ignored
- }
-
- var err error
- l.idxToMsgMap, err = store.readIniToIdxToMsgMap(source)
- if err != nil {
- return err
- }
-
- store.langNames = append(store.langNames, langName)
- store.langDescs = append(store.langDescs, langDesc)
-
- store.localeMap[l.langName] = l
-
- return nil
-}
-
-// readIniToIdxToMsgMap will read a provided ini and creates an idxToMsgMap
-func (store *LocaleStore) readIniToIdxToMsgMap(source interface{}) (map[int]string, error) {
- iniFile, err := ini.LoadSources(ini.LoadOptions{
- IgnoreInlineComment: true,
- UnescapeValueCommentSymbols: true,
- }, source)
- if err != nil {
- return nil, fmt.Errorf("unable to load ini: %w", err)
- }
- iniFile.BlockMode = false
-
- idxToMsgMap := make(map[int]string)
-
- if store.reloadMu != nil {
- store.reloadMu.Lock()
- defer store.reloadMu.Unlock()
- }
-
- for _, section := range iniFile.Sections() {
- for _, key := range section.Keys() {
-
- var trKey string
- if section.Name() == "" || section.Name() == "DEFAULT" {
- trKey = key.Name()
- } else {
- trKey = section.Name() + "." + key.Name()
- }
-
- // Instead of storing the key strings in multiple different maps we compute a idx which will act as numeric code for key
- // This reduces the size of the locale idxToMsgMaps
- idx, ok := store.trKeyToIdxMap[trKey]
- if !ok {
- idx = len(store.trKeyToIdxMap)
- store.trKeyToIdxMap[trKey] = idx
- }
- idxToMsgMap[idx] = key.Value()
- }
- }
- iniFile = nil
- return idxToMsgMap, nil
-}
-
-func (store *LocaleStore) idxForTrKey(trKey string) (int, bool) {
- if store.reloadMu != nil {
- store.reloadMu.RLock()
- defer store.reloadMu.RUnlock()
- }
- idx, ok := store.trKeyToIdxMap[trKey]
- return idx, ok
-}
-
-// HasLang reports if a language is available in the store
-func (store *LocaleStore) HasLang(langName string) bool {
- _, ok := store.localeMap[langName]
- return ok
-}
-
-// ListLangNameDesc reports if a language available in the store
-func (store *LocaleStore) ListLangNameDesc() (names, desc []string) {
- return store.langNames, store.langDescs
-}
-
-// SetDefaultLang sets default language as a fallback
-func (store *LocaleStore) SetDefaultLang(lang string) {
- store.defaultLang = lang
-}
-
-// Tr translates content to target language. fall back to default language.
-func (store *LocaleStore) Tr(lang, trKey string, trArgs ...interface{}) string {
- l, ok := store.localeMap[lang]
- if !ok {
- l, ok = store.localeMap[store.defaultLang]
- }
-
- if ok {
- return l.Tr(trKey, trArgs...)
- }
- return trKey
-}
-
-// reloadIfNeeded will check if the locale needs to be reloaded
-// this function will assume that the l.reloadMu has been RLocked if it already exists
-func (l *locale) reloadIfNeeded() {
- if l.reloadMu == nil {
- return
- }
-
- now := time.Now()
- if now.Sub(l.lastReloadCheckTime) < time.Second || l.sourceFileInfo == nil || l.sourceFileName == "" {
- return
- }
-
- l.reloadMu.RUnlock()
- l.reloadMu.Lock() // (NOTE: a pre-emption can occur between these two locks so we need to recheck)
- defer l.reloadMu.RLock()
- defer l.reloadMu.Unlock()
-
- if now.Sub(l.lastReloadCheckTime) < time.Second || l.sourceFileInfo == nil || l.sourceFileName == "" {
- return
- }
-
- l.lastReloadCheckTime = now
- sourceFileInfo, err := os.Stat(l.sourceFileName)
- if err != nil || sourceFileInfo.ModTime().Equal(l.sourceFileInfo.ModTime()) {
- return
- }
-
- idxToMsgMap, err := l.store.readIniToIdxToMsgMap(l.sourceFileName)
- if err == nil {
- l.idxToMsgMap = idxToMsgMap
- } else {
- log.Error("Unable to live-reload the locale file %q, err: %v", l.sourceFileName, err)
- }
-
- // We will set the sourceFileInfo to this file to prevent repeated attempts to re-load this broken file
- l.sourceFileInfo = sourceFileInfo
-}
-
-// Tr translates content to locale language. fall back to default language.
-func (l *locale) Tr(trKey string, trArgs ...interface{}) string {
- if l.reloadMu != nil {
- l.reloadMu.RLock()
- defer l.reloadMu.RUnlock()
- l.reloadIfNeeded()
- }
-
- msg, _ := l.tryTr(trKey, trArgs...)
- return msg
-}
-
-func (l *locale) tryTr(trKey string, trArgs ...interface{}) (msg string, found bool) {
- trMsg := trKey
-
- // convert the provided trKey to a common idx from the store
- idx, ok := l.store.idxForTrKey(trKey)
-
- if ok {
- if msg, found = l.idxToMsgMap[idx]; found {
- trMsg = msg // use the translation that we have found
- } else if l.langName != l.store.defaultLang {
- // No translation available in our current language... fallback to the default language
-
- // Attempt to get the default language from the locale store
- if def, ok := l.store.localeMap[l.store.defaultLang]; ok {
-
- if def.reloadMu != nil {
- def.reloadMu.RLock()
- def.reloadIfNeeded()
- }
- if msg, found = def.idxToMsgMap[idx]; found {
- trMsg = msg // use the translation that we have found
- }
- if def.reloadMu != nil {
- def.reloadMu.RUnlock()
- }
- }
- }
- }
-
- if !found && !setting.IsProd {
- log.Error("missing i18n translation key: %q", trKey)
- }
-
- if len(trArgs) == 0 {
- return trMsg, found
- }
-
- fmtArgs := make([]interface{}, 0, len(trArgs))
- for _, arg := range trArgs {
- val := reflect.ValueOf(arg)
- if val.Kind() == reflect.Slice {
- // Previously, we would accept Tr(lang, key, a, [b, c], d, [e, f]) as Sprintf(msg, a, b, c, d, e, f)
- // but this is an unstable behavior.
- //
- // So we restrict the accepted arguments to either:
- //
- // 1. Tr(lang, key, [slice-items]) as Sprintf(msg, items...)
- // 2. Tr(lang, key, args...) as Sprintf(msg, args...)
- if len(trArgs) == 1 {
- for i := 0; i < val.Len(); i++ {
- fmtArgs = append(fmtArgs, val.Index(i).Interface())
- }
- } else {
- log.Error("the args for i18n shouldn't contain uncertain slices, key=%q, args=%v", trKey, trArgs)
- break
- }
- } else {
- fmtArgs = append(fmtArgs, arg)
- }
- }
-
- return fmt.Sprintf(trMsg, fmtArgs...), found
+ // Tr translates a given key and arguments for a language
+ Tr(lang, trKey string, trArgs ...interface{}) string
+ // Has reports if a locale has a translation for a given key
+ Has(lang, trKey string) bool
+ // SetDefaultLang sets the default language to fall back to
+ SetDefaultLang(lang string)
+ // ListLangNameDesc provides paired slices of language names to descriptors
+ ListLangNameDesc() (names, desc []string)
+ // Locale return the locale for the provided language or the default language if not found
+ Locale(langName string) (Locale, bool)
+ // HasLang returns whether a given language is present in the store
+ HasLang(langName string) bool
+ // AddLocaleByIni adds a new language to the store
+ AddLocaleByIni(langName, langDesc string, source interface{}) error
}
// ResetDefaultLocales resets the current default locales
// NOTE: this is not synchronized
-func ResetDefaultLocales(isProd bool) {
- DefaultLocales = NewLocaleStore(isProd)
+func ResetDefaultLocales() {
+ if DefaultLocales != nil {
+ _ = DefaultLocales.Close()
+ }
+ DefaultLocales = NewLocaleStore()
}
-// Tr use default locales to translate content to target language.
-func Tr(lang, trKey string, trArgs ...interface{}) string {
- return DefaultLocales.Tr(lang, trKey, trArgs...)
+// GetLocales returns the locale from the default locales
+func GetLocale(lang string) (Locale, bool) {
+ return DefaultLocales.Locale(lang)
}
diff --git a/modules/translation/i18n/i18n_test.go b/modules/translation/i18n/i18n_test.go
index 32f7585b3..7940e59c9 100644
--- a/modules/translation/i18n/i18n_test.go
+++ b/modules/translation/i18n/i18n_test.go
@@ -27,36 +27,34 @@ fmt = %[2]s %[1]s
sub = Changed Sub String
`)
- for _, isProd := range []bool{true, false} {
- ls := NewLocaleStore(isProd)
- assert.NoError(t, ls.AddLocaleByIni("lang1", "Lang1", testData1))
- assert.NoError(t, ls.AddLocaleByIni("lang2", "Lang2", testData2))
- ls.SetDefaultLang("lang1")
+ ls := NewLocaleStore()
+ assert.NoError(t, ls.AddLocaleByIni("lang1", "Lang1", testData1))
+ assert.NoError(t, ls.AddLocaleByIni("lang2", "Lang2", testData2))
+ ls.SetDefaultLang("lang1")
- result := ls.Tr("lang1", "fmt", "a", "b")
- assert.Equal(t, "a b", result)
+ result := ls.Tr("lang1", "fmt", "a", "b")
+ assert.Equal(t, "a b", result)
- result = ls.Tr("lang2", "fmt", "a", "b")
- assert.Equal(t, "b a", result)
+ result = ls.Tr("lang2", "fmt", "a", "b")
+ assert.Equal(t, "b a", result)
- result = ls.Tr("lang1", "section.sub")
- assert.Equal(t, "Sub String", result)
+ result = ls.Tr("lang1", "section.sub")
+ assert.Equal(t, "Sub String", result)
- result = ls.Tr("lang2", "section.sub")
- assert.Equal(t, "Changed Sub String", result)
+ result = ls.Tr("lang2", "section.sub")
+ assert.Equal(t, "Changed Sub String", result)
- result = ls.Tr("", ".dot.name")
- assert.Equal(t, "Dot Name", result)
+ result = ls.Tr("", ".dot.name")
+ assert.Equal(t, "Dot Name", result)
- result = ls.Tr("lang2", "section.mixed")
- assert.Equal(t, `test value; more text`, result)
+ result = ls.Tr("lang2", "section.mixed")
+ assert.Equal(t, `test value; more text`, result)
- langs, descs := ls.ListLangNameDesc()
- assert.Equal(t, []string{"lang1", "lang2"}, langs)
- assert.Equal(t, []string{"Lang1", "Lang2"}, descs)
+ langs, descs := ls.ListLangNameDesc()
+ assert.Equal(t, []string{"lang1", "lang2"}, langs)
+ assert.Equal(t, []string{"Lang1", "Lang2"}, descs)
- result, found := ls.localeMap["lang1"].tryTr("no-such")
- assert.Equal(t, "no-such", result)
- assert.False(t, found)
- }
+ found := ls.Has("lang1", "no-such")
+ assert.False(t, found)
+ assert.NoError(t, ls.Close())
}
diff --git a/modules/translation/i18n/localestore.go b/modules/translation/i18n/localestore.go
new file mode 100644
index 000000000..e3b88ad96
--- /dev/null
+++ b/modules/translation/i18n/localestore.go
@@ -0,0 +1,161 @@
+// Copyright 2022 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package i18n
+
+import (
+ "fmt"
+
+ "code.gitea.io/gitea/modules/log"
+
+ "gopkg.in/ini.v1"
+)
+
+// This file implements the static LocaleStore that will not watch for changes
+
+type locale struct {
+ store *localeStore
+ langName string
+ idxToMsgMap map[int]string // the map idx is generated by store's trKeyToIdxMap
+}
+
+type localeStore struct {
+ // After initializing has finished, these fields are read-only.
+ langNames []string
+ langDescs []string
+
+ localeMap map[string]*locale
+ trKeyToIdxMap map[string]int
+
+ defaultLang string
+}
+
+// NewLocaleStore creates a static locale store
+func NewLocaleStore() LocaleStore {
+ return &localeStore{localeMap: make(map[string]*locale), trKeyToIdxMap: make(map[string]int)}
+}
+
+// AddLocaleByIni adds locale by ini into the store
+// if source is a string, then the file is loaded
+// if source is a []byte, then the content is used
+func (store *localeStore) AddLocaleByIni(langName, langDesc string, source interface{}) error {
+ if _, ok := store.localeMap[langName]; ok {
+ return ErrLocaleAlreadyExist
+ }
+
+ store.langNames = append(store.langNames, langName)
+ store.langDescs = append(store.langDescs, langDesc)
+
+ l := &locale{store: store, langName: langName, idxToMsgMap: make(map[int]string)}
+ store.localeMap[l.langName] = l
+
+ iniFile, err := ini.LoadSources(ini.LoadOptions{
+ IgnoreInlineComment: true,
+ UnescapeValueCommentSymbols: true,
+ }, source)
+ if err != nil {
+ return fmt.Errorf("unable to load ini: %w", err)
+ }
+ iniFile.BlockMode = false
+
+ for _, section := range iniFile.Sections() {
+ for _, key := range section.Keys() {
+ var trKey string
+ if section.Name() == "" || section.Name() == "DEFAULT" {
+ trKey = key.Name()
+ } else {
+ trKey = section.Name() + "." + key.Name()
+ }
+ idx, ok := store.trKeyToIdxMap[trKey]
+ if !ok {
+ idx = len(store.trKeyToIdxMap)
+ store.trKeyToIdxMap[trKey] = idx
+ }
+ l.idxToMsgMap[idx] = key.Value()
+ }
+ }
+ iniFile = nil
+
+ return nil
+}
+
+func (store *localeStore) HasLang(langName string) bool {
+ _, ok := store.localeMap[langName]
+ return ok
+}
+
+func (store *localeStore) ListLangNameDesc() (names, desc []string) {
+ return store.langNames, store.langDescs
+}
+
+// SetDefaultLang sets default language as a fallback
+func (store *localeStore) SetDefaultLang(lang string) {
+ store.defaultLang = lang
+}
+
+// Tr translates content to target language. fall back to default language.
+func (store *localeStore) Tr(lang, trKey string, trArgs ...interface{}) string {
+ l, _ := store.Locale(lang)
+
+ return l.Tr(trKey, trArgs...)
+}
+
+// Has returns whether the given language has a translation for the provided key
+func (store *localeStore) Has(lang, trKey string) bool {
+ l, _ := store.Locale(lang)
+
+ return l.Has(trKey)
+}
+
+// Locale returns the locale for the lang or the default language
+func (store *localeStore) Locale(lang string) (Locale, bool) {
+ l, found := store.localeMap[lang]
+ if !found {
+ var ok bool
+ l, ok = store.localeMap[store.defaultLang]
+ if !ok {
+ // no default - return an empty locale
+ l = &locale{store: store, idxToMsgMap: make(map[int]string)}
+ }
+ }
+ return l, found
+}
+
+// Close implements io.Closer
+func (store *localeStore) Close() error {
+ return nil
+}
+
+// Tr translates content to locale language. fall back to default language.
+func (l *locale) Tr(trKey string, trArgs ...interface{}) string {
+ format := trKey
+
+ idx, ok := l.store.trKeyToIdxMap[trKey]
+ if ok {
+ if msg, ok := l.idxToMsgMap[idx]; ok {
+ format = msg // use the found translation
+ } else if def, ok := l.store.localeMap[l.store.defaultLang]; ok {
+ // try to use default locale's translation
+ if msg, ok := def.idxToMsgMap[idx]; ok {
+ format = msg
+ }
+ }
+ }
+
+ msg, err := Format(format, trArgs...)
+ if err != nil {
+ log.Error("Error whilst formatting %q in %s: %v", trKey, l.langName, err)
+ }
+ return msg
+}
+
+// Has returns whether a key is present in this locale or not
+func (l *locale) Has(trKey string) bool {
+ idx, ok := l.store.trKeyToIdxMap[trKey]
+ if !ok {
+ return false
+ }
+ _, ok = l.idxToMsgMap[idx]
+ return ok
+}
diff --git a/modules/translation/translation.go b/modules/translation/translation.go
index fcc101d96..e40a9357f 100644
--- a/modules/translation/translation.go
+++ b/modules/translation/translation.go
@@ -5,15 +5,16 @@
package translation
import (
- "path"
+ "context"
"sort"
"strings"
+ "sync"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/options"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/translation/i18n"
- "code.gitea.io/gitea/modules/util"
+ "code.gitea.io/gitea/modules/watcher"
"golang.org/x/text/language"
)
@@ -31,6 +32,7 @@ type LangType struct {
}
var (
+ lock *sync.RWMutex
matcher language.Matcher
allLangs []*LangType
allLangMap map[string]*LangType
@@ -43,57 +45,52 @@ func AllLangs() []*LangType {
}
// InitLocales loads the locales
-func InitLocales() {
- i18n.ResetDefaultLocales(setting.IsProd)
- localeNames, err := options.Dir("locale")
- if err != nil {
- log.Fatal("Failed to list locale files: %v", err)
+func InitLocales(ctx context.Context) {
+ if lock != nil {
+ lock.Lock()
+ defer lock.Unlock()
+ } else if !setting.IsProd && lock == nil {
+ lock = &sync.RWMutex{}
}
- localFiles := make(map[string]interface{}, len(localeNames))
- for _, name := range localeNames {
- if options.IsDynamic() {
- // Try to check if CustomPath has the file, otherwise fallback to StaticRootPath
- value := path.Join(setting.CustomPath, "options/locale", name)
+ refreshLocales := func() {
+ i18n.ResetDefaultLocales()
+ localeNames, err := options.Dir("locale")
+ if err != nil {
+ log.Fatal("Failed to list locale files: %v", err)
+ }
- isFile, err := util.IsFile(value)
- if err != nil {
- log.Fatal("Failed to load %s locale file. %v", name, err)
- }
-
- if isFile {
- localFiles[name] = value
- } else {
- localFiles[name] = path.Join(setting.StaticRootPath, "options/locale", name)
- }
- } else {
+ localFiles := make(map[string]interface{}, len(localeNames))
+ for _, name := range localeNames {
localFiles[name], err = options.Locale(name)
if err != nil {
log.Fatal("Failed to load %s locale file. %v", name, err)
}
}
- }
- supportedTags = make([]language.Tag, len(setting.Langs))
- for i, lang := range setting.Langs {
- supportedTags[i] = language.Raw.Make(lang)
- }
+ supportedTags = make([]language.Tag, len(setting.Langs))
+ for i, lang := range setting.Langs {
+ supportedTags[i] = language.Raw.Make(lang)
+ }
- matcher = language.NewMatcher(supportedTags)
- for i := range setting.Names {
- key := "locale_" + setting.Langs[i] + ".ini"
+ matcher = language.NewMatcher(supportedTags)
+ for i := range setting.Names {
+ key := "locale_" + setting.Langs[i] + ".ini"
- if err = i18n.DefaultLocales.AddLocaleByIni(setting.Langs[i], setting.Names[i], localFiles[key]); err != nil {
- log.Error("Failed to set messages to %s: %v", setting.Langs[i], err)
+ if err = i18n.DefaultLocales.AddLocaleByIni(setting.Langs[i], setting.Names[i], localFiles[key]); err != nil {
+ log.Error("Failed to set messages to %s: %v", setting.Langs[i], err)
+ }
+ }
+ if len(setting.Langs) != 0 {
+ defaultLangName := setting.Langs[0]
+ if defaultLangName != "en-US" {
+ log.Info("Use the first locale (%s) in LANGS setting option as default", defaultLangName)
+ }
+ i18n.DefaultLocales.SetDefaultLang(defaultLangName)
}
}
- if len(setting.Langs) != 0 {
- defaultLangName := setting.Langs[0]
- if defaultLangName != "en-US" {
- log.Info("Use the first locale (%s) in LANGS setting option as default", defaultLangName)
- }
- i18n.DefaultLocales.SetDefaultLang(defaultLangName)
- }
+
+ refreshLocales()
langs, descs := i18n.DefaultLocales.ListLangNameDesc()
allLangs = make([]*LangType, 0, len(langs))
@@ -108,6 +105,17 @@ func InitLocales() {
sort.Slice(allLangs, func(i, j int) bool {
return strings.ToLower(allLangs[i].Name) < strings.ToLower(allLangs[j].Name)
})
+
+ if !setting.IsProd {
+ watcher.CreateWatcher(ctx, "Locales", &watcher.CreateWatcherOpts{
+ PathsCallback: options.WalkLocales,
+ BetweenCallback: func() {
+ lock.Lock()
+ defer lock.Unlock()
+ refreshLocales()
+ },
+ })
+ }
}
// Match matches accept languages
@@ -118,16 +126,24 @@ func Match(tags ...language.Tag) language.Tag {
// locale represents the information of localization.
type locale struct {
+ i18n.Locale
Lang, LangName string // these fields are used directly in templates: .i18n.Lang
}
// NewLocale return a locale
func NewLocale(lang string) Locale {
+ if lock != nil {
+ lock.RLock()
+ defer lock.RUnlock()
+ }
+
langName := "unknown"
if l, ok := allLangMap[lang]; ok {
langName = l.Name
}
+ i18nLocale, _ := i18n.GetLocale(lang)
return &locale{
+ Locale: i18nLocale,
Lang: lang,
LangName: langName,
}
@@ -137,11 +153,6 @@ func (l *locale) Language() string {
return l.Lang
}
-// Tr translates content to target language.
-func (l *locale) Tr(format string, args ...interface{}) string {
- return i18n.Tr(l.Lang, format, args...)
-}
-
// Language specific rules for translating plural texts
var trNLangRules = map[string]func(int64) int{
// the default rule is "en-US" if a language isn't listed here
diff --git a/modules/util/path.go b/modules/util/path.go
index 0ccc7a1dc..3d4ddec21 100644
--- a/modules/util/path.go
+++ b/modules/util/path.go
@@ -12,7 +12,6 @@ import (
"path/filepath"
"regexp"
"runtime"
- "strings"
)
// EnsureAbsolutePath ensure that a path is absolute, making it
@@ -91,7 +90,7 @@ func statDir(dirPath, recPath string, includeDir, isDirOnly, followSymlinks bool
statList := make([]string, 0)
for _, fi := range fis {
- if strings.Contains(fi.Name(), ".DS_Store") {
+ if CommonSkip(fi.Name()) {
continue
}
@@ -199,3 +198,21 @@ func HomeDir() (home string, err error) {
return home, nil
}
+
+// CommonSkip will check a provided name to see if it represents file or directory that should not be watched
+func CommonSkip(name string) bool {
+ if name == "" {
+ return true
+ }
+
+ switch name[0] {
+ case '.':
+ return true
+ case 't', 'T':
+ return name[1:] == "humbs.db"
+ case 'd', 'D':
+ return name[1:] == "esktop.ini"
+ }
+
+ return false
+}
diff --git a/modules/watcher/watcher.go b/modules/watcher/watcher.go
new file mode 100644
index 000000000..d737f6ccb
--- /dev/null
+++ b/modules/watcher/watcher.go
@@ -0,0 +1,115 @@
+// Copyright 2022 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package watcher
+
+import (
+ "context"
+ "io/fs"
+ "os"
+
+ "code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/modules/process"
+
+ "github.com/fsnotify/fsnotify"
+)
+
+// CreateWatcherOpts are options to configure the watcher
+type CreateWatcherOpts struct {
+ // PathsCallback is used to set the required paths to watch
+ PathsCallback func(func(path, name string, d fs.DirEntry, err error) error) error
+
+ // BeforeCallback is called before any files are watched
+ BeforeCallback func()
+
+ // Between Callback is called between after a watched event has occurred
+ BetweenCallback func()
+
+ // AfterCallback is called as this watcher ends
+ AfterCallback func()
+}
+
+// CreateWatcher creates a watcher labelled with the provided description and running with the provided options.
+// The created watcher will create a subcontext from the provided ctx and register it with the process manager.
+func CreateWatcher(ctx context.Context, desc string, opts *CreateWatcherOpts) {
+ go run(ctx, desc, opts)
+}
+
+func run(ctx context.Context, desc string, opts *CreateWatcherOpts) {
+ if opts.BeforeCallback != nil {
+ opts.BeforeCallback()
+ }
+ if opts.AfterCallback != nil {
+ defer opts.AfterCallback()
+ }
+ ctx, _, finished := process.GetManager().AddTypedContext(ctx, "Watcher: "+desc, process.SystemProcessType, true)
+ defer finished()
+
+ log.Trace("Watcher loop starting for %s", desc)
+ defer log.Trace("Watcher loop ended for %s", desc)
+
+ watcher, err := fsnotify.NewWatcher()
+ if err != nil {
+ log.Error("Unable to create watcher for %s: %v", desc, err)
+ return
+ }
+ if err := opts.PathsCallback(func(path, _ string, d fs.DirEntry, err error) error {
+ if err != nil && !os.IsNotExist(err) {
+ return err
+ }
+ log.Trace("Watcher: %s watching %q", desc, path)
+ _ = watcher.Add(path)
+ return nil
+ }); err != nil {
+ log.Error("Unable to create watcher for %s: %v", desc, err)
+ _ = watcher.Close()
+ return
+ }
+
+ // Note we don't call the BetweenCallback here
+
+ for {
+ select {
+ case event, ok := <-watcher.Events:
+ if !ok {
+ _ = watcher.Close()
+ return
+ }
+ log.Debug("Watched file for %s had event: %v", desc, event)
+ case err, ok := <-watcher.Errors:
+ if !ok {
+ _ = watcher.Close()
+ return
+ }
+ log.Error("Error whilst watching files for %s: %v", desc, err)
+ case <-ctx.Done():
+ _ = watcher.Close()
+ return
+ }
+
+ // Recreate the watcher - only call the BetweenCallback after the new watcher is set-up
+ _ = watcher.Close()
+ watcher, err = fsnotify.NewWatcher()
+ if err != nil {
+ log.Error("Unable to create watcher for %s: %v", desc, err)
+ return
+ }
+ if err := opts.PathsCallback(func(path, _ string, _ fs.DirEntry, err error) error {
+ if err != nil {
+ return err
+ }
+ _ = watcher.Add(path)
+ return nil
+ }); err != nil {
+ log.Error("Unable to create watcher for %s: %v", desc, err)
+ _ = watcher.Close()
+ return
+ }
+
+ // Inform our BetweenCallback that there has been an event
+ if opts.BetweenCallback != nil {
+ opts.BetweenCallback()
+ }
+ }
+}
diff --git a/routers/api/packages/api.go b/routers/api/packages/api.go
index cbf041a7e..0761cacdf 100644
--- a/routers/api/packages/api.go
+++ b/routers/api/packages/api.go
@@ -5,6 +5,7 @@
package packages
import (
+ gocontext "context"
"net/http"
"regexp"
"strings"
@@ -38,10 +39,10 @@ func reqPackageAccess(accessMode perm.AccessMode) func(ctx *context.Context) {
}
}
-func Routes() *web.Route {
+func Routes(ctx gocontext.Context) *web.Route {
r := web.NewRoute()
- r.Use(context.PackageContexter())
+ r.Use(context.PackageContexter(ctx))
authMethods := []auth.Method{
&auth.OAuth2{},
@@ -270,10 +271,10 @@ func Routes() *web.Route {
return r
}
-func ContainerRoutes() *web.Route {
+func ContainerRoutes(ctx gocontext.Context) *web.Route {
r := web.NewRoute()
- r.Use(context.PackageContexter())
+ r.Use(context.PackageContexter(ctx))
authMethods := []auth.Method{
&auth.Basic{},
diff --git a/routers/api/packages/pypi/pypi.go b/routers/api/packages/pypi/pypi.go
index 390907450..07cf766c6 100644
--- a/routers/api/packages/pypi/pypi.go
+++ b/routers/api/packages/pypi/pypi.go
@@ -16,7 +16,6 @@ import (
packages_module "code.gitea.io/gitea/modules/packages"
pypi_module "code.gitea.io/gitea/modules/packages/pypi"
"code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/modules/templates"
"code.gitea.io/gitea/modules/validation"
"code.gitea.io/gitea/routers/api/packages/helper"
packages_service "code.gitea.io/gitea/services/packages"
@@ -58,7 +57,6 @@ func PackageMetadata(ctx *context.Context) {
ctx.Data["RegistryURL"] = setting.AppURL + "api/packages/" + ctx.Package.Owner.Name + "/pypi"
ctx.Data["PackageDescriptor"] = pds[0]
ctx.Data["PackageDescriptors"] = pds
- ctx.Render = templates.HTMLRenderer()
ctx.HTML(http.StatusOK, "api/packages/pypi/simple")
}
diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go
index e1478fa2a..b413370ad 100644
--- a/routers/api/v1/api.go
+++ b/routers/api/v1/api.go
@@ -65,6 +65,7 @@
package v1
import (
+ gocontext "context"
"fmt"
"net/http"
"reflect"
@@ -605,7 +606,7 @@ func buildAuthGroup() *auth.Group {
}
// Routes registers all v1 APIs routes to web application.
-func Routes() *web.Route {
+func Routes(ctx gocontext.Context) *web.Route {
m := web.NewRoute()
m.Use(securityHeaders())
@@ -623,7 +624,7 @@ func Routes() *web.Route {
m.Use(context.APIContexter())
group := buildAuthGroup()
- if err := group.Init(); err != nil {
+ if err := group.Init(ctx); err != nil {
log.Error("Could not initialize '%s' auth method, error: %s", group.Name(), err)
}
diff --git a/routers/api/v1/misc/markdown_test.go b/routers/api/v1/misc/markdown_test.go
index 9beb88be1..7809fa5cc 100644
--- a/routers/api/v1/misc/markdown_test.go
+++ b/routers/api/v1/misc/markdown_test.go
@@ -29,7 +29,7 @@ const (
)
func createContext(req *http.Request) (*context.Context, *httptest.ResponseRecorder) {
- rnd := templates.HTMLRenderer()
+ _, rnd := templates.HTMLRenderer(req.Context())
resp := httptest.NewRecorder()
c := &context.Context{
Req: req,
diff --git a/routers/init.go b/routers/init.go
index 612fc5a83..85a38899e 100644
--- a/routers/init.go
+++ b/routers/init.go
@@ -27,6 +27,7 @@ import (
"code.gitea.io/gitea/modules/ssh"
"code.gitea.io/gitea/modules/storage"
"code.gitea.io/gitea/modules/svg"
+ "code.gitea.io/gitea/modules/templates"
"code.gitea.io/gitea/modules/translation"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
@@ -110,12 +111,12 @@ func GlobalInitInstalled(ctx context.Context) {
log.Info("Run Mode: %s", util.ToTitleCase(setting.RunMode))
// Setup i18n
- translation.InitLocales()
+ translation.InitLocales(ctx)
setting.NewServices()
mustInit(storage.Init)
- mailer.NewContext()
+ mailer.NewContext(ctx)
mustInit(cache.NewContext)
notification.NewContext()
mustInit(archiver.Init)
@@ -163,18 +164,19 @@ func GlobalInitInstalled(ctx context.Context) {
}
// NormalRoutes represents non install routes
-func NormalRoutes() *web.Route {
+func NormalRoutes(ctx context.Context) *web.Route {
+ ctx, _ = templates.HTMLRenderer(ctx)
r := web.NewRoute()
for _, middle := range common.Middlewares() {
r.Use(middle)
}
- r.Mount("/", web_routers.Routes())
- r.Mount("/api/v1", apiv1.Routes())
+ r.Mount("/", web_routers.Routes(ctx))
+ r.Mount("/api/v1", apiv1.Routes(ctx))
r.Mount("/api/internal", private.Routes())
if setting.Packages.Enabled {
- r.Mount("/api/packages", packages_router.Routes())
- r.Mount("/v2", packages_router.ContainerRoutes())
+ r.Mount("/api/packages", packages_router.Routes(ctx))
+ r.Mount("/v2", packages_router.ContainerRoutes(ctx))
}
return r
}
diff --git a/routers/install/install.go b/routers/install/install.go
index 8060414a1..890725b9a 100644
--- a/routers/install/install.go
+++ b/routers/install/install.go
@@ -6,6 +6,7 @@
package install
import (
+ goctx "context"
"fmt"
"net/http"
"os"
@@ -51,39 +52,41 @@ func getSupportedDbTypeNames() (dbTypeNames []map[string]string) {
}
// Init prepare for rendering installation page
-func Init(next http.Handler) http.Handler {
- rnd := templates.HTMLRenderer()
+func Init(ctx goctx.Context) func(next http.Handler) http.Handler {
+ _, rnd := templates.HTMLRenderer(ctx)
dbTypeNames := getSupportedDbTypeNames()
- return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
- if setting.InstallLock {
- resp.Header().Add("Refresh", "1; url="+setting.AppURL+"user/login")
- _ = rnd.HTML(resp, http.StatusOK, string(tplPostInstall), nil)
- return
- }
- locale := middleware.Locale(resp, req)
- startTime := time.Now()
- ctx := context.Context{
- Resp: context.NewResponse(resp),
- Flash: &middleware.Flash{},
- Locale: locale,
- Render: rnd,
- Session: session.GetSession(req),
- Data: map[string]interface{}{
- "locale": locale,
- "Title": locale.Tr("install.install"),
- "PageIsInstall": true,
- "DbTypeNames": dbTypeNames,
- "AllLangs": translation.AllLangs(),
- "PageStartTime": startTime,
+ return func(next http.Handler) http.Handler {
+ return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
+ if setting.InstallLock {
+ resp.Header().Add("Refresh", "1; url="+setting.AppURL+"user/login")
+ _ = rnd.HTML(resp, http.StatusOK, string(tplPostInstall), nil)
+ return
+ }
+ locale := middleware.Locale(resp, req)
+ startTime := time.Now()
+ ctx := context.Context{
+ Resp: context.NewResponse(resp),
+ Flash: &middleware.Flash{},
+ Locale: locale,
+ Render: rnd,
+ Session: session.GetSession(req),
+ Data: map[string]interface{}{
+ "locale": locale,
+ "Title": locale.Tr("install.install"),
+ "PageIsInstall": true,
+ "DbTypeNames": dbTypeNames,
+ "AllLangs": translation.AllLangs(),
+ "PageStartTime": startTime,
- "PasswordHashAlgorithms": user_model.AvailableHashAlgorithms,
- },
- }
- defer ctx.Close()
+ "PasswordHashAlgorithms": user_model.AvailableHashAlgorithms,
+ },
+ }
+ defer ctx.Close()
- ctx.Req = context.WithContext(req, &ctx)
- next.ServeHTTP(resp, ctx.Req)
- })
+ ctx.Req = context.WithContext(req, &ctx)
+ next.ServeHTTP(resp, ctx.Req)
+ })
+ }
}
// Install render installation page
diff --git a/routers/install/routes.go b/routers/install/routes.go
index fdabcb9dc..761747782 100644
--- a/routers/install/routes.go
+++ b/routers/install/routes.go
@@ -5,6 +5,7 @@
package install
import (
+ goctx "context"
"fmt"
"net/http"
"path"
@@ -29,8 +30,8 @@ func (d *dataStore) GetData() map[string]interface{} {
return *d
}
-func installRecovery() func(next http.Handler) http.Handler {
- rnd := templates.HTMLRenderer()
+func installRecovery(ctx goctx.Context) func(next http.Handler) http.Handler {
+ _, rnd := templates.HTMLRenderer(ctx)
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
defer func() {
@@ -82,7 +83,7 @@ func installRecovery() func(next http.Handler) http.Handler {
}
// Routes registers the install routes
-func Routes() *web.Route {
+func Routes(ctx goctx.Context) *web.Route {
r := web.NewRoute()
for _, middle := range common.Middlewares() {
r.Use(middle)
@@ -105,8 +106,8 @@ func Routes() *web.Route {
Domain: setting.SessionConfig.Domain,
}))
- r.Use(installRecovery())
- r.Use(Init)
+ r.Use(installRecovery(ctx))
+ r.Use(Init(ctx))
r.Get("/", Install)
r.Post("/", web.Bind(forms.InstallForm{}), SubmitInstall)
r.Get("/api/healthz", healthcheck.Check)
diff --git a/routers/install/routes_test.go b/routers/install/routes_test.go
index 29003c384..e69d2d15d 100644
--- a/routers/install/routes_test.go
+++ b/routers/install/routes_test.go
@@ -5,13 +5,16 @@
package install
import (
+ "context"
"testing"
"github.com/stretchr/testify/assert"
)
func TestRoutes(t *testing.T) {
- routes := Routes()
+ ctx, cancel := context.WithCancel(context.Background())
+ defer cancel()
+ routes := Routes(ctx)
assert.NotNil(t, routes)
assert.EqualValues(t, "/", routes.R.Routes()[0].Pattern)
assert.Nil(t, routes.R.Routes()[0].SubRoutes)
diff --git a/routers/install/setting.go b/routers/install/setting.go
index cf0a01ce3..c4912f112 100644
--- a/routers/install/setting.go
+++ b/routers/install/setting.go
@@ -24,7 +24,7 @@ func PreloadSettings(ctx context.Context) bool {
log.Info("Log path: %s", setting.LogRootPath)
log.Info("Configuration file: %s", setting.CustomConf)
log.Info("Prepare to run install page")
- translation.InitLocales()
+ translation.InitLocales(ctx)
if setting.EnableSQLite3 {
log.Info("SQLite3 is supported")
}
diff --git a/routers/web/base.go b/routers/web/base.go
index 30a24a127..2441d6d51 100644
--- a/routers/web/base.go
+++ b/routers/web/base.go
@@ -5,6 +5,7 @@
package web
import (
+ goctx "context"
"errors"
"fmt"
"io"
@@ -123,8 +124,8 @@ func (d *dataStore) GetData() map[string]interface{} {
// Recovery returns a middleware that recovers from any panics and writes a 500 and a log if so.
// This error will be created with the gitea 500 page.
-func Recovery() func(next http.Handler) http.Handler {
- rnd := templates.HTMLRenderer()
+func Recovery(ctx goctx.Context) func(next http.Handler) http.Handler {
+ _, rnd := templates.HTMLRenderer(ctx)
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
defer func() {
diff --git a/routers/web/web.go b/routers/web/web.go
index 55bce1611..1852ecc2e 100644
--- a/routers/web/web.go
+++ b/routers/web/web.go
@@ -22,6 +22,7 @@ import (
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/storage"
"code.gitea.io/gitea/modules/structs"
+ "code.gitea.io/gitea/modules/templates"
"code.gitea.io/gitea/modules/validation"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/modules/web/routing"
@@ -97,7 +98,7 @@ func buildAuthGroup() *auth_service.Group {
}
// Routes returns all web routes
-func Routes() *web.Route {
+func Routes(ctx gocontext.Context) *web.Route {
routes := web.NewRoute()
routes.Use(web.WrapWithPrefix(public.AssetsURLPathPrefix, public.AssetsHandlerFunc(&public.Options{
@@ -119,7 +120,9 @@ func Routes() *web.Route {
})
routes.Use(sessioner)
- routes.Use(Recovery())
+ ctx, _ = templates.HTMLRenderer(ctx)
+
+ routes.Use(Recovery(ctx))
// We use r.Route here over r.Use because this prevents requests that are not for avatars having to go through this additional handler
routes.Route("/avatars/*", "GET, HEAD", storageHandler(setting.Avatar.Storage, "avatars", storage.Avatars))
@@ -192,10 +195,10 @@ func Routes() *web.Route {
routes.Get("/api/healthz", healthcheck.Check)
// Removed: toolbox.Toolboxer middleware will provide debug information which seems unnecessary
- common = append(common, context.Contexter())
+ common = append(common, context.Contexter(ctx))
group := buildAuthGroup()
- if err := group.Init(); err != nil {
+ if err := group.Init(ctx); err != nil {
log.Error("Could not initialize '%s' auth method, error: %s", group.Name(), err)
}
diff --git a/services/auth/group.go b/services/auth/group.go
index 0f40e1a76..bbafe64b4 100644
--- a/services/auth/group.go
+++ b/services/auth/group.go
@@ -5,6 +5,7 @@
package auth
import (
+ "context"
"net/http"
"reflect"
"strings"
@@ -51,14 +52,14 @@ func (b *Group) Name() string {
}
// Init does nothing as the Basic implementation does not need to allocate any resources
-func (b *Group) Init() error {
+func (b *Group) Init(ctx context.Context) error {
for _, method := range b.methods {
initializable, ok := method.(Initializable)
if !ok {
continue
}
- if err := initializable.Init(); err != nil {
+ if err := initializable.Init(ctx); err != nil {
return err
}
}
diff --git a/services/auth/interface.go b/services/auth/interface.go
index a05ece207..ecc9ad2ca 100644
--- a/services/auth/interface.go
+++ b/services/auth/interface.go
@@ -34,7 +34,7 @@ type Method interface {
type Initializable interface {
// Init should be called exactly once before using any of the other methods,
// in order to allow the plugin to allocate necessary resources
- Init() error
+ Init(ctx context.Context) error
}
// Named represents a named thing
diff --git a/services/auth/sspi_windows.go b/services/auth/sspi_windows.go
index 7e31378b6..757d596c4 100644
--- a/services/auth/sspi_windows.go
+++ b/services/auth/sspi_windows.go
@@ -5,6 +5,7 @@
package auth
import (
+ "context"
"errors"
"net/http"
"strings"
@@ -52,21 +53,14 @@ type SSPI struct {
}
// Init creates a new global websspi.Authenticator object
-func (s *SSPI) Init() error {
+func (s *SSPI) Init(ctx context.Context) error {
config := websspi.NewConfig()
var err error
sspiAuth, err = websspi.New(config)
if err != nil {
return err
}
- s.rnd = render.New(render.Options{
- Extensions: []string{".tmpl"},
- Directory: "templates",
- Funcs: templates.NewFuncMap(),
- Asset: templates.GetAsset,
- AssetNames: templates.GetAssetNames,
- IsDevelopment: !setting.IsProd,
- })
+ _, s.rnd = templates.HTMLRenderer(ctx)
return nil
}
diff --git a/services/mailer/mailer.go b/services/mailer/mailer.go
index fdbb6e562..1f43c7f82 100644
--- a/services/mailer/mailer.go
+++ b/services/mailer/mailer.go
@@ -7,6 +7,7 @@ package mailer
import (
"bytes"
+ "context"
"crypto/tls"
"fmt"
"hash/fnv"
@@ -348,7 +349,7 @@ var mailQueue queue.Queue
var Sender gomail.Sender
// NewContext start mail queue service
-func NewContext() {
+func NewContext(ctx context.Context) {
// Need to check if mailQueue is nil because in during reinstall (user had installed
// before but switched install lock off), this function will be called again
// while mail queue is already processing tasks, and produces a race condition.
@@ -381,7 +382,7 @@ func NewContext() {
go graceful.GetManager().RunWithShutdownFns(mailQueue.Run)
- subjectTemplates, bodyTemplates = templates.Mailer()
+ subjectTemplates, bodyTemplates = templates.Mailer(ctx)
}
// SendAsync send mail asynchronously