diff options
author | wxiaoguang <wxiaoguang@gmail.com> | 2023-04-12 18:16:45 +0800 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-04-12 18:16:45 +0800 |
commit | 50a72e7a83a16d183a264e969a73cdbc7fb808f4 (patch) | |
tree | 013456110621c36edb3fa0d1bb77906ba8d4e013 /modules/public | |
parent | 42919ccb7cd32ab67d0878baf2bac6cd007899a8 (diff) | |
download | gitea-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.go | 92 | ||||
-rw-r--r-- | modules/public/serve_dynamic.go | 15 | ||||
-rw-r--r-- | modules/public/serve_static.go | 66 |
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) } |