1
1
mirror of https://github.com/go-gitea/gitea synced 2025-07-22 18:28:37 +00:00

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:
wxiaoguang
2023-04-12 18:16:45 +08:00
committed by GitHub
parent 42919ccb7c
commit 50a72e7a83
36 changed files with 689 additions and 1055 deletions

View File

@@ -4,131 +4,39 @@
package options
import (
"fmt"
"io/fs"
"os"
"path/filepath"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/assetfs"
"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.
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.
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.
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.
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.
func Labels(name string) ([]byte, error) {
return fileFromOptionsDir("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
return AssetFS().ReadFile("label", name)
}

View File

@@ -6,29 +6,10 @@
package options
import (
"code.gitea.io/gitea/modules/assetfs"
"code.gitea.io/gitea/modules/setting"
)
// Dir returns all files from static or custom directory.
func Dir(name string) ([]string, error) {
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
func BuiltinAssets() *assetfs.Layer {
return assetfs.Local("builtin(static)", setting.StaticRootPath, "options")
}

View File

@@ -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
}

View File

@@ -6,98 +6,9 @@
package options
import (
"fmt"
"io"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/assetfs"
)
// Dir returns all files from custom directory or bindata.
func Dir(name string) ([]string, error) {
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
func BuiltinAssets() *assetfs.Layer {
return assetfs.Bindata("builtin(bindata)", Assets)
}