summaryrefslogtreecommitdiffstats
path: root/modules/public
diff options
context:
space:
mode:
authorwxiaoguang <wxiaoguang@gmail.com>2023-04-12 18:16:45 +0800
committerGitHub <noreply@github.com>2023-04-12 18:16:45 +0800
commit50a72e7a83a16d183a264e969a73cdbc7fb808f4 (patch)
tree013456110621c36edb3fa0d1bb77906ba8d4e013 /modules/public
parent42919ccb7cd32ab67d0878baf2bac6cd007899a8 (diff)
downloadgitea-50a72e7a83a16d183a264e969a73cdbc7fb808f4.tar.gz
gitea-50a72e7a83a16d183a264e969a73cdbc7fb808f4.zip
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>
Diffstat (limited to 'modules/public')
-rw-r--r--modules/public/public.go92
-rw-r--r--modules/public/serve_dynamic.go15
-rw-r--r--modules/public/serve_static.go66
3 files changed, 58 insertions, 115 deletions
diff --git a/modules/public/public.go b/modules/public/public.go
index 2c96cf9e76..0c0e6dc1cc 100644
--- a/modules/public/public.go
+++ b/modules/public/public.go
@@ -4,11 +4,15 @@
package public
import (
+ "bytes"
+ "io"
"net/http"
"os"
- "path/filepath"
+ "path"
"strings"
+ "time"
+ "code.gitea.io/gitea/modules/assetfs"
"code.gitea.io/gitea/modules/container"
"code.gitea.io/gitea/modules/httpcache"
"code.gitea.io/gitea/modules/log"
@@ -16,55 +20,31 @@ import (
"code.gitea.io/gitea/modules/util"
)
-// Options represents the available options to configure the handler.
-type Options struct {
- Directory string
- Prefix string
- CorsHandler func(http.Handler) http.Handler
+func CustomAssets() *assetfs.Layer {
+ return assetfs.Local("custom", setting.CustomPath, "public")
}
-// AssetsURLPathPrefix is the path prefix for static asset files
-const AssetsURLPathPrefix = "/assets/"
+func AssetFS() *assetfs.LayeredFS {
+ return assetfs.Layered(CustomAssets(), BuiltinAssets())
+}
// AssetsHandlerFunc implements the static handler for serving custom or original assets.
-func AssetsHandlerFunc(opts *Options) http.HandlerFunc {
- custPath := filepath.Join(setting.CustomPath, "public")
- if !filepath.IsAbs(custPath) {
- 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 += "/"
- }
-
+func AssetsHandlerFunc(prefix string) http.HandlerFunc {
+ assetFS := AssetFS()
+ prefix = strings.TrimSuffix(prefix, "/") + "/"
return func(resp http.ResponseWriter, req *http.Request) {
- if req.Method != "GET" && req.Method != "HEAD" {
- resp.WriteHeader(http.StatusNotFound)
+ subPath := req.URL.Path
+ if !strings.HasPrefix(subPath, prefix) {
return
}
+ subPath = strings.TrimPrefix(subPath, prefix)
- if opts.CorsHandler != nil {
- 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) {
+ if req.Method != "GET" && req.Method != "HEAD" {
+ resp.WriteHeader(http.StatusNotFound)
return
}
- // internal files
- if opts.handle(resp, req, fileSystem(opts.Directory), file) {
+ if handleRequest(resp, req, assetFS, subPath) {
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.
// See the comments of detectWellKnownMimeType
func setWellKnownContentType(w http.ResponseWriter, file string) {
- mimeType := detectWellKnownMimeType(filepath.Ext(file))
+ mimeType := detectWellKnownMimeType(path.Ext(file))
if 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
f, err := fs.Open(util.PathJoinRelX(file))
if err != nil {
@@ -121,8 +101,34 @@ func (opts *Options) handle(w http.ResponseWriter, req *http.Request, fs http.Fi
return true
}
- setWellKnownContentType(w, file)
-
serveContent(w, req, fi, fi.ModTime(), f)
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
+}
diff --git a/modules/public/serve_dynamic.go b/modules/public/serve_dynamic.go
index cd74ee5743..a668b17c34 100644
--- a/modules/public/serve_dynamic.go
+++ b/modules/public/serve_dynamic.go
@@ -6,17 +6,10 @@
package public
import (
- "io"
- "net/http"
- "os"
- "time"
+ "code.gitea.io/gitea/modules/assetfs"
+ "code.gitea.io/gitea/modules/setting"
)
-func fileSystem(dir string) http.FileSystem {
- return http.Dir(dir)
-}
-
-// 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)
+func BuiltinAssets() *assetfs.Layer {
+ return assetfs.Local("builtin(static)", setting.StaticRootPath, "public")
}
diff --git a/modules/public/serve_static.go b/modules/public/serve_static.go
index e85ca79253..e79085021e 100644
--- a/modules/public/serve_static.go
+++ b/modules/public/serve_static.go
@@ -6,75 +6,19 @@
package public
import (
- "bytes"
- "io"
- "net/http"
- "os"
- "path/filepath"
"time"
+ "code.gitea.io/gitea/modules/assetfs"
"code.gitea.io/gitea/modules/timeutil"
)
+var _ GzipBytesProvider = (*vfsgen۰CompressedFileInfo)(nil)
+
// GlobalModTime provide a global mod time for embedded asset files
func GlobalModTime(filename string) time.Time {
return timeutil.GetExecutableModTime()
}
-func fileSystem(dir string) http.FileSystem {
- return 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
+func BuiltinAssets() *assetfs.Layer {
+ return assetfs.Bindata("builtin(bindata)", Assets)
}