Use a general approach to access custom/static/builtin assets (#24022)
The idea is to use a Layered Asset File-system (modules/assetfs/layered.go) For example: when there are 2 layers: "custom", "builtin", when access to asset "my/page.tmpl", the Layered Asset File-system will first try to use "custom" assets, if not found, then use "builtin" assets. This approach will hugely simplify a lot of code, make them testable. Other changes: * Simplify the AssetsHandlerFunc code * Simplify the `gitea embedded` sub-command code --------- Co-authored-by: Jason Song <i@wolfogre.com> Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
This commit is contained in:
parent
42919ccb7c
commit
50a72e7a83
108
cmd/embedded.go
108
cmd/embedded.go
@ -1,8 +1,6 @@
|
|||||||
// Copyright 2020 The Gitea Authors. All rights reserved.
|
// Copyright 2020 The Gitea Authors. All rights reserved.
|
||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
//go:build bindata
|
|
||||||
|
|
||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@ -10,9 +8,9 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"sort"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"code.gitea.io/gitea/modules/assetfs"
|
||||||
"code.gitea.io/gitea/modules/log"
|
"code.gitea.io/gitea/modules/log"
|
||||||
"code.gitea.io/gitea/modules/options"
|
"code.gitea.io/gitea/modules/options"
|
||||||
"code.gitea.io/gitea/modules/public"
|
"code.gitea.io/gitea/modules/public"
|
||||||
@ -89,24 +87,20 @@ var (
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
sections map[string]*section
|
matchedAssetFiles []assetFile
|
||||||
assets []asset
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type section struct {
|
type assetFile struct {
|
||||||
Path string
|
fs *assetfs.LayeredFS
|
||||||
Names func() []string
|
name string
|
||||||
IsDir func(string) (bool, error)
|
path string
|
||||||
Asset func(string) ([]byte, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
type asset struct {
|
|
||||||
Section *section
|
|
||||||
Name string
|
|
||||||
Path string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func initEmbeddedExtractor(c *cli.Context) error {
|
func initEmbeddedExtractor(c *cli.Context) error {
|
||||||
|
// FIXME: there is a bug, if the user runs `gitea embedded` with a different user or root,
|
||||||
|
// The setting.Init (loadRunModeFrom) will fail and do log.Fatal
|
||||||
|
// But the console logger has been deleted, so nothing is printed, the user sees nothing and Gitea just exits.
|
||||||
|
|
||||||
// Silence the console logger
|
// Silence the console logger
|
||||||
log.DelNamedLogger("console")
|
log.DelNamedLogger("console")
|
||||||
log.DelNamedLogger(log.DEFAULT)
|
log.DelNamedLogger(log.DEFAULT)
|
||||||
@ -115,24 +109,14 @@ func initEmbeddedExtractor(c *cli.Context) error {
|
|||||||
setting.InitProviderAllowEmpty()
|
setting.InitProviderAllowEmpty()
|
||||||
setting.LoadCommonSettings()
|
setting.LoadCommonSettings()
|
||||||
|
|
||||||
pats, err := getPatterns(c.Args())
|
patterns, err := compileCollectPatterns(c.Args())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
sections := make(map[string]*section, 3)
|
|
||||||
|
|
||||||
sections["public"] = §ion{Path: "public", Names: public.AssetNames, IsDir: public.AssetIsDir, Asset: public.Asset}
|
collectAssetFilesByPattern(c, patterns, "options", options.BuiltinAssets())
|
||||||
sections["options"] = §ion{Path: "options", Names: options.AssetNames, IsDir: options.AssetIsDir, Asset: options.Asset}
|
collectAssetFilesByPattern(c, patterns, "public", public.BuiltinAssets())
|
||||||
sections["templates"] = §ion{Path: "templates", Names: templates.BuiltinAssetNames, IsDir: templates.BuiltinAssetIsDir, Asset: templates.BuiltinAsset}
|
collectAssetFilesByPattern(c, patterns, "templates", templates.BuiltinAssets())
|
||||||
|
|
||||||
for _, sec := range sections {
|
|
||||||
assets = append(assets, buildAssetList(sec, pats, c)...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sort assets
|
|
||||||
sort.SliceStable(assets, func(i, j int) bool {
|
|
||||||
return assets[i].Path < assets[j].Path
|
|
||||||
})
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -166,8 +150,8 @@ func runListDo(c *cli.Context) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, a := range assets {
|
for _, a := range matchedAssetFiles {
|
||||||
fmt.Println(a.Path)
|
fmt.Println(a.path)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@ -178,19 +162,19 @@ func runViewDo(c *cli.Context) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(assets) == 0 {
|
if len(matchedAssetFiles) == 0 {
|
||||||
return fmt.Errorf("No files matched the given pattern")
|
return fmt.Errorf("no files matched the given pattern")
|
||||||
} else if len(assets) > 1 {
|
} else if len(matchedAssetFiles) > 1 {
|
||||||
return fmt.Errorf("Too many files matched the given pattern; try to be more specific")
|
return fmt.Errorf("too many files matched the given pattern, try to be more specific")
|
||||||
}
|
}
|
||||||
|
|
||||||
data, err := assets[0].Section.Asset(assets[0].Name)
|
data, err := matchedAssetFiles[0].fs.ReadFile(matchedAssetFiles[0].name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("%s: %w", assets[0].Path, err)
|
return fmt.Errorf("%s: %w", matchedAssetFiles[0].path, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err = os.Stdout.Write(data); err != nil {
|
if _, err = os.Stdout.Write(data); err != nil {
|
||||||
return fmt.Errorf("%s: %w", assets[0].Path, err)
|
return fmt.Errorf("%s: %w", matchedAssetFiles[0].path, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@ -202,7 +186,7 @@ func runExtractDo(c *cli.Context) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if len(c.Args()) == 0 {
|
if len(c.Args()) == 0 {
|
||||||
return fmt.Errorf("A list of pattern of files to extract is mandatory (e.g. '**' for all)")
|
return fmt.Errorf("a list of pattern of files to extract is mandatory (e.g. '**' for all)")
|
||||||
}
|
}
|
||||||
|
|
||||||
destdir := "."
|
destdir := "."
|
||||||
@ -227,7 +211,7 @@ func runExtractDo(c *cli.Context) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("%s: %s", destdir, err)
|
return fmt.Errorf("%s: %s", destdir, err)
|
||||||
} else if !fi.IsDir() {
|
} else if !fi.IsDir() {
|
||||||
return fmt.Errorf("%s is not a directory.", destdir)
|
return fmt.Errorf("destination %q is not a directory", destdir)
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("Extracting to %s:\n", destdir)
|
fmt.Printf("Extracting to %s:\n", destdir)
|
||||||
@ -235,23 +219,23 @@ func runExtractDo(c *cli.Context) error {
|
|||||||
overwrite := c.Bool("overwrite")
|
overwrite := c.Bool("overwrite")
|
||||||
rename := c.Bool("rename")
|
rename := c.Bool("rename")
|
||||||
|
|
||||||
for _, a := range assets {
|
for _, a := range matchedAssetFiles {
|
||||||
if err := extractAsset(destdir, a, overwrite, rename); err != nil {
|
if err := extractAsset(destdir, a, overwrite, rename); err != nil {
|
||||||
// Non-fatal error
|
// Non-fatal error
|
||||||
fmt.Fprintf(os.Stderr, "%s: %v", a.Path, err)
|
fmt.Fprintf(os.Stderr, "%s: %v", a.path, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func extractAsset(d string, a asset, overwrite, rename bool) error {
|
func extractAsset(d string, a assetFile, overwrite, rename bool) error {
|
||||||
dest := filepath.Join(d, filepath.FromSlash(a.Path))
|
dest := filepath.Join(d, filepath.FromSlash(a.path))
|
||||||
dir := filepath.Dir(dest)
|
dir := filepath.Dir(dest)
|
||||||
|
|
||||||
data, err := a.Section.Asset(a.Name)
|
data, err := a.fs.ReadFile(a.name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("%s: %w", a.Path, err)
|
return fmt.Errorf("%s: %w", a.path, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := os.MkdirAll(dir, os.ModePerm); err != nil {
|
if err := os.MkdirAll(dir, os.ModePerm); err != nil {
|
||||||
@ -272,7 +256,7 @@ func extractAsset(d string, a asset, overwrite, rename bool) error {
|
|||||||
return fmt.Errorf("%s already exists, but it's not a regular file", dest)
|
return fmt.Errorf("%s already exists, but it's not a regular file", dest)
|
||||||
} else if rename {
|
} else if rename {
|
||||||
if err := util.Rename(dest, dest+".bak"); err != nil {
|
if err := util.Rename(dest, dest+".bak"); err != nil {
|
||||||
return fmt.Errorf("Error creating backup for %s: %w", dest, err)
|
return fmt.Errorf("error creating backup for %s: %w", dest, err)
|
||||||
}
|
}
|
||||||
// Attempt to respect file permissions mask (even if user:group will be set anew)
|
// Attempt to respect file permissions mask (even if user:group will be set anew)
|
||||||
perms = fi.Mode()
|
perms = fi.Mode()
|
||||||
@ -293,32 +277,30 @@ func extractAsset(d string, a asset, overwrite, rename bool) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildAssetList(sec *section, globs []glob.Glob, c *cli.Context) []asset {
|
func collectAssetFilesByPattern(c *cli.Context, globs []glob.Glob, path string, layer *assetfs.Layer) {
|
||||||
results := make([]asset, 0, 64)
|
fs := assetfs.Layered(layer)
|
||||||
for _, name := range sec.Names() {
|
files, err := fs.ListAllFiles(".", true)
|
||||||
if isdir, err := sec.IsDir(name); !isdir && err == nil {
|
if err != nil {
|
||||||
if sec.Path == "public" &&
|
log.Error("Error listing files in %q: %v", path, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for _, name := range files {
|
||||||
|
if path == "public" &&
|
||||||
strings.HasPrefix(name, "vendor/") &&
|
strings.HasPrefix(name, "vendor/") &&
|
||||||
!c.Bool("include-vendored") {
|
!c.Bool("include-vendored") {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
matchName := sec.Path + "/" + name
|
matchName := path + "/" + name
|
||||||
for _, g := range globs {
|
for _, g := range globs {
|
||||||
if g.Match(matchName) {
|
if g.Match(matchName) {
|
||||||
results = append(results, asset{
|
matchedAssetFiles = append(matchedAssetFiles, assetFile{fs: fs, name: name, path: path + "/" + name})
|
||||||
Section: sec,
|
|
||||||
Name: name,
|
|
||||||
Path: sec.Path + "/" + name,
|
|
||||||
})
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
return results
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func getPatterns(args []string) ([]glob.Glob, error) {
|
func compileCollectPatterns(args []string) ([]glob.Glob, error) {
|
||||||
if len(args) == 0 {
|
if len(args) == 0 {
|
||||||
args = []string{"**"}
|
args = []string{"**"}
|
||||||
}
|
}
|
||||||
@ -326,7 +308,7 @@ func getPatterns(args []string) ([]glob.Glob, error) {
|
|||||||
for i := range args {
|
for i := range args {
|
||||||
if g, err := glob.Compile(args[i], '/'); err != nil {
|
if g, err := glob.Compile(args[i], '/'); err != nil {
|
||||||
return nil, fmt.Errorf("'%s': Invalid glob pattern: %w", args[i], err)
|
return nil, fmt.Errorf("'%s': Invalid glob pattern: %w", args[i], err)
|
||||||
} else {
|
} else { //nolint:revive
|
||||||
pat[i] = g
|
pat[i] = g
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,29 +0,0 @@
|
|||||||
// Copyright 2020 The Gitea Authors. All rights reserved.
|
|
||||||
// SPDX-License-Identifier: MIT
|
|
||||||
|
|
||||||
//go:build !bindata
|
|
||||||
|
|
||||||
package cmd
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"github.com/urfave/cli"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Cmdembedded represents the available extract sub-command.
|
|
||||||
var (
|
|
||||||
Cmdembedded = cli.Command{
|
|
||||||
Name: "embedded",
|
|
||||||
Usage: "Extract embedded resources",
|
|
||||||
Description: "A command for extracting embedded resources, like templates and images",
|
|
||||||
Action: extractorNotImplemented,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
func extractorNotImplemented(c *cli.Context) error {
|
|
||||||
err := fmt.Errorf("Sorry: the 'embedded' subcommand is not available in builds without bindata")
|
|
||||||
fmt.Fprintf(os.Stderr, "%s\n", err)
|
|
||||||
return err
|
|
||||||
}
|
|
260
modules/assetfs/layered.go
Normal file
260
modules/assetfs/layered.go
Normal file
@ -0,0 +1,260 @@
|
|||||||
|
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package assetfs
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"io/fs"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"sort"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"code.gitea.io/gitea/modules/log"
|
||||||
|
"code.gitea.io/gitea/modules/process"
|
||||||
|
"code.gitea.io/gitea/modules/util"
|
||||||
|
|
||||||
|
"github.com/fsnotify/fsnotify"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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
|
||||||
|
localPath string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Layer) Name() string {
|
||||||
|
return l.name
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open opens the named file. The caller is responsible for closing the file.
|
||||||
|
func (l *Layer) Open(name string) (http.File, error) {
|
||||||
|
return l.fs.Open(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Local returns a new Layer with the given name, it serves files from the given local path.
|
||||||
|
func Local(name, base string, sub ...string) *Layer {
|
||||||
|
// TODO: the old behavior (StaticRootPath might not be absolute), not ideal, just keep the same as before
|
||||||
|
// Ideally, the caller should guarantee the base is absolute, guessing a relative path based on the current working directory is unreliable.
|
||||||
|
base, err := filepath.Abs(base)
|
||||||
|
if err != nil {
|
||||||
|
// This should never happen in a real system. If it happens, the user must have already been in trouble: the system is not able to resolve its own paths.
|
||||||
|
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}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bindata returns a new Layer with the given name, it serves files from the given bindata asset.
|
||||||
|
func Bindata(name string, fs http.FileSystem) *Layer {
|
||||||
|
return &Layer{name: name, fs: fs}
|
||||||
|
}
|
||||||
|
|
||||||
|
// LayeredFS is a layered asset file-system. It works like http.FileSystem, but it can have multiple layers.
|
||||||
|
// The first layer is the top layer, and it will be used first.
|
||||||
|
// If the file is not found in the top layer, it will be searched in the next layer.
|
||||||
|
type LayeredFS struct {
|
||||||
|
layers []*Layer
|
||||||
|
}
|
||||||
|
|
||||||
|
// Layered returns a new LayeredFS with the given layers. The first layer is the top layer.
|
||||||
|
func Layered(layers ...*Layer) *LayeredFS {
|
||||||
|
return &LayeredFS{layers: layers}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open opens the named file. The caller is responsible for closing the file.
|
||||||
|
func (l *LayeredFS) Open(name string) (http.File, error) {
|
||||||
|
for _, layer := range l.layers {
|
||||||
|
f, err := layer.Open(name)
|
||||||
|
if err == nil || !os.IsNotExist(err) {
|
||||||
|
return f, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, fs.ErrNotExist
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadFile reads the named file.
|
||||||
|
func (l *LayeredFS) ReadFile(elems ...string) ([]byte, error) {
|
||||||
|
bs, _, err := l.ReadLayeredFile(elems...)
|
||||||
|
return bs, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadLayeredFile reads the named file, and returns the layer name.
|
||||||
|
func (l *LayeredFS) ReadLayeredFile(elems ...string) ([]byte, string, error) {
|
||||||
|
name := util.PathJoinRel(elems...)
|
||||||
|
for _, layer := range l.layers {
|
||||||
|
f, err := layer.Open(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.CommonSkip(info.Name()) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if len(fileMode) == 0 {
|
||||||
|
return true
|
||||||
|
} else if len(fileMode) == 1 {
|
||||||
|
return fileMode[0] == !info.Mode().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) {
|
||||||
|
return nil, nil
|
||||||
|
} else if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
return f.Readdir(-1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListFiles lists files/directories in the given directory. The fileMode controls the returned files.
|
||||||
|
// * omitted: all files and directories will be returned.
|
||||||
|
// * true: only files will be returned.
|
||||||
|
// * false: only directories will be returned.
|
||||||
|
// The returned files are sorted by name.
|
||||||
|
func (l *LayeredFS) ListFiles(name string, fileMode ...bool) ([]string, error) {
|
||||||
|
fileMap := map[string]bool{}
|
||||||
|
for _, layer := range l.layers {
|
||||||
|
infos, err := readDir(layer, name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
for _, info := range infos {
|
||||||
|
if shouldInclude(info, fileMode...) {
|
||||||
|
fileMap[info.Name()] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
files := make([]string, 0, len(fileMap))
|
||||||
|
for file := range fileMap {
|
||||||
|
files = append(files, file)
|
||||||
|
}
|
||||||
|
sort.Strings(files)
|
||||||
|
return files, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListAllFiles returns files/directories in the given directory, including subdirectories, recursively.
|
||||||
|
// The fileMode controls the returned files:
|
||||||
|
// * omitted: all files and directories will be returned.
|
||||||
|
// * true: only files will be returned.
|
||||||
|
// * false: only directories will be returned.
|
||||||
|
// The returned files are sorted by name.
|
||||||
|
func (l *LayeredFS) ListAllFiles(name string, fileMode ...bool) ([]string, error) {
|
||||||
|
return listAllFiles(l.layers, name, fileMode...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func listAllFiles(layers []*Layer, name string, fileMode ...bool) ([]string, error) {
|
||||||
|
fileMap := map[string]bool{}
|
||||||
|
var list func(dir string) error
|
||||||
|
list = func(dir string) error {
|
||||||
|
for _, layer := range layers {
|
||||||
|
infos, err := readDir(layer, dir)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, info := range infos {
|
||||||
|
path := util.PathJoinRelX(dir, info.Name())
|
||||||
|
if shouldInclude(info, fileMode...) {
|
||||||
|
fileMap[path] = true
|
||||||
|
}
|
||||||
|
if info.IsDir() {
|
||||||
|
if err = list(path); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if err := list(name); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var files []string
|
||||||
|
for file := range fileMap {
|
||||||
|
files = append(files, file)
|
||||||
|
}
|
||||||
|
sort.Strings(files)
|
||||||
|
return files, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// WatchLocalChanges watches local changes in the file-system. It's used to help to reload assets when the local file-system changes.
|
||||||
|
func (l *LayeredFS) WatchLocalChanges(ctx context.Context, callback func()) {
|
||||||
|
ctx, _, finished := process.GetManager().AddTypedContext(ctx, "Asset Local FileSystem Watcher", process.SystemProcessType, true)
|
||||||
|
defer finished()
|
||||||
|
|
||||||
|
watcher, err := fsnotify.NewWatcher()
|
||||||
|
if err != nil {
|
||||||
|
log.Error("Unable to create watcher for asset local file-system: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer watcher.Close()
|
||||||
|
|
||||||
|
for _, layer := range l.layers {
|
||||||
|
if layer.localPath == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
layerDirs, err := listAllFiles([]*Layer{layer}, ".", false)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("Unable to list directories for asset local file-system %q: %v", layer.localPath, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for _, dir := range layerDirs {
|
||||||
|
if err = watcher.Add(util.FilePathJoinAbs(layer.localPath, dir)); err != nil {
|
||||||
|
log.Error("Unable to watch directory %s: %v", dir, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
debounce := util.Debounce(100 * time.Millisecond)
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
case event, ok := <-watcher.Events:
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.Trace("Watched asset local file-system had event: %v", event)
|
||||||
|
debounce(callback)
|
||||||
|
case err, ok := <-watcher.Errors:
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.Error("Watched asset local file-system had error: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetFileLayerName returns the name of the first-seen layer that contains the given file.
|
||||||
|
func (l *LayeredFS) GetFileLayerName(elems ...string) string {
|
||||||
|
name := util.PathJoinRel(elems...)
|
||||||
|
for _, layer := range l.layers {
|
||||||
|
f, err := layer.Open(name)
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
continue
|
||||||
|
} else if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
_ = f.Close()
|
||||||
|
return layer.name
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
109
modules/assetfs/layered_test.go
Normal file
109
modules/assetfs/layered_test.go
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package assetfs
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"io/fs"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestLayered(t *testing.T) {
|
||||||
|
dir := filepath.Join(t.TempDir(), "assetfs-layers")
|
||||||
|
dir1 := filepath.Join(dir, "l1")
|
||||||
|
dir2 := filepath.Join(dir, "l2")
|
||||||
|
|
||||||
|
mkdir := func(elems ...string) {
|
||||||
|
assert.NoError(t, os.MkdirAll(filepath.Join(elems...), 0o755))
|
||||||
|
}
|
||||||
|
write := func(content string, elems ...string) {
|
||||||
|
assert.NoError(t, os.WriteFile(filepath.Join(elems...), []byte(content), 0o644))
|
||||||
|
}
|
||||||
|
|
||||||
|
// d1 & f1: only in "l1"; d2 & f2: only in "l2"
|
||||||
|
// da & fa: in both "l1" and "l2"
|
||||||
|
mkdir(dir1, "d1")
|
||||||
|
mkdir(dir1, "da")
|
||||||
|
mkdir(dir1, "da/sub1")
|
||||||
|
|
||||||
|
mkdir(dir2, "d2")
|
||||||
|
mkdir(dir2, "da")
|
||||||
|
mkdir(dir2, "da/sub2")
|
||||||
|
|
||||||
|
write("dummy", dir1, ".DS_Store")
|
||||||
|
write("f1", dir1, "f1")
|
||||||
|
write("fa-1", dir1, "fa")
|
||||||
|
write("d1-f", dir1, "d1/f")
|
||||||
|
write("da-f-1", dir1, "da/f")
|
||||||
|
|
||||||
|
write("f2", dir2, "f2")
|
||||||
|
write("fa-2", dir2, "fa")
|
||||||
|
write("d2-f", dir2, "d2/f")
|
||||||
|
write("da-f-2", dir2, "da/f")
|
||||||
|
|
||||||
|
assets := Layered(Local("l1", dir1), Local("l2", dir2))
|
||||||
|
|
||||||
|
f, err := assets.Open("f1")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
bs, err := io.ReadAll(f)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.EqualValues(t, "f1", string(bs))
|
||||||
|
_ = f.Close()
|
||||||
|
|
||||||
|
assertRead := func(expected string, expectedErr error, elems ...string) {
|
||||||
|
bs, err := assets.ReadFile(elems...)
|
||||||
|
if err != nil {
|
||||||
|
assert.ErrorAs(t, err, &expectedErr)
|
||||||
|
} else {
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, expected, string(bs))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assertRead("f1", nil, "f1")
|
||||||
|
assertRead("f2", nil, "f2")
|
||||||
|
assertRead("fa-1", nil, "fa")
|
||||||
|
|
||||||
|
assertRead("d1-f", nil, "d1/f")
|
||||||
|
assertRead("d2-f", nil, "d2/f")
|
||||||
|
assertRead("da-f-1", nil, "da/f")
|
||||||
|
|
||||||
|
assertRead("", fs.ErrNotExist, "no-such")
|
||||||
|
|
||||||
|
files, err := assets.ListFiles(".", true)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.EqualValues(t, []string{"f1", "f2", "fa"}, files)
|
||||||
|
|
||||||
|
files, err = assets.ListFiles(".", false)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.EqualValues(t, []string{"d1", "d2", "da"}, files)
|
||||||
|
|
||||||
|
files, err = assets.ListFiles(".")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.EqualValues(t, []string{"d1", "d2", "da", "f1", "f2", "fa"}, files)
|
||||||
|
|
||||||
|
files, err = assets.ListAllFiles(".", true)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.EqualValues(t, []string{"d1/f", "d2/f", "da/f", "f1", "f2", "fa"}, files)
|
||||||
|
|
||||||
|
files, err = assets.ListAllFiles(".", false)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.EqualValues(t, []string{"d1", "d2", "da", "da/sub1", "da/sub2"}, files)
|
||||||
|
|
||||||
|
files, err = assets.ListAllFiles(".")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.EqualValues(t, []string{
|
||||||
|
"d1", "d1/f",
|
||||||
|
"d2", "d2/f",
|
||||||
|
"da", "da/f", "da/sub1", "da/sub2",
|
||||||
|
"f1", "f2", "fa",
|
||||||
|
}, files)
|
||||||
|
|
||||||
|
assert.Empty(t, assets.GetFileLayerName("no-such"))
|
||||||
|
assert.EqualValues(t, "l1", assets.GetFileLayerName("f1"))
|
||||||
|
assert.EqualValues(t, "l2", assets.GetFileLayerName("f2"))
|
||||||
|
}
|
@ -14,5 +14,9 @@ var Supported = false
|
|||||||
|
|
||||||
// Auth not supported lack of pam tag
|
// Auth not supported lack of pam tag
|
||||||
func Auth(serviceName, userName, passwd string) (string, error) {
|
func Auth(serviceName, userName, passwd string) (string, error) {
|
||||||
|
// bypass the lint on callers: SA4023: this comparison is always true (staticcheck)
|
||||||
|
if !Supported {
|
||||||
return "", errors.New("PAM not supported")
|
return "", errors.New("PAM not supported")
|
||||||
|
}
|
||||||
|
return "", nil
|
||||||
}
|
}
|
||||||
|
@ -240,19 +240,15 @@ func (ctx *Context) HTML(status int, name base.TplName) {
|
|||||||
}
|
}
|
||||||
line, _ := strconv.Atoi(lineStr) // Cannot error out as groups[2] is [1-9][0-9]*
|
line, _ := strconv.Atoi(lineStr) // Cannot error out as groups[2] is [1-9][0-9]*
|
||||||
pos, _ := strconv.Atoi(posStr) // Cannot error out as groups[3] is [1-9][0-9]*
|
pos, _ := strconv.Atoi(posStr) // Cannot error out as groups[3] is [1-9][0-9]*
|
||||||
filename, filenameErr := templates.GetAssetFilename("templates/" + errorTemplateName + ".tmpl")
|
assetLayerName := templates.AssetFS().GetFileLayerName(errorTemplateName + ".tmpl")
|
||||||
if filenameErr != nil {
|
filename := fmt.Sprintf("(%s) %s", assetLayerName, errorTemplateName)
|
||||||
filename = "(template) " + errorTemplateName
|
|
||||||
}
|
|
||||||
if errorTemplateName != string(name) {
|
if errorTemplateName != string(name) {
|
||||||
filename += " (subtemplate of " + string(name) + ")"
|
filename += " (subtemplate of " + string(name) + ")"
|
||||||
}
|
}
|
||||||
err = fmt.Errorf("failed to render %s, error: %w:\n%s", filename, err, templates.GetLineFromTemplate(errorTemplateName, line, target, pos))
|
err = fmt.Errorf("failed to render %s, error: %w:\n%s", filename, err, templates.GetLineFromTemplate(errorTemplateName, line, target, pos))
|
||||||
} else {
|
} else {
|
||||||
filename, filenameErr := templates.GetAssetFilename("templates/" + execErr.Name + ".tmpl")
|
assetLayerName := templates.AssetFS().GetFileLayerName(execErr.Name + ".tmpl")
|
||||||
if filenameErr != nil {
|
filename := fmt.Sprintf("(%s) %s", assetLayerName, execErr.Name)
|
||||||
filename = "(template) " + execErr.Name
|
|
||||||
}
|
|
||||||
if execErr.Name != string(name) {
|
if execErr.Name != string(name) {
|
||||||
filename += " (subtemplate of " + string(name) + ")"
|
filename += " (subtemplate of " + string(name) + ")"
|
||||||
}
|
}
|
||||||
|
@ -9,7 +9,6 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
|
|
||||||
"code.gitea.io/gitea/modules/log"
|
"code.gitea.io/gitea/modules/log"
|
||||||
"code.gitea.io/gitea/modules/options"
|
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -79,7 +78,7 @@ func checkConfigurationFiles(ctx context.Context, logger log.Logger, autofix boo
|
|||||||
{"Log Root Path", setting.Log.RootPath, true, true, true},
|
{"Log Root Path", setting.Log.RootPath, true, true, true},
|
||||||
}
|
}
|
||||||
|
|
||||||
if options.IsDynamic() {
|
if !setting.HasBuiltinBindata {
|
||||||
configurationFiles = append(configurationFiles, configurationFile{"Static File Root Path", setting.StaticRootPath, true, true, false})
|
configurationFiles = append(configurationFiles, configurationFile{"Static File Root Path", setting.StaticRootPath, true, true, false})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4,131 +4,39 @@
|
|||||||
package options
|
package options
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"code.gitea.io/gitea/modules/assetfs"
|
||||||
"io/fs"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
|
|
||||||
"code.gitea.io/gitea/modules/log"
|
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
"code.gitea.io/gitea/modules/util"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var directories = make(directorySet)
|
func CustomAssets() *assetfs.Layer {
|
||||||
|
return assetfs.Local("custom", setting.CustomPath, "options")
|
||||||
|
}
|
||||||
|
|
||||||
|
func AssetFS() *assetfs.LayeredFS {
|
||||||
|
return assetfs.Layered(CustomAssets(), BuiltinAssets())
|
||||||
|
}
|
||||||
|
|
||||||
// Locale reads the content of a specific locale from static/bindata or custom path.
|
// Locale reads the content of a specific locale from static/bindata or custom path.
|
||||||
func Locale(name string) ([]byte, error) {
|
func Locale(name string) ([]byte, error) {
|
||||||
return fileFromOptionsDir("locale", name)
|
return AssetFS().ReadFile("locale", name)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Readme reads the content of a specific readme from static/bindata or custom path.
|
// Readme reads the content of a specific readme from static/bindata or custom path.
|
||||||
func Readme(name string) ([]byte, error) {
|
func Readme(name string) ([]byte, error) {
|
||||||
return fileFromOptionsDir("readme", name)
|
return AssetFS().ReadFile("readme", name)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Gitignore reads the content of a gitignore locale from static/bindata or custom path.
|
// Gitignore reads the content of a gitignore locale from static/bindata or custom path.
|
||||||
func Gitignore(name string) ([]byte, error) {
|
func Gitignore(name string) ([]byte, error) {
|
||||||
return fileFromOptionsDir("gitignore", name)
|
return AssetFS().ReadFile("gitignore", name)
|
||||||
}
|
}
|
||||||
|
|
||||||
// License reads the content of a specific license from static/bindata or custom path.
|
// License reads the content of a specific license from static/bindata or custom path.
|
||||||
func License(name string) ([]byte, error) {
|
func License(name string) ([]byte, error) {
|
||||||
return fileFromOptionsDir("license", name)
|
return AssetFS().ReadFile("license", name)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Labels reads the content of a specific labels from static/bindata or custom path.
|
// Labels reads the content of a specific labels from static/bindata or custom path.
|
||||||
func Labels(name string) ([]byte, error) {
|
func Labels(name string) ([]byte, error) {
|
||||||
return fileFromOptionsDir("label", name)
|
return AssetFS().ReadFile("label", name)
|
||||||
}
|
|
||||||
|
|
||||||
// WalkLocales reads the content of a specific locale
|
|
||||||
func WalkLocales(callback func(path, name string, d fs.DirEntry, err error) error) error {
|
|
||||||
if IsDynamic() {
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
// mustLocalPathAbs coverts a path to absolute path
|
|
||||||
// FIXME: the old behavior (StaticRootPath might not be absolute), not ideal, just keep the same as before
|
|
||||||
func mustLocalPathAbs(s string) string {
|
|
||||||
abs, err := filepath.Abs(s)
|
|
||||||
if err != nil {
|
|
||||||
// This should never happen in a real system. If it happens, the user must have already been in trouble: the system is not able to resolve its own paths.
|
|
||||||
log.Fatal("Unable to get absolute path for %q: %v", s, err)
|
|
||||||
}
|
|
||||||
return abs
|
|
||||||
}
|
|
||||||
|
|
||||||
func joinLocalPaths(baseDirs []string, subDir string, elems ...string) (paths []string) {
|
|
||||||
abs := make([]string, len(elems)+2)
|
|
||||||
abs[1] = subDir
|
|
||||||
copy(abs[2:], elems)
|
|
||||||
for _, baseDir := range baseDirs {
|
|
||||||
abs[0] = mustLocalPathAbs(baseDir)
|
|
||||||
paths = append(paths, util.FilePathJoinAbs(abs...))
|
|
||||||
}
|
|
||||||
return paths
|
|
||||||
}
|
|
||||||
|
|
||||||
func listLocalDirIfExist(baseDirs []string, subDir string, elems ...string) (files []string, err error) {
|
|
||||||
for _, localPath := range joinLocalPaths(baseDirs, subDir, elems...) {
|
|
||||||
isDir, err := util.IsDir(localPath)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("unable to check if path %q is a directory. %w", localPath, err)
|
|
||||||
} else if !isDir {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
dirFiles, err := util.StatDir(localPath, true)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("unable to read directory %q. %w", localPath, err)
|
|
||||||
}
|
|
||||||
files = append(files, dirFiles...)
|
|
||||||
}
|
|
||||||
return files, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func readLocalFile(baseDirs []string, subDir string, elems ...string) ([]byte, error) {
|
|
||||||
for _, localPath := range joinLocalPaths(baseDirs, subDir, elems...) {
|
|
||||||
data, err := os.ReadFile(localPath)
|
|
||||||
if err == nil {
|
|
||||||
return data, nil
|
|
||||||
} else if !os.IsNotExist(err) {
|
|
||||||
log.Error("Unable to read file %q. Error: %v", localPath, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil, os.ErrNotExist
|
|
||||||
}
|
}
|
||||||
|
@ -6,29 +6,10 @@
|
|||||||
package options
|
package options
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"code.gitea.io/gitea/modules/assetfs"
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Dir returns all files from static or custom directory.
|
func BuiltinAssets() *assetfs.Layer {
|
||||||
func Dir(name string) ([]string, error) {
|
return assetfs.Local("builtin(static)", setting.StaticRootPath, "options")
|
||||||
if directories.Filled(name) {
|
|
||||||
return directories.Get(name), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
result, err := listLocalDirIfExist([]string{setting.CustomPath, setting.StaticRootPath}, "options", name)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return directories.AddAndGet(name, result), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// fileFromOptionsDir is a helper to read files from custom or static path.
|
|
||||||
func fileFromOptionsDir(elems ...string) ([]byte, error) {
|
|
||||||
return readLocalFile([]string{setting.CustomPath, setting.StaticRootPath}, "options", elems...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsDynamic will return false when using embedded data (-tags bindata)
|
|
||||||
func IsDynamic() bool {
|
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
|
@ -1,45 +0,0 @@
|
|||||||
// Copyright 2016 The Gitea Authors. All rights reserved.
|
|
||||||
// SPDX-License-Identifier: MIT
|
|
||||||
|
|
||||||
package options
|
|
||||||
|
|
||||||
type directorySet map[string][]string
|
|
||||||
|
|
||||||
func (s directorySet) Add(key string, value []string) {
|
|
||||||
_, ok := s[key]
|
|
||||||
|
|
||||||
if !ok {
|
|
||||||
s[key] = make([]string, 0, len(value))
|
|
||||||
}
|
|
||||||
|
|
||||||
s[key] = append(s[key], value...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s directorySet) Get(key string) []string {
|
|
||||||
_, ok := s[key]
|
|
||||||
|
|
||||||
if ok {
|
|
||||||
result := []string{}
|
|
||||||
seen := map[string]string{}
|
|
||||||
|
|
||||||
for _, val := range s[key] {
|
|
||||||
if _, ok := seen[val]; !ok {
|
|
||||||
result = append(result, val)
|
|
||||||
seen[val] = val
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
return []string{}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s directorySet) AddAndGet(key string, value []string) []string {
|
|
||||||
s.Add(key, value)
|
|
||||||
return s.Get(key)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s directorySet) Filled(key string) bool {
|
|
||||||
return len(s[key]) > 0
|
|
||||||
}
|
|
@ -6,98 +6,9 @@
|
|||||||
package options
|
package options
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"code.gitea.io/gitea/modules/assetfs"
|
||||||
"io"
|
|
||||||
|
|
||||||
"code.gitea.io/gitea/modules/setting"
|
|
||||||
"code.gitea.io/gitea/modules/util"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Dir returns all files from custom directory or bindata.
|
func BuiltinAssets() *assetfs.Layer {
|
||||||
func Dir(name string) ([]string, error) {
|
return assetfs.Bindata("builtin(bindata)", Assets)
|
||||||
if directories.Filled(name) {
|
|
||||||
return directories.Get(name), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
result, err := listLocalDirIfExist([]string{setting.CustomPath}, "options", name)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
files, err := AssetDir(name)
|
|
||||||
if err != nil {
|
|
||||||
return []string{}, fmt.Errorf("unable to read embedded directory %q. %w", name, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
result = append(result, files...)
|
|
||||||
return directories.AddAndGet(name, result), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func AssetDir(dirName string) ([]string, error) {
|
|
||||||
d, err := Assets.Open(dirName)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer d.Close()
|
|
||||||
|
|
||||||
files, err := d.Readdir(-1)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
results := make([]string, 0, len(files))
|
|
||||||
for _, file := range files {
|
|
||||||
results = append(results, file.Name())
|
|
||||||
}
|
|
||||||
return results, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// fileFromOptionsDir is a helper to read files from custom path or bindata.
|
|
||||||
func fileFromOptionsDir(elems ...string) ([]byte, error) {
|
|
||||||
// only try custom dir, no static dir
|
|
||||||
if data, err := readLocalFile([]string{setting.CustomPath}, "options", elems...); err == nil {
|
|
||||||
return data, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
f, err := Assets.Open(util.PathJoinRelX(elems...))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer f.Close()
|
|
||||||
return io.ReadAll(f)
|
|
||||||
}
|
|
||||||
|
|
||||||
func Asset(name string) ([]byte, error) {
|
|
||||||
f, err := Assets.Open("/" + name)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer f.Close()
|
|
||||||
return io.ReadAll(f)
|
|
||||||
}
|
|
||||||
|
|
||||||
func AssetNames() []string {
|
|
||||||
realFS := Assets.(vfsgen۰FS)
|
|
||||||
results := make([]string, 0, len(realFS))
|
|
||||||
for k := range realFS {
|
|
||||||
results = append(results, k[1:])
|
|
||||||
}
|
|
||||||
return results
|
|
||||||
}
|
|
||||||
|
|
||||||
func AssetIsDir(name string) (bool, error) {
|
|
||||||
if f, err := Assets.Open("/" + name); err != nil {
|
|
||||||
return false, err
|
|
||||||
} else {
|
|
||||||
defer f.Close()
|
|
||||||
if fi, err := f.Stat(); err != nil {
|
|
||||||
return false, err
|
|
||||||
} else {
|
|
||||||
return fi.IsDir(), nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsDynamic will return false when using embedded data (-tags bindata)
|
|
||||||
func IsDynamic() bool {
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
@ -4,11 +4,15 @@
|
|||||||
package public
|
package public
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"code.gitea.io/gitea/modules/assetfs"
|
||||||
"code.gitea.io/gitea/modules/container"
|
"code.gitea.io/gitea/modules/container"
|
||||||
"code.gitea.io/gitea/modules/httpcache"
|
"code.gitea.io/gitea/modules/httpcache"
|
||||||
"code.gitea.io/gitea/modules/log"
|
"code.gitea.io/gitea/modules/log"
|
||||||
@ -16,55 +20,31 @@ import (
|
|||||||
"code.gitea.io/gitea/modules/util"
|
"code.gitea.io/gitea/modules/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Options represents the available options to configure the handler.
|
func CustomAssets() *assetfs.Layer {
|
||||||
type Options struct {
|
return assetfs.Local("custom", setting.CustomPath, "public")
|
||||||
Directory string
|
|
||||||
Prefix string
|
|
||||||
CorsHandler func(http.Handler) http.Handler
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// AssetsURLPathPrefix is the path prefix for static asset files
|
func AssetFS() *assetfs.LayeredFS {
|
||||||
const AssetsURLPathPrefix = "/assets/"
|
return assetfs.Layered(CustomAssets(), BuiltinAssets())
|
||||||
|
}
|
||||||
|
|
||||||
// AssetsHandlerFunc implements the static handler for serving custom or original assets.
|
// AssetsHandlerFunc implements the static handler for serving custom or original assets.
|
||||||
func AssetsHandlerFunc(opts *Options) http.HandlerFunc {
|
func AssetsHandlerFunc(prefix string) http.HandlerFunc {
|
||||||
custPath := filepath.Join(setting.CustomPath, "public")
|
assetFS := AssetFS()
|
||||||
if !filepath.IsAbs(custPath) {
|
prefix = strings.TrimSuffix(prefix, "/") + "/"
|
||||||
custPath = filepath.Join(setting.AppWorkPath, custPath)
|
|
||||||
}
|
|
||||||
if !filepath.IsAbs(opts.Directory) {
|
|
||||||
opts.Directory = filepath.Join(setting.AppWorkPath, opts.Directory)
|
|
||||||
}
|
|
||||||
if !strings.HasSuffix(opts.Prefix, "/") {
|
|
||||||
opts.Prefix += "/"
|
|
||||||
}
|
|
||||||
|
|
||||||
return func(resp http.ResponseWriter, req *http.Request) {
|
return func(resp http.ResponseWriter, req *http.Request) {
|
||||||
|
subPath := req.URL.Path
|
||||||
|
if !strings.HasPrefix(subPath, prefix) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
subPath = strings.TrimPrefix(subPath, prefix)
|
||||||
|
|
||||||
if req.Method != "GET" && req.Method != "HEAD" {
|
if req.Method != "GET" && req.Method != "HEAD" {
|
||||||
resp.WriteHeader(http.StatusNotFound)
|
resp.WriteHeader(http.StatusNotFound)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if opts.CorsHandler != nil {
|
if handleRequest(resp, req, assetFS, subPath) {
|
||||||
var corsSent bool
|
|
||||||
opts.CorsHandler(http.HandlerFunc(func(http.ResponseWriter, *http.Request) {
|
|
||||||
corsSent = true
|
|
||||||
})).ServeHTTP(resp, req)
|
|
||||||
// If CORS is not sent, the response must have been written by other handlers
|
|
||||||
if !corsSent {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
file := req.URL.Path[len(opts.Prefix):]
|
|
||||||
|
|
||||||
// custom files
|
|
||||||
if opts.handle(resp, req, http.Dir(custPath), file) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// internal files
|
|
||||||
if opts.handle(resp, req, fileSystem(opts.Directory), file) {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -85,13 +65,13 @@ func parseAcceptEncoding(val string) container.Set[string] {
|
|||||||
// setWellKnownContentType will set the Content-Type if the file is a well-known type.
|
// setWellKnownContentType will set the Content-Type if the file is a well-known type.
|
||||||
// See the comments of detectWellKnownMimeType
|
// See the comments of detectWellKnownMimeType
|
||||||
func setWellKnownContentType(w http.ResponseWriter, file string) {
|
func setWellKnownContentType(w http.ResponseWriter, file string) {
|
||||||
mimeType := detectWellKnownMimeType(filepath.Ext(file))
|
mimeType := detectWellKnownMimeType(path.Ext(file))
|
||||||
if mimeType != "" {
|
if mimeType != "" {
|
||||||
w.Header().Set("Content-Type", mimeType)
|
w.Header().Set("Content-Type", mimeType)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (opts *Options) handle(w http.ResponseWriter, req *http.Request, fs http.FileSystem, file string) bool {
|
func handleRequest(w http.ResponseWriter, req *http.Request, fs http.FileSystem, file string) bool {
|
||||||
// actually, fs (http.FileSystem) is designed to be a safe interface, relative paths won't bypass its parent directory, it's also fine to do a clean here
|
// actually, fs (http.FileSystem) is designed to be a safe interface, relative paths won't bypass its parent directory, it's also fine to do a clean here
|
||||||
f, err := fs.Open(util.PathJoinRelX(file))
|
f, err := fs.Open(util.PathJoinRelX(file))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -121,8 +101,34 @@ func (opts *Options) handle(w http.ResponseWriter, req *http.Request, fs http.Fi
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
setWellKnownContentType(w, file)
|
|
||||||
|
|
||||||
serveContent(w, req, fi, fi.ModTime(), f)
|
serveContent(w, req, fi, fi.ModTime(), f)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type GzipBytesProvider interface {
|
||||||
|
GzipBytes() []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
// serveContent serve http content
|
||||||
|
func serveContent(w http.ResponseWriter, req *http.Request, fi os.FileInfo, modtime time.Time, content io.ReadSeeker) {
|
||||||
|
setWellKnownContentType(w, fi.Name())
|
||||||
|
|
||||||
|
encodings := parseAcceptEncoding(req.Header.Get("Accept-Encoding"))
|
||||||
|
if encodings.Contains("gzip") {
|
||||||
|
// try to provide gzip content directly from bindata (provided by vfsgen۰CompressedFileInfo)
|
||||||
|
if compressed, ok := fi.(GzipBytesProvider); ok {
|
||||||
|
rdGzip := bytes.NewReader(compressed.GzipBytes())
|
||||||
|
// all gzipped static files (from bindata) are managed by Gitea, so we can make sure every file has the correct ext name
|
||||||
|
// then we can get the correct Content-Type, we do not need to do http.DetectContentType on the decompressed data
|
||||||
|
if w.Header().Get("Content-Type") == "" {
|
||||||
|
w.Header().Set("Content-Type", "application/octet-stream")
|
||||||
|
}
|
||||||
|
w.Header().Set("Content-Encoding", "gzip")
|
||||||
|
http.ServeContent(w, req, fi.Name(), modtime, rdGzip)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
http.ServeContent(w, req, fi.Name(), modtime, content)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
@ -6,17 +6,10 @@
|
|||||||
package public
|
package public
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"io"
|
"code.gitea.io/gitea/modules/assetfs"
|
||||||
"net/http"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
"os"
|
|
||||||
"time"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func fileSystem(dir string) http.FileSystem {
|
func BuiltinAssets() *assetfs.Layer {
|
||||||
return http.Dir(dir)
|
return assetfs.Local("builtin(static)", setting.StaticRootPath, "public")
|
||||||
}
|
|
||||||
|
|
||||||
// serveContent serve http content
|
|
||||||
func serveContent(w http.ResponseWriter, req *http.Request, fi os.FileInfo, modtime time.Time, content io.ReadSeeker) {
|
|
||||||
http.ServeContent(w, req, fi.Name(), modtime, content)
|
|
||||||
}
|
}
|
||||||
|
@ -6,75 +6,19 @@
|
|||||||
package public
|
package public
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"code.gitea.io/gitea/modules/assetfs"
|
||||||
"code.gitea.io/gitea/modules/timeutil"
|
"code.gitea.io/gitea/modules/timeutil"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var _ GzipBytesProvider = (*vfsgen۰CompressedFileInfo)(nil)
|
||||||
|
|
||||||
// GlobalModTime provide a global mod time for embedded asset files
|
// GlobalModTime provide a global mod time for embedded asset files
|
||||||
func GlobalModTime(filename string) time.Time {
|
func GlobalModTime(filename string) time.Time {
|
||||||
return timeutil.GetExecutableModTime()
|
return timeutil.GetExecutableModTime()
|
||||||
}
|
}
|
||||||
|
|
||||||
func fileSystem(dir string) http.FileSystem {
|
func BuiltinAssets() *assetfs.Layer {
|
||||||
return Assets
|
return assetfs.Bindata("builtin(bindata)", Assets)
|
||||||
}
|
|
||||||
|
|
||||||
func Asset(name string) ([]byte, error) {
|
|
||||||
f, err := Assets.Open("/" + name)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer f.Close()
|
|
||||||
return io.ReadAll(f)
|
|
||||||
}
|
|
||||||
|
|
||||||
func AssetNames() []string {
|
|
||||||
realFS := Assets.(vfsgen۰FS)
|
|
||||||
results := make([]string, 0, len(realFS))
|
|
||||||
for k := range realFS {
|
|
||||||
results = append(results, k[1:])
|
|
||||||
}
|
|
||||||
return results
|
|
||||||
}
|
|
||||||
|
|
||||||
func AssetIsDir(name string) (bool, error) {
|
|
||||||
if f, err := Assets.Open("/" + name); err != nil {
|
|
||||||
return false, err
|
|
||||||
} else {
|
|
||||||
defer f.Close()
|
|
||||||
if fi, err := f.Stat(); err != nil {
|
|
||||||
return false, err
|
|
||||||
} else {
|
|
||||||
return fi.IsDir(), nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// serveContent serve http content
|
|
||||||
func serveContent(w http.ResponseWriter, req *http.Request, fi os.FileInfo, modtime time.Time, content io.ReadSeeker) {
|
|
||||||
encodings := parseAcceptEncoding(req.Header.Get("Accept-Encoding"))
|
|
||||||
if encodings.Contains("gzip") {
|
|
||||||
if cf, ok := fi.(*vfsgen۰CompressedFileInfo); ok {
|
|
||||||
rdGzip := bytes.NewReader(cf.GzipBytes())
|
|
||||||
// all static files are managed by Gitea, so we can make sure every file has the correct ext name
|
|
||||||
// then we can get the correct Content-Type, we do not need to do http.DetectContentType on the decompressed data
|
|
||||||
mimeType := detectWellKnownMimeType(filepath.Ext(fi.Name()))
|
|
||||||
if mimeType == "" {
|
|
||||||
mimeType = "application/octet-stream"
|
|
||||||
}
|
|
||||||
w.Header().Set("Content-Type", mimeType)
|
|
||||||
w.Header().Set("Content-Encoding", "gzip")
|
|
||||||
http.ServeContent(w, req, fi.Name(), modtime, rdGzip)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
http.ServeContent(w, req, fi.Name(), modtime, content)
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
@ -79,7 +79,7 @@ func LoadRepoConfig() error {
|
|||||||
typeFiles := make([]optionFileList, len(types))
|
typeFiles := make([]optionFileList, len(types))
|
||||||
for i, t := range types {
|
for i, t := range types {
|
||||||
var err error
|
var err error
|
||||||
if typeFiles[i].all, err = options.Dir(t); err != nil {
|
if typeFiles[i].all, err = options.AssetFS().ListFiles(t, true); err != nil {
|
||||||
return fmt.Errorf("failed to list %s files: %w", t, err)
|
return fmt.Errorf("failed to list %s files: %w", t, err)
|
||||||
}
|
}
|
||||||
sort.Strings(typeFiles[i].all)
|
sort.Strings(typeFiles[i].all)
|
||||||
|
8
modules/setting/asset_dynamic.go
Normal file
8
modules/setting/asset_dynamic.go
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
//go:build !bindata
|
||||||
|
|
||||||
|
package setting
|
||||||
|
|
||||||
|
const HasBuiltinBindata = false
|
8
modules/setting/asset_static.go
Normal file
8
modules/setting/asset_static.go
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
//go:build bindata
|
||||||
|
|
||||||
|
package setting
|
||||||
|
|
||||||
|
const HasBuiltinBindata = true
|
@ -1,30 +0,0 @@
|
|||||||
// Copyright 2020 The Gitea Authors. All rights reserved.
|
|
||||||
// SPDX-License-Identifier: MIT
|
|
||||||
|
|
||||||
//go:build bindata
|
|
||||||
|
|
||||||
package svg
|
|
||||||
|
|
||||||
import (
|
|
||||||
"path/filepath"
|
|
||||||
|
|
||||||
"code.gitea.io/gitea/modules/public"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Discover returns a map of discovered SVG icons in bindata
|
|
||||||
func Discover() map[string]string {
|
|
||||||
svgs := make(map[string]string)
|
|
||||||
|
|
||||||
for _, file := range public.AssetNames() {
|
|
||||||
matched, _ := filepath.Match("img/svg/*.svg", file)
|
|
||||||
if matched {
|
|
||||||
content, err := public.Asset(file)
|
|
||||||
if err == nil {
|
|
||||||
filename := filepath.Base(file)
|
|
||||||
svgs[filename[:len(filename)-4]] = string(content)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return svgs
|
|
||||||
}
|
|
@ -1,29 +0,0 @@
|
|||||||
// Copyright 2020 The Gitea Authors. All rights reserved.
|
|
||||||
// SPDX-License-Identifier: MIT
|
|
||||||
|
|
||||||
//go:build !bindata
|
|
||||||
|
|
||||||
package svg
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
|
|
||||||
"code.gitea.io/gitea/modules/setting"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Discover returns a map of discovered SVG icons in the file system
|
|
||||||
func Discover() map[string]string {
|
|
||||||
svgs := make(map[string]string)
|
|
||||||
|
|
||||||
files, _ := filepath.Glob(filepath.Join(setting.StaticRootPath, "public", "img", "svg", "*.svg"))
|
|
||||||
for _, file := range files {
|
|
||||||
content, err := os.ReadFile(file)
|
|
||||||
if err == nil {
|
|
||||||
filename := filepath.Base(file)
|
|
||||||
svgs[filename[:len(filename)-4]] = string(content)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return svgs
|
|
||||||
}
|
|
@ -6,15 +6,18 @@ package svg
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"html/template"
|
"html/template"
|
||||||
|
"path"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"code.gitea.io/gitea/modules/html"
|
"code.gitea.io/gitea/modules/html"
|
||||||
|
"code.gitea.io/gitea/modules/log"
|
||||||
|
"code.gitea.io/gitea/modules/public"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
// SVGs contains discovered SVGs
|
// SVGs contains discovered SVGs
|
||||||
SVGs map[string]string
|
SVGs = map[string]string{}
|
||||||
|
|
||||||
widthRe = regexp.MustCompile(`width="[0-9]+?"`)
|
widthRe = regexp.MustCompile(`width="[0-9]+?"`)
|
||||||
heightRe = regexp.MustCompile(`height="[0-9]+?"`)
|
heightRe = regexp.MustCompile(`height="[0-9]+?"`)
|
||||||
@ -23,17 +26,29 @@ var (
|
|||||||
const defaultSize = 16
|
const defaultSize = 16
|
||||||
|
|
||||||
// Init discovers SVGs and populates the `SVGs` variable
|
// Init discovers SVGs and populates the `SVGs` variable
|
||||||
func Init() {
|
func Init() error {
|
||||||
SVGs = Discover()
|
files, err := public.AssetFS().ListFiles("img/svg")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
// Remove `xmlns` because inline SVG does not need it
|
// Remove `xmlns` because inline SVG does not need it
|
||||||
r := regexp.MustCompile(`(<svg\b[^>]*?)\s+xmlns="[^"]*"`)
|
reXmlns := regexp.MustCompile(`(<svg\b[^>]*?)\s+xmlns="[^"]*"`)
|
||||||
for name, svg := range SVGs {
|
for _, file := range files {
|
||||||
SVGs[name] = r.ReplaceAllString(svg, "$1")
|
if path.Ext(file) != ".svg" {
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
|
bs, err := public.AssetFS().ReadFile("img/svg", file)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("Failed to read SVG file %s: %v", file, err)
|
||||||
|
} else {
|
||||||
|
SVGs[file[:len(file)-4]] = reXmlns.ReplaceAllString(string(bs), "$1")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Render render icons - arguments icon name (string), size (int), class (string)
|
// RenderHTML renders icons - arguments icon name (string), size (int), class (string)
|
||||||
func RenderHTML(icon string, others ...interface{}) template.HTML {
|
func RenderHTML(icon string, others ...interface{}) template.HTML {
|
||||||
size, class := html.ParseSizeAndClass(defaultSize, "", others...)
|
size, class := html.ParseSizeAndClass(defaultSize, "", others...)
|
||||||
|
|
||||||
|
@ -4,14 +4,10 @@
|
|||||||
package templates
|
package templates
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"io/fs"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"code.gitea.io/gitea/modules/log"
|
"code.gitea.io/gitea/modules/assetfs"
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
"code.gitea.io/gitea/modules/util"
|
"code.gitea.io/gitea/modules/util"
|
||||||
)
|
)
|
||||||
@ -47,81 +43,30 @@ func BaseVars() Vars {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func getDirTemplateAssetNames(dir string) []string {
|
func AssetFS() *assetfs.LayeredFS {
|
||||||
return getDirAssetNames(dir, false)
|
return assetfs.Layered(CustomAssets(), BuiltinAssets())
|
||||||
}
|
}
|
||||||
|
|
||||||
func getDirAssetNames(dir string, mailer bool) []string {
|
func CustomAssets() *assetfs.Layer {
|
||||||
var tmpls []string
|
return assetfs.Local("custom", setting.CustomPath, "templates")
|
||||||
|
|
||||||
if mailer {
|
|
||||||
dir += filepath.Join(dir, "mail")
|
|
||||||
}
|
|
||||||
f, err := os.Stat(dir)
|
|
||||||
if err != nil {
|
|
||||||
if os.IsNotExist(err) {
|
|
||||||
return tmpls
|
|
||||||
}
|
|
||||||
log.Warn("Unable to check if templates dir %s is a directory. Error: %v", dir, err)
|
|
||||||
return tmpls
|
|
||||||
}
|
|
||||||
if !f.IsDir() {
|
|
||||||
log.Warn("Templates dir %s is a not directory.", dir)
|
|
||||||
return tmpls
|
|
||||||
}
|
|
||||||
|
|
||||||
files, err := util.StatDir(dir)
|
|
||||||
if err != nil {
|
|
||||||
log.Warn("Failed to read %s templates dir. %v", dir, err)
|
|
||||||
return tmpls
|
|
||||||
}
|
|
||||||
|
|
||||||
prefix := "templates/"
|
|
||||||
if mailer {
|
|
||||||
prefix += "mail/"
|
|
||||||
}
|
|
||||||
for _, filePath := range files {
|
|
||||||
if !mailer && strings.HasPrefix(filePath, "mail/") {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if !strings.HasSuffix(filePath, ".tmpl") {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
tmpls = append(tmpls, prefix+filePath)
|
|
||||||
}
|
|
||||||
return tmpls
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func walkAssetDir(root string, skipMail bool, callback func(path, name string, d fs.DirEntry, err error) error) error {
|
func ListWebTemplateAssetNames(assets *assetfs.LayeredFS) ([]string, error) {
|
||||||
mailRoot := filepath.Join(root, "mail")
|
files, err := assets.ListAllFiles(".", true)
|
||||||
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 err != nil {
|
||||||
if os.IsNotExist(err) {
|
return nil, err
|
||||||
return callback(path, name, d, err)
|
|
||||||
}
|
}
|
||||||
return err
|
return util.SliceRemoveAllFunc(files, func(file string) bool {
|
||||||
}
|
return strings.HasPrefix(file, "mail/") || !strings.HasSuffix(file, ".tmpl")
|
||||||
if skipMail && path == mailRoot && d.IsDir() {
|
}), nil
|
||||||
return fs.SkipDir
|
}
|
||||||
}
|
|
||||||
if util.CommonSkip(d.Name()) {
|
func ListMailTemplateAssetNames(assets *assetfs.LayeredFS) ([]string, error) {
|
||||||
if d.IsDir() {
|
files, err := assets.ListAllFiles(".", true)
|
||||||
return fs.SkipDir
|
if err != nil {
|
||||||
}
|
return nil, err
|
||||||
return nil
|
}
|
||||||
}
|
return util.SliceRemoveAllFunc(files, func(file string) bool {
|
||||||
if strings.HasSuffix(d.Name(), ".tmpl") || d.IsDir() {
|
return !strings.HasPrefix(file, "mail/") || !strings.HasSuffix(file, ".tmpl")
|
||||||
return callback(path, name, d, err)
|
}), nil
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}); err != nil && !os.IsNotExist(err) {
|
|
||||||
return fmt.Errorf("unable to get files for template assets in %s: %w", root, err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
@ -6,76 +6,10 @@
|
|||||||
package templates
|
package templates
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"io/fs"
|
"code.gitea.io/gitea/modules/assetfs"
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
|
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
)
|
)
|
||||||
|
|
||||||
// GetAsset returns asset content via name
|
func BuiltinAssets() *assetfs.Layer {
|
||||||
func GetAsset(name string) ([]byte, error) {
|
return assetfs.Local("builtin(static)", setting.StaticRootPath, "templates")
|
||||||
bs, err := os.ReadFile(filepath.Join(setting.CustomPath, name))
|
|
||||||
if err != nil && !os.IsNotExist(err) {
|
|
||||||
return nil, err
|
|
||||||
} else if err == nil {
|
|
||||||
return bs, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return os.ReadFile(filepath.Join(setting.StaticRootPath, name))
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetAssetFilename returns the filename of the provided asset
|
|
||||||
func GetAssetFilename(name string) (string, error) {
|
|
||||||
filename := filepath.Join(setting.CustomPath, name)
|
|
||||||
_, err := os.Stat(filename)
|
|
||||||
if err != nil && !os.IsNotExist(err) {
|
|
||||||
return filename, err
|
|
||||||
} else if err == nil {
|
|
||||||
return filename, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
filename = filepath.Join(setting.StaticRootPath, name)
|
|
||||||
_, err = os.Stat(filename)
|
|
||||||
return filename, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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...)
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
if err := walkAssetDir(filepath.Join(setting.CustomPath, "templates", "mail"), false, callback); err != nil && !os.IsNotExist(err) {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
@ -21,7 +21,6 @@ import (
|
|||||||
"code.gitea.io/gitea/modules/log"
|
"code.gitea.io/gitea/modules/log"
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
"code.gitea.io/gitea/modules/util"
|
"code.gitea.io/gitea/modules/util"
|
||||||
"code.gitea.io/gitea/modules/watcher"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@ -66,20 +65,23 @@ func (h *HTMLRender) TemplateLookup(name string) (*template.Template, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (h *HTMLRender) CompileTemplates() error {
|
func (h *HTMLRender) CompileTemplates() error {
|
||||||
dirPrefix := "templates/"
|
|
||||||
extSuffix := ".tmpl"
|
extSuffix := ".tmpl"
|
||||||
tmpls := template.New("")
|
tmpls := template.New("")
|
||||||
for _, path := range GetTemplateAssetNames() {
|
assets := AssetFS()
|
||||||
if !strings.HasSuffix(path, extSuffix) {
|
files, err := ListWebTemplateAssetNames(assets)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
for _, file := range files {
|
||||||
|
if !strings.HasSuffix(file, extSuffix) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
name := strings.TrimPrefix(path, dirPrefix)
|
name := strings.TrimSuffix(file, extSuffix)
|
||||||
name = strings.TrimSuffix(name, extSuffix)
|
|
||||||
tmpl := tmpls.New(filepath.ToSlash(name))
|
tmpl := tmpls.New(filepath.ToSlash(name))
|
||||||
for _, fm := range NewFuncMap() {
|
for _, fm := range NewFuncMap() {
|
||||||
tmpl.Funcs(fm)
|
tmpl.Funcs(fm)
|
||||||
}
|
}
|
||||||
buf, err := GetAsset(path)
|
buf, err := assets.ReadFile(file)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -112,13 +114,10 @@ func HTMLRenderer(ctx context.Context) (context.Context, *HTMLRender) {
|
|||||||
log.Fatal("HTMLRenderer error: %v", err)
|
log.Fatal("HTMLRenderer error: %v", err)
|
||||||
}
|
}
|
||||||
if !setting.IsProd {
|
if !setting.IsProd {
|
||||||
watcher.CreateWatcher(ctx, "HTML Templates", &watcher.CreateWatcherOpts{
|
go AssetFS().WatchLocalChanges(ctx, func() {
|
||||||
PathsCallback: walkTemplateFiles,
|
|
||||||
BetweenCallback: func() {
|
|
||||||
if err := renderer.CompileTemplates(); err != nil {
|
if err := renderer.CompileTemplates(); err != nil {
|
||||||
log.Error("Template error: %v\n%s", err, log.Stack(2))
|
log.Error("Template error: %v\n%s", err, log.Stack(2))
|
||||||
}
|
}
|
||||||
},
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return context.WithValue(ctx, rendererKey, renderer), renderer
|
return context.WithValue(ctx, rendererKey, renderer), renderer
|
||||||
@ -138,14 +137,8 @@ func handleGenericTemplateError(err error) (string, []interface{}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
templateName, lineNumberStr, message := groups[1], groups[2], groups[3]
|
templateName, lineNumberStr, message := groups[1], groups[2], groups[3]
|
||||||
|
filename := fmt.Sprintf("%s (provided by %s)", templateName, AssetFS().GetFileLayerName(templateName+".tmpl"))
|
||||||
filename, assetErr := GetAssetFilename("templates/" + templateName + ".tmpl")
|
|
||||||
if assetErr != nil {
|
|
||||||
return "", nil
|
|
||||||
}
|
|
||||||
|
|
||||||
lineNumber, _ := strconv.Atoi(lineNumberStr)
|
lineNumber, _ := strconv.Atoi(lineNumberStr)
|
||||||
|
|
||||||
line := GetLineFromTemplate(templateName, lineNumber, "", -1)
|
line := GetLineFromTemplate(templateName, lineNumber, "", -1)
|
||||||
|
|
||||||
return "PANIC: Unable to compile templates!\n%s in template file %s at line %d:\n\n%s\nStacktrace:\n\n%s", []interface{}{message, filename, lineNumber, log.NewColoredValue(line, log.Reset), log.Stack(2)}
|
return "PANIC: Unable to compile templates!\n%s in template file %s at line %d:\n\n%s\nStacktrace:\n\n%s", []interface{}{message, filename, lineNumber, log.NewColoredValue(line, log.Reset), log.Stack(2)}
|
||||||
@ -158,16 +151,9 @@ func handleNotDefinedPanicError(err error) (string, []interface{}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
templateName, lineNumberStr, functionName := groups[1], groups[2], groups[3]
|
templateName, lineNumberStr, functionName := groups[1], groups[2], groups[3]
|
||||||
|
|
||||||
functionName, _ = strconv.Unquote(`"` + functionName + `"`)
|
functionName, _ = strconv.Unquote(`"` + functionName + `"`)
|
||||||
|
filename := fmt.Sprintf("%s (provided by %s)", templateName, AssetFS().GetFileLayerName(templateName+".tmpl"))
|
||||||
filename, assetErr := GetAssetFilename("templates/" + templateName + ".tmpl")
|
|
||||||
if assetErr != nil {
|
|
||||||
return "", nil
|
|
||||||
}
|
|
||||||
|
|
||||||
lineNumber, _ := strconv.Atoi(lineNumberStr)
|
lineNumber, _ := strconv.Atoi(lineNumberStr)
|
||||||
|
|
||||||
line := GetLineFromTemplate(templateName, lineNumber, functionName, -1)
|
line := GetLineFromTemplate(templateName, lineNumber, functionName, -1)
|
||||||
|
|
||||||
return "PANIC: Unable to compile templates!\nUndefined function %q in template file %s at line %d:\n\n%s", []interface{}{functionName, filename, lineNumber, log.NewColoredValue(line, log.Reset)}
|
return "PANIC: Unable to compile templates!\nUndefined function %q in template file %s at line %d:\n\n%s", []interface{}{functionName, filename, lineNumber, log.NewColoredValue(line, log.Reset)}
|
||||||
@ -181,14 +167,8 @@ func handleUnexpected(err error) (string, []interface{}) {
|
|||||||
|
|
||||||
templateName, lineNumberStr, unexpected := groups[1], groups[2], groups[3]
|
templateName, lineNumberStr, unexpected := groups[1], groups[2], groups[3]
|
||||||
unexpected, _ = strconv.Unquote(`"` + unexpected + `"`)
|
unexpected, _ = strconv.Unquote(`"` + unexpected + `"`)
|
||||||
|
filename := fmt.Sprintf("%s (provided by %s)", templateName, AssetFS().GetFileLayerName(templateName+".tmpl"))
|
||||||
filename, assetErr := GetAssetFilename("templates/" + templateName + ".tmpl")
|
|
||||||
if assetErr != nil {
|
|
||||||
return "", nil
|
|
||||||
}
|
|
||||||
|
|
||||||
lineNumber, _ := strconv.Atoi(lineNumberStr)
|
lineNumber, _ := strconv.Atoi(lineNumberStr)
|
||||||
|
|
||||||
line := GetLineFromTemplate(templateName, lineNumber, unexpected, -1)
|
line := GetLineFromTemplate(templateName, lineNumber, unexpected, -1)
|
||||||
|
|
||||||
return "PANIC: Unable to compile templates!\nUnexpected %q in template file %s at line %d:\n\n%s", []interface{}{unexpected, filename, lineNumber, log.NewColoredValue(line, log.Reset)}
|
return "PANIC: Unable to compile templates!\nUnexpected %q in template file %s at line %d:\n\n%s", []interface{}{unexpected, filename, lineNumber, log.NewColoredValue(line, log.Reset)}
|
||||||
@ -201,14 +181,8 @@ func handleExpectedEnd(err error) (string, []interface{}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
templateName, lineNumberStr, unexpected := groups[1], groups[2], groups[3]
|
templateName, lineNumberStr, unexpected := groups[1], groups[2], groups[3]
|
||||||
|
filename := fmt.Sprintf("%s (provided by %s)", templateName, AssetFS().GetFileLayerName(templateName+".tmpl"))
|
||||||
filename, assetErr := GetAssetFilename("templates/" + templateName + ".tmpl")
|
|
||||||
if assetErr != nil {
|
|
||||||
return "", nil
|
|
||||||
}
|
|
||||||
|
|
||||||
lineNumber, _ := strconv.Atoi(lineNumberStr)
|
lineNumber, _ := strconv.Atoi(lineNumberStr)
|
||||||
|
|
||||||
line := GetLineFromTemplate(templateName, lineNumber, unexpected, -1)
|
line := GetLineFromTemplate(templateName, lineNumber, unexpected, -1)
|
||||||
|
|
||||||
return "PANIC: Unable to compile templates!\nMissing end with unexpected %q in template file %s at line %d:\n\n%s", []interface{}{unexpected, filename, lineNumber, log.NewColoredValue(line, log.Reset)}
|
return "PANIC: Unable to compile templates!\nMissing end with unexpected %q in template file %s at line %d:\n\n%s", []interface{}{unexpected, filename, lineNumber, log.NewColoredValue(line, log.Reset)}
|
||||||
@ -218,7 +192,7 @@ const dashSeparator = "---------------------------------------------------------
|
|||||||
|
|
||||||
// GetLineFromTemplate returns a line from a template with some context
|
// GetLineFromTemplate returns a line from a template with some context
|
||||||
func GetLineFromTemplate(templateName string, targetLineNum int, target string, position int) string {
|
func GetLineFromTemplate(templateName string, targetLineNum int, target string, position int) string {
|
||||||
bs, err := GetAsset("templates/" + templateName + ".tmpl")
|
bs, err := AssetFS().ReadFile(templateName + ".tmpl")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Sprintf("(unable to read template file: %v)", err)
|
return fmt.Sprintf("(unable to read template file: %v)", err)
|
||||||
}
|
}
|
||||||
|
@ -6,15 +6,12 @@ package templates
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"html/template"
|
"html/template"
|
||||||
"io/fs"
|
|
||||||
"os"
|
|
||||||
"strings"
|
"strings"
|
||||||
texttmpl "text/template"
|
texttmpl "text/template"
|
||||||
|
|
||||||
"code.gitea.io/gitea/modules/base"
|
"code.gitea.io/gitea/modules/base"
|
||||||
"code.gitea.io/gitea/modules/log"
|
"code.gitea.io/gitea/modules/log"
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
"code.gitea.io/gitea/modules/watcher"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// mailSubjectTextFuncMap returns functions for injecting to text templates, it's only used for mail subject
|
// mailSubjectTextFuncMap returns functions for injecting to text templates, it's only used for mail subject
|
||||||
@ -62,54 +59,23 @@ func Mailer(ctx context.Context) (*texttmpl.Template, *template.Template) {
|
|||||||
bodyTemplates.Funcs(funcs)
|
bodyTemplates.Funcs(funcs)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
assetFS := AssetFS()
|
||||||
refreshTemplates := func() {
|
refreshTemplates := func() {
|
||||||
for _, assetPath := range BuiltinAssetNames() {
|
assetPaths, err := ListMailTemplateAssetNames(assetFS)
|
||||||
if !strings.HasPrefix(assetPath, "mail/") {
|
if err != nil {
|
||||||
|
log.Error("Failed to list mail templates: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, assetPath := range assetPaths {
|
||||||
|
content, layerName, err := assetFS.ReadLayeredFile(assetPath)
|
||||||
|
if err != nil {
|
||||||
|
log.Warn("Failed to read mail template %s by %s: %v", assetPath, layerName, err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
tmplName := strings.TrimPrefix(strings.TrimSuffix(assetPath, ".tmpl"), "mail/")
|
||||||
if !strings.HasSuffix(assetPath, ".tmpl") {
|
log.Trace("Adding mail template %s: %s by %s", tmplName, assetPath, layerName)
|
||||||
continue
|
buildSubjectBodyTemplate(subjectTemplates, bodyTemplates, tmplName, content)
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -118,10 +84,7 @@ func Mailer(ctx context.Context) (*texttmpl.Template, *template.Template) {
|
|||||||
if !setting.IsProd {
|
if !setting.IsProd {
|
||||||
// Now subjectTemplates and bodyTemplates are both synchronized
|
// Now subjectTemplates and bodyTemplates are both synchronized
|
||||||
// thus it is safe to call refresh from a different goroutine
|
// thus it is safe to call refresh from a different goroutine
|
||||||
watcher.CreateWatcher(ctx, "Mailer Templates", &watcher.CreateWatcherOpts{
|
go assetFS.WatchLocalChanges(ctx, refreshTemplates)
|
||||||
PathsCallback: walkMailerTemplates,
|
|
||||||
BetweenCallback: refreshTemplates,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return subjectTemplates, bodyTemplates
|
return subjectTemplates, bodyTemplates
|
||||||
|
@ -6,114 +6,17 @@
|
|||||||
package templates
|
package templates
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"html/template"
|
|
||||||
"io"
|
|
||||||
"io/fs"
|
|
||||||
"os"
|
|
||||||
"path"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
texttmpl "text/template"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/assetfs"
|
||||||
"code.gitea.io/gitea/modules/timeutil"
|
"code.gitea.io/gitea/modules/timeutil"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
|
||||||
subjectTemplates = texttmpl.New("")
|
|
||||||
bodyTemplates = template.New("")
|
|
||||||
)
|
|
||||||
|
|
||||||
// GlobalModTime provide a global mod time for embedded asset files
|
// GlobalModTime provide a global mod time for embedded asset files
|
||||||
func GlobalModTime(filename string) time.Time {
|
func GlobalModTime(filename string) time.Time {
|
||||||
return timeutil.GetExecutableModTime()
|
return timeutil.GetExecutableModTime()
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetAssetFilename returns the filename of the provided asset
|
func BuiltinAssets() *assetfs.Layer {
|
||||||
func GetAssetFilename(name string) (string, error) {
|
return assetfs.Bindata("builtin(bindata)", Assets)
|
||||||
filename := filepath.Join(setting.CustomPath, name)
|
|
||||||
_, err := os.Stat(filename)
|
|
||||||
if err != nil && !os.IsNotExist(err) {
|
|
||||||
return name, err
|
|
||||||
} else if err == nil {
|
|
||||||
return filename, nil
|
|
||||||
}
|
|
||||||
return "(builtin) " + name, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetAsset get a special asset, only for chi
|
|
||||||
func GetAsset(name string) ([]byte, error) {
|
|
||||||
bs, err := os.ReadFile(filepath.Join(setting.CustomPath, name))
|
|
||||||
if err != nil && !os.IsNotExist(err) {
|
|
||||||
return nil, err
|
|
||||||
} else if err == nil {
|
|
||||||
return bs, nil
|
|
||||||
}
|
|
||||||
return BuiltinAsset(strings.TrimPrefix(name, "templates/"))
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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 := getDirTemplateAssetNames(customDir)
|
|
||||||
return append(tmpls, customTmpls...)
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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
|
|
||||||
}
|
|
||||||
defer f.Close()
|
|
||||||
return io.ReadAll(f)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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 {
|
|
||||||
results = append(results, k[1:])
|
|
||||||
}
|
|
||||||
return results
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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 {
|
|
||||||
defer f.Close()
|
|
||||||
if fi, err := f.Stat(); err != nil {
|
|
||||||
return false, err
|
|
||||||
} else {
|
|
||||||
return fi.IsDir(), nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -13,7 +13,6 @@ import (
|
|||||||
"code.gitea.io/gitea/modules/options"
|
"code.gitea.io/gitea/modules/options"
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
"code.gitea.io/gitea/modules/translation/i18n"
|
"code.gitea.io/gitea/modules/translation/i18n"
|
||||||
"code.gitea.io/gitea/modules/watcher"
|
|
||||||
|
|
||||||
"golang.org/x/text/language"
|
"golang.org/x/text/language"
|
||||||
)
|
)
|
||||||
@ -58,7 +57,7 @@ func InitLocales(ctx context.Context) {
|
|||||||
|
|
||||||
refreshLocales := func() {
|
refreshLocales := func() {
|
||||||
i18n.ResetDefaultLocales()
|
i18n.ResetDefaultLocales()
|
||||||
localeNames, err := options.Dir("locale")
|
localeNames, err := options.AssetFS().ListFiles("locale", true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal("Failed to list locale files: %v", err)
|
log.Fatal("Failed to list locale files: %v", err)
|
||||||
}
|
}
|
||||||
@ -118,13 +117,10 @@ func InitLocales(ctx context.Context) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
if !setting.IsProd {
|
if !setting.IsProd {
|
||||||
watcher.CreateWatcher(ctx, "Locales", &watcher.CreateWatcherOpts{
|
go options.AssetFS().WatchLocalChanges(ctx, func() {
|
||||||
PathsCallback: options.WalkLocales,
|
|
||||||
BetweenCallback: func() {
|
|
||||||
lock.Lock()
|
lock.Lock()
|
||||||
defer lock.Unlock()
|
defer lock.Unlock()
|
||||||
refreshLocales()
|
refreshLocales()
|
||||||
},
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -74,29 +74,28 @@ const pathSeparator = string(os.PathSeparator)
|
|||||||
//
|
//
|
||||||
// {`/foo`, ``, `bar`} => `/foo/bar`
|
// {`/foo`, ``, `bar`} => `/foo/bar`
|
||||||
// {`/foo`, `..`, `bar`} => `/foo/bar`
|
// {`/foo`, `..`, `bar`} => `/foo/bar`
|
||||||
func FilePathJoinAbs(elem ...string) string {
|
func FilePathJoinAbs(base string, sub ...string) string {
|
||||||
elems := make([]string, len(elem))
|
elems := make([]string, 1, len(sub)+1)
|
||||||
|
|
||||||
// POISX filesystem can have `\` in file names. Windows: `\` and `/` are both used for path separators
|
// POSIX filesystem can have `\` in file names. Windows: `\` and `/` are both used for path separators
|
||||||
// to keep the behavior consistent, we do not allow `\` in file names, replace all `\` with `/`
|
// to keep the behavior consistent, we do not allow `\` in file names, replace all `\` with `/`
|
||||||
if isOSWindows() {
|
if isOSWindows() {
|
||||||
elems[0] = filepath.Clean(elem[0])
|
elems[0] = filepath.Clean(base)
|
||||||
} else {
|
} else {
|
||||||
elems[0] = filepath.Clean(strings.ReplaceAll(elem[0], "\\", pathSeparator))
|
elems[0] = filepath.Clean(strings.ReplaceAll(base, "\\", pathSeparator))
|
||||||
}
|
}
|
||||||
if !filepath.IsAbs(elems[0]) {
|
if !filepath.IsAbs(elems[0]) {
|
||||||
// This shouldn't happen. If there is really necessary to pass in relative path, return the full path with filepath.Abs() instead
|
// This shouldn't happen. If there is really necessary to pass in relative path, return the full path with filepath.Abs() instead
|
||||||
panic(fmt.Sprintf("FilePathJoinAbs: %q (for path %v) is not absolute, do not guess a relative path based on current working directory", elems[0], elems))
|
panic(fmt.Sprintf("FilePathJoinAbs: %q (for path %v) is not absolute, do not guess a relative path based on current working directory", elems[0], elems))
|
||||||
}
|
}
|
||||||
|
for _, s := range sub {
|
||||||
for i := 1; i < len(elem); i++ {
|
if s == "" {
|
||||||
if elem[i] == "" {
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if isOSWindows() {
|
if isOSWindows() {
|
||||||
elems[i] = filepath.Clean(pathSeparator + elem[i])
|
elems = append(elems, filepath.Clean(pathSeparator+s))
|
||||||
} else {
|
} else {
|
||||||
elems[i] = filepath.Clean(pathSeparator + strings.ReplaceAll(elem[i], "\\", pathSeparator))
|
elems = append(elems, filepath.Clean(pathSeparator+strings.ReplaceAll(s, "\\", pathSeparator)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// the elems[0] must be an absolute path, just join them together
|
// the elems[0] must be an absolute path, just join them together
|
||||||
|
@ -207,6 +207,6 @@ func TestCleanPath(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
for _, c := range cases {
|
for _, c := range cases {
|
||||||
assert.Equal(t, c.expected, FilePathJoinAbs(c.elems...), "case: %v", c.elems)
|
assert.Equal(t, c.expected, FilePathJoinAbs(c.elems[0], c.elems[1:]...), "case: %v", c.elems)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,7 @@
|
|||||||
package util
|
package util
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -18,3 +19,30 @@ func StopTimer(t *time.Timer) bool {
|
|||||||
}
|
}
|
||||||
return stopped
|
return stopped
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Debounce(d time.Duration) func(f func()) {
|
||||||
|
type debouncer struct {
|
||||||
|
mu sync.Mutex
|
||||||
|
t *time.Timer
|
||||||
|
}
|
||||||
|
db := &debouncer{}
|
||||||
|
|
||||||
|
return func(f func()) {
|
||||||
|
db.mu.Lock()
|
||||||
|
defer db.mu.Unlock()
|
||||||
|
|
||||||
|
if db.t != nil {
|
||||||
|
db.t.Stop()
|
||||||
|
}
|
||||||
|
var trigger *time.Timer
|
||||||
|
trigger = time.AfterFunc(d, func() {
|
||||||
|
db.mu.Lock()
|
||||||
|
defer db.mu.Unlock()
|
||||||
|
if trigger == db.t {
|
||||||
|
f()
|
||||||
|
db.t = nil
|
||||||
|
}
|
||||||
|
})
|
||||||
|
db.t = trigger
|
||||||
|
}
|
||||||
|
}
|
||||||
|
30
modules/util/timer_test.go
Normal file
30
modules/util/timer_test.go
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package util
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync/atomic"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestDebounce(t *testing.T) {
|
||||||
|
var c int64
|
||||||
|
d := Debounce(50 * time.Millisecond)
|
||||||
|
d(func() { atomic.AddInt64(&c, 1) })
|
||||||
|
assert.EqualValues(t, 0, atomic.LoadInt64(&c))
|
||||||
|
d(func() { atomic.AddInt64(&c, 1) })
|
||||||
|
d(func() { atomic.AddInt64(&c, 1) })
|
||||||
|
time.Sleep(100 * time.Millisecond)
|
||||||
|
assert.EqualValues(t, 1, atomic.LoadInt64(&c))
|
||||||
|
d(func() { atomic.AddInt64(&c, 1) })
|
||||||
|
assert.EqualValues(t, 1, atomic.LoadInt64(&c))
|
||||||
|
d(func() { atomic.AddInt64(&c, 1) })
|
||||||
|
d(func() { atomic.AddInt64(&c, 1) })
|
||||||
|
d(func() { atomic.AddInt64(&c, 1) })
|
||||||
|
time.Sleep(100 * time.Millisecond)
|
||||||
|
assert.EqualValues(t, 2, atomic.LoadInt64(&c))
|
||||||
|
}
|
@ -1,114 +0,0 @@
|
|||||||
// Copyright 2022 The Gitea Authors. All rights reserved.
|
|
||||||
// SPDX-License-Identifier: MIT
|
|
||||||
|
|
||||||
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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -71,13 +71,6 @@ func mustInitCtx(ctx context.Context, fn func(ctx context.Context) error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// InitGitServices init new services for git, this is also called in `contrib/pr/checkout.go`
|
|
||||||
func InitGitServices() {
|
|
||||||
setting.LoadSettings()
|
|
||||||
mustInit(storage.Init)
|
|
||||||
mustInit(repo_service.Init)
|
|
||||||
}
|
|
||||||
|
|
||||||
func syncAppConfForGit(ctx context.Context) error {
|
func syncAppConfForGit(ctx context.Context) error {
|
||||||
runtimeState := new(system.RuntimeState)
|
runtimeState := new(system.RuntimeState)
|
||||||
if err := system.AppState.Get(runtimeState); err != nil {
|
if err := system.AppState.Get(runtimeState); err != nil {
|
||||||
@ -172,7 +165,7 @@ func GlobalInitInstalled(ctx context.Context) {
|
|||||||
mustInit(ssh.Init)
|
mustInit(ssh.Init)
|
||||||
|
|
||||||
auth.Init()
|
auth.Init()
|
||||||
svg.Init()
|
mustInit(svg.Init)
|
||||||
|
|
||||||
actions_service.Init()
|
actions_service.Init()
|
||||||
|
|
||||||
|
@ -8,7 +8,6 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"html"
|
"html"
|
||||||
"net/http"
|
"net/http"
|
||||||
"path"
|
|
||||||
|
|
||||||
"code.gitea.io/gitea/modules/httpcache"
|
"code.gitea.io/gitea/modules/httpcache"
|
||||||
"code.gitea.io/gitea/modules/log"
|
"code.gitea.io/gitea/modules/log"
|
||||||
@ -89,10 +88,7 @@ func Routes(ctx goctx.Context) *web.Route {
|
|||||||
r.Use(middle)
|
r.Use(middle)
|
||||||
}
|
}
|
||||||
|
|
||||||
r.Use(web.WrapWithPrefix(public.AssetsURLPathPrefix, public.AssetsHandlerFunc(&public.Options{
|
r.Use(web.WrapWithPrefix("/assets/", public.AssetsHandlerFunc("/assets/"), "AssetsHandler"))
|
||||||
Directory: path.Join(setting.StaticRootPath, "public"),
|
|
||||||
Prefix: public.AssetsURLPathPrefix,
|
|
||||||
}), "InstallAssetsHandler"))
|
|
||||||
|
|
||||||
r.Use(session.Sessioner(session.Options{
|
r.Use(session.Sessioner(session.Options{
|
||||||
Provider: setting.SessionConfig.Provider,
|
Provider: setting.SessionConfig.Provider,
|
||||||
|
@ -30,7 +30,7 @@ func PreloadSettings(ctx context.Context) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setting.LoadSettingsForInstall()
|
setting.LoadSettingsForInstall()
|
||||||
svg.Init()
|
_ = svg.Init()
|
||||||
}
|
}
|
||||||
|
|
||||||
return !setting.InstallLock
|
return !setting.InstallLock
|
||||||
@ -47,6 +47,5 @@ func reloadSettings(ctx context.Context) {
|
|||||||
} else {
|
} else {
|
||||||
log.Fatal("ORM engine initialization failed: %v", err)
|
log.Fatal("ORM engine initialization failed: %v", err)
|
||||||
}
|
}
|
||||||
svg.Init()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -15,17 +15,18 @@ import (
|
|||||||
|
|
||||||
// List all devtest templates, they will be used for e2e tests for the UI components
|
// List all devtest templates, they will be used for e2e tests for the UI components
|
||||||
func List(ctx *context.Context) {
|
func List(ctx *context.Context) {
|
||||||
templateNames := templates.GetTemplateAssetNames()
|
templateNames, err := templates.AssetFS().ListFiles("devtest", true)
|
||||||
|
if err != nil {
|
||||||
|
ctx.ServerError("AssetFS().ListFiles", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
var subNames []string
|
var subNames []string
|
||||||
const prefix = "templates/devtest/"
|
|
||||||
for _, tmplName := range templateNames {
|
for _, tmplName := range templateNames {
|
||||||
if strings.HasPrefix(tmplName, prefix) {
|
subName := strings.TrimSuffix(tmplName, ".tmpl")
|
||||||
subName := strings.TrimSuffix(strings.TrimPrefix(tmplName, prefix), ".tmpl")
|
|
||||||
if subName != "list" {
|
if subName != "list" {
|
||||||
subNames = append(subNames, subName)
|
subNames = append(subNames, subName)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
ctx.Data["SubNames"] = subNames
|
ctx.Data["SubNames"] = subNames
|
||||||
ctx.HTML(http.StatusOK, "devtest/list")
|
ctx.HTML(http.StatusOK, "devtest/list")
|
||||||
}
|
}
|
||||||
|
@ -103,11 +103,7 @@ func buildAuthGroup() *auth_service.Group {
|
|||||||
func Routes(ctx gocontext.Context) *web.Route {
|
func Routes(ctx gocontext.Context) *web.Route {
|
||||||
routes := web.NewRoute()
|
routes := web.NewRoute()
|
||||||
|
|
||||||
routes.Use(web.WrapWithPrefix(public.AssetsURLPathPrefix, public.AssetsHandlerFunc(&public.Options{
|
routes.Use(web.WrapWithPrefix("/assets/", web.Wrap(CorsHandler(), public.AssetsHandlerFunc("/assets/")), "AssetsHandler"))
|
||||||
Directory: path.Join(setting.StaticRootPath, "public"),
|
|
||||||
Prefix: public.AssetsURLPathPrefix,
|
|
||||||
CorsHandler: CorsHandler(),
|
|
||||||
}), "AssetsHandler"))
|
|
||||||
|
|
||||||
sessioner := session.Sessioner(session.Options{
|
sessioner := session.Sessioner(session.Options{
|
||||||
Provider: setting.SessionConfig.Provider,
|
Provider: setting.SessionConfig.Provider,
|
||||||
|
Loading…
Reference in New Issue
Block a user