aboutsummaryrefslogtreecommitdiffstats
path: root/modules/public/public.go
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/public.go
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/public.go')
-rw-r--r--modules/public/public.go92
1 files changed, 49 insertions, 43 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
+}