diff --git a/Makefile b/Makefile index 1e742d3..300144c 100644 --- a/Makefile +++ b/Makefile @@ -54,3 +54,8 @@ lint: lint-fix: @(test -f "$(GOPATH)/bin/golangci-lint" || go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.6.0) && \ "$(GOPATH)/bin/golangci-lint" run -c .golangci.yml --fix + +EXAMPLE_DIRS := $(shell find examples -maxdepth 1 -type d ! -path "examples" | sort) +.PHONY: $(EXAMPLE_DIRS) +$(EXAMPLE_DIRS): + @go run ./cmd/local/main.go -path $@ \ No newline at end of file diff --git a/cmd/local/main.go b/cmd/local/main.go index 68e8c76..9d035a2 100644 --- a/cmd/local/main.go +++ b/cmd/local/main.go @@ -1,8 +1,20 @@ package main import ( + "context" "flag" + "fmt" + "io" + "net/http" "os" + "time" + + "github.com/pkg/errors" + "go.uber.org/zap" + "gopkg.d7z.net/gitea-pages/pkg" + "gopkg.d7z.net/gitea-pages/pkg/providers" + "gopkg.d7z.net/middleware/cache" + "gopkg.d7z.net/middleware/kv" ) var ( @@ -11,19 +23,68 @@ var ( repo = org + "." + domain path = "" - port = 8080 + port = ":8080" ) func init() { + atom := zap.NewAtomicLevel() + atom.SetLevel(zap.DebugLevel) + cfg := zap.NewProductionConfig() + cfg.Level = atom + logger, _ := cfg.Build() + zap.ReplaceGlobals(logger) dir, _ := os.Getwd() path = dir + zap.L().Info("exec workdir", zap.String("path", path)) flag.StringVar(&org, "org", org, "org") flag.StringVar(&repo, "repo", repo, "repo") flag.StringVar(&domain, "domain", domain, "domain") flag.StringVar(&path, "path", path, "path") - flag.IntVar(&port, "port", port, "port") + flag.StringVar(&port, "port", port, "port") flag.Parse() } func main() { + fmt.Printf("请访问 http://%s%s/", repo, port) + if stat, err := os.Stat(path); err != nil || !stat.IsDir() { + zap.L().Fatal("path is not a directory", zap.String("path", path)) + } + provider := providers.NewLocalProvider(map[string][]string{ + org: {repo}, + }, path) + memory, err := kv.NewMemory("") + if err != nil { + zap.L().Fatal("failed to init memory provider", zap.Error(err)) + } + server := pkg.NewPageServer(http.DefaultClient, + provider, domain, "gh-pages", memory, memory, 0, &nopCache{}, + func(w http.ResponseWriter, r *http.Request, err error) { + if errors.Is(err, os.ErrNotExist) { + http.Error(w, "page not found.", http.StatusNotFound) + } else if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } + }) + err = http.ListenAndServe(port, server) + if err != nil && !errors.Is(err, http.ErrServerClosed) { + zap.L().Fatal("failed to start server", zap.Error(err)) + } +} + +type nopCache struct{} + +func (n *nopCache) Child(_ string) cache.Cache { + return n +} + +func (n *nopCache) Put(_ context.Context, _ string, _ map[string]string, _ io.Reader, _ time.Duration) error { + return nil +} + +func (n *nopCache) Get(_ context.Context, _ string) (*cache.Content, error) { + return nil, os.ErrNotExist +} + +func (n *nopCache) Delete(_ context.Context, _ string) error { + return nil } diff --git a/cmd/server/main.go b/cmd/server/main.go index 25ab0da..60e0a9d 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -2,6 +2,7 @@ package main import ( "context" + "errors" "flag" "fmt" "log" @@ -36,7 +37,7 @@ func main() { log.Fatalf("fail to load config file: %v", err) } - gitea, err := providers.NewGitea(config.Auth.Server, config.Auth.Token) + gitea, err := providers.NewGitea(http.DefaultClient, config.Auth.Server, config.Auth.Token) if err != nil { log.Fatalln(err) } @@ -51,7 +52,7 @@ func main() { } defer cacheBlob.Close() backend := providers.NewProviderCache(gitea, cacheMeta, config.Cache.MetaTTL, - cacheBlob, uint64(config.Cache.BlobLimit), + cacheBlob.Child("backend"), uint64(config.Cache.BlobLimit), ) defer backend.Close() db, err := kv.NewKVFromURL(config.Database.URL) @@ -59,14 +60,19 @@ func main() { log.Fatalln(err) } defer db.Close() + cdb, ok := db.(kv.RawKV).Raw().(kv.CursorPagedKV) + if !ok { + log.Fatalln(errors.New("database not support cursor")) + } pageServer := pkg.NewPageServer( http.DefaultClient, backend, config.Domain, config.Page.DefaultBranch, - db, + cdb, cacheMeta, config.Cache.MetaTTL, + cacheBlob.Child("filter"), config.ErrorHandler, ) ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM, syscall.SIGQUIT) diff --git a/examples/HelloWorld/index.html b/examples/HelloWorld/index.html new file mode 100644 index 0000000..719c0fb --- /dev/null +++ b/examples/HelloWorld/index.html @@ -0,0 +1,13 @@ + + + + + + + qjs 验证 + + +

Hello World

+ + \ No newline at end of file diff --git a/examples/QuickJS/.pages.yaml b/examples/QuickJS/.pages.yaml new file mode 100644 index 0000000..6947309 --- /dev/null +++ b/examples/QuickJS/.pages.yaml @@ -0,0 +1,5 @@ +routes: +- path: "api/v1/**" + qjs: + exec: "index.js" + debug: true \ No newline at end of file diff --git a/examples/QuickJS/index.html b/examples/QuickJS/index.html new file mode 100644 index 0000000..686f81a --- /dev/null +++ b/examples/QuickJS/index.html @@ -0,0 +1,13 @@ + + + + + + + Document + + + + + \ No newline at end of file diff --git a/examples/QuickJS/index.js b/examples/QuickJS/index.js new file mode 100644 index 0000000..8c9502c --- /dev/null +++ b/examples/QuickJS/index.js @@ -0,0 +1,7 @@ +response.write("hello world") +console.log("hello world") +console.log(req.methaaod) +function aaa(){ + throw Error("Method not implemented") +} +aaa() \ No newline at end of file diff --git a/go.mod b/go.mod index a20fba6..77676c7 100644 --- a/go.mod +++ b/go.mod @@ -11,7 +11,7 @@ require ( github.com/pkg/errors v0.9.1 github.com/stretchr/testify v1.11.1 go.uber.org/zap v1.27.0 - gopkg.d7z.net/middleware v0.0.0-20251114092249-67753b883a45 + gopkg.d7z.net/middleware v0.0.0-20251114145539-bb74bd940f32 gopkg.in/yaml.v3 v3.0.1 ) diff --git a/go.sum b/go.sum index 3408369..0ebbc0a 100644 --- a/go.sum +++ b/go.sum @@ -195,6 +195,12 @@ gopkg.d7z.net/middleware v0.0.0-20251113064153-9f946bf959f5 h1:RwEXivoUP8qEbKxRW gopkg.d7z.net/middleware v0.0.0-20251113064153-9f946bf959f5/go.mod h1:BJ8ySXqmlBpM9B2zFJfmvYQ61XPA+G0O1VDmYomxyrM= gopkg.d7z.net/middleware v0.0.0-20251114092249-67753b883a45 h1:ujyhl4Di/z6fGOcIAqydzRQNwI13F9JD3xj8+s+rTVM= gopkg.d7z.net/middleware v0.0.0-20251114092249-67753b883a45/go.mod h1:BJ8ySXqmlBpM9B2zFJfmvYQ61XPA+G0O1VDmYomxyrM= +gopkg.d7z.net/middleware v0.0.0-20251114132513-93389190aeca h1:5RUUXCIUFBMNvCXiYNDdcEu27HeNr3mfH7MRKS1ftdo= +gopkg.d7z.net/middleware v0.0.0-20251114132513-93389190aeca/go.mod h1:/1/EuissKhUbuhUe01rcWuwpA5mt7jASb4uKVNOLtR8= +gopkg.d7z.net/middleware v0.0.0-20251114144707-95f41bfca5bc h1:mPcaskQN8j32dI59txtCAFVIKUb427bh7sXS9adv2jM= +gopkg.d7z.net/middleware v0.0.0-20251114144707-95f41bfca5bc/go.mod h1:/1/EuissKhUbuhUe01rcWuwpA5mt7jASb4uKVNOLtR8= +gopkg.d7z.net/middleware v0.0.0-20251114145539-bb74bd940f32 h1:3JvqnWFLWzAoS57vLBT1LVePO3RqR32ijM3ZyjyoqyY= +gopkg.d7z.net/middleware v0.0.0-20251114145539-bb74bd940f32/go.mod h1:/1/EuissKhUbuhUe01rcWuwpA5mt7jASb4uKVNOLtR8= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= diff --git a/pkg/core/backend.go b/pkg/core/backend.go index a322e82..a42b2c7 100644 --- a/pkg/core/backend.go +++ b/pkg/core/backend.go @@ -19,5 +19,5 @@ type Backend interface { // Branches return branch + commit id Branches(ctx context.Context, owner, repo string) (map[string]*BranchInfo, error) // Open return file or error (error) - Open(ctx context.Context, client *http.Client, owner, repo, commit, path string, headers http.Header) (*http.Response, error) + Open(ctx context.Context, owner, repo, commit, path string, headers http.Header) (*http.Response, error) } diff --git a/pkg/core/domain.go b/pkg/core/domain.go index 26b46c7..934527e 100644 --- a/pkg/core/domain.go +++ b/pkg/core/domain.go @@ -7,36 +7,31 @@ import ( "github.com/pkg/errors" "go.uber.org/zap" - "gopkg.d7z.net/middleware/kv" ) type PageDomain struct { *ServerMeta alias *DomainAlias - pageDB kv.KV baseDomain string defaultBranch string } -func NewPageDomain(meta *ServerMeta, alias *DomainAlias, pageDB kv.KV, baseDomain, defaultBranch string) *PageDomain { +func NewPageDomain(meta *ServerMeta, alias *DomainAlias, baseDomain, defaultBranch string) *PageDomain { return &PageDomain{ baseDomain: baseDomain, defaultBranch: defaultBranch, ServerMeta: meta, alias: alias, - pageDB: pageDB, } } type PageContent struct { *PageMetaContent - *PageVFS - OrgDB kv.KV - RepoDB kv.KV - Owner string - Repo string - Path string + + Owner string + Repo string + Path string } func (p *PageDomain) ParseDomainMeta(ctx context.Context, domain, path, branch string) (*PageContent, error) { @@ -87,9 +82,6 @@ func (p *PageDomain) returnMeta(ctx context.Context, owner, repo, branch string, result.PageMetaContent = meta result.Owner = owner result.Repo = repo - result.PageVFS = NewPageVFS(p.client, p.Backend, owner, repo, result.CommitID) - result.OrgDB = p.pageDB.Child("org").Child(owner) - result.RepoDB = p.pageDB.Child("repo").Child(owner).Child(repo) result.Path = strings.Join(path, "/") if err = p.alias.Bind(ctx, meta.Alias, result.Owner, result.Repo, branch); err != nil { diff --git a/pkg/core/filter.go b/pkg/core/filter.go index ca4a791..002c67e 100644 --- a/pkg/core/filter.go +++ b/pkg/core/filter.go @@ -9,8 +9,19 @@ import ( "strings" "go.uber.org/zap" + "gopkg.d7z.net/middleware/kv" + "gopkg.d7z.net/middleware/tools" ) +type FilterContext struct { + context.Context + *PageContent + *PageVFS + Cache *tools.TTLCache + OrgDB kv.CursorPagedKV + RepoDB kv.CursorPagedKV +} + type FilterParams map[string]any func (f FilterParams) String() string { @@ -33,30 +44,28 @@ type Filter struct { } func NextCallWrapper(call FilterCall, parentCall NextCall, stack Filter) NextCall { - return func(ctx context.Context, writer http.ResponseWriter, request *http.Request, metadata *PageContent) error { + return func(ctx FilterContext, writer http.ResponseWriter, request *http.Request) error { zap.L().Debug(fmt.Sprintf("call filter(%s) before", stack.Type), zap.Any("filter", stack)) - err := call(ctx, writer, request, metadata, parentCall) + err := call(ctx, writer, request, parentCall) zap.L().Debug(fmt.Sprintf("call filter(%s) after", stack.Type), zap.Any("filter", stack), zap.Error(err)) return err } } type NextCall func( - ctx context.Context, + ctx FilterContext, writer http.ResponseWriter, request *http.Request, - metadata *PageContent, ) error -var NotFountNextCall = func(ctx context.Context, writer http.ResponseWriter, request *http.Request, metadata *PageContent) error { +var NotFountNextCall = func(ctx FilterContext, writer http.ResponseWriter, request *http.Request) error { return os.ErrNotExist } type FilterCall func( - ctx context.Context, + ctx FilterContext, writer http.ResponseWriter, request *http.Request, - metadata *PageContent, next NextCall, ) error diff --git a/pkg/core/meta.go b/pkg/core/meta.go index e442ece..56a08c7 100644 --- a/pkg/core/meta.go +++ b/pkg/core/meta.go @@ -38,8 +38,7 @@ type PageMetaContent struct { IsPage bool `json:"is_page"` // 是否为 Page ErrorMsg string `json:"error"` // 错误消息 (作为 500 错误日志暴露至前端) - Alias []string `json:"alias"` // alias - + Alias []string `json:"alias"` // alias Filters []Filter `json:"filters"` // 路由消息 } @@ -127,7 +126,7 @@ func (s *ServerMeta) GetMeta(ctx context.Context, owner, repo, branch string) (* } rel := NewEmptyPageMetaContent() - vfs := NewPageVFS(s.client, s.Backend, owner, repo, info.ID) + vfs := NewPageVFS(s.Backend, owner, repo, info.ID) rel.CommitID = info.ID rel.LastModified = info.LastModified diff --git a/pkg/core/vfs.go b/pkg/core/vfs.go index f5821c5..a30d893 100644 --- a/pkg/core/vfs.go +++ b/pkg/core/vfs.go @@ -9,7 +9,6 @@ import ( type PageVFS struct { backend Backend - client *http.Client org string repo string @@ -18,14 +17,12 @@ type PageVFS struct { // todo: 限制最大文件加载大小 func NewPageVFS( - client *http.Client, backend Backend, org string, repo string, commitID string, ) *PageVFS { return &PageVFS{ - client: client, backend: backend, org: org, repo: repo, @@ -34,7 +31,7 @@ func NewPageVFS( } func (p *PageVFS) NativeOpen(ctx context.Context, path string, headers http.Header) (*http.Response, error) { - return p.backend.Open(ctx, p.client, p.org, p.repo, p.commitID, path, headers) + return p.backend.Open(ctx, p.org, p.repo, p.commitID, path, headers) } func (p *PageVFS) Exists(ctx context.Context, path string) (bool, error) { diff --git a/pkg/filters/block.go b/pkg/filters/block.go index 6b90ab8..bef006e 100644 --- a/pkg/filters/block.go +++ b/pkg/filters/block.go @@ -1,7 +1,6 @@ package filters import ( - "context" "net/http" "gopkg.d7z.net/gitea-pages/pkg/core" @@ -21,7 +20,7 @@ var FilterInstBlock core.FilterInstance = func(config core.FilterParams) (core.F if param.Message == "" { param.Message = http.StatusText(param.Code) } - return func(ctx context.Context, writer http.ResponseWriter, request *http.Request, metadata *core.PageContent, next core.NextCall) error { + return func(ctx core.FilterContext, writer http.ResponseWriter, request *http.Request, next core.NextCall) error { writer.WriteHeader(param.Code) if param.Message != "" { _, _ = writer.Write([]byte(param.Message)) diff --git a/pkg/filters/default.go b/pkg/filters/default.go index 140102a..bde67e8 100644 --- a/pkg/filters/default.go +++ b/pkg/filters/default.go @@ -1,7 +1,6 @@ package filters import ( - "context" "io" "net/http" "os" @@ -11,10 +10,10 @@ import ( ) var FilterInstDefaultNotFound core.FilterInstance = func(config core.FilterParams) (core.FilterCall, error) { - return func(ctx context.Context, writer http.ResponseWriter, request *http.Request, metadata *core.PageContent, next core.NextCall) error { - err := next(ctx, writer, request, metadata) + return func(ctx core.FilterContext, writer http.ResponseWriter, request *http.Request, next core.NextCall) error { + err := next(ctx, writer, request) if err != nil && errors.Is(err, os.ErrNotExist) { - open, err := metadata.NativeOpen(ctx, "/404.html", nil) + open, err := ctx.NativeOpen(ctx, "/404.html", nil) if open != nil { defer open.Body.Close() } diff --git a/pkg/filters/direct.go b/pkg/filters/direct.go index 52a668e..cadd6d8 100644 --- a/pkg/filters/direct.go +++ b/pkg/filters/direct.go @@ -1,7 +1,6 @@ package filters import ( - "context" "io" "mime" "net/http" @@ -23,8 +22,8 @@ var FilterInstDirect core.FilterInstance = func(config core.FilterParams) (core. return nil, err } param.Prefix = strings.Trim(param.Prefix, "/") + "/" - return func(ctx context.Context, writer http.ResponseWriter, request *http.Request, metadata *core.PageContent, next core.NextCall) error { - err := next(ctx, writer, request, metadata) + return func(ctx core.FilterContext, writer http.ResponseWriter, request *http.Request, next core.NextCall) error { + err := next(ctx, writer, request) if (err != nil && !errors.Is(err, os.ErrNotExist)) || err == nil { return err } @@ -34,10 +33,10 @@ var FilterInstDirect core.FilterInstance = func(config core.FilterParams) (core. } var resp *http.Response var path string - defaultPath := param.Prefix + strings.TrimSuffix(metadata.Path, "/") + defaultPath := param.Prefix + strings.TrimSuffix(ctx.Path, "/") for _, p := range []string{defaultPath, defaultPath + "/index.html"} { zap.L().Debug("direct fetch", zap.String("path", p)) - resp, err = metadata.NativeOpen(request.Context(), p, nil) + resp, err = ctx.NativeOpen(request.Context(), p, nil) if err != nil { if resp != nil { resp.Body.Close() diff --git a/pkg/filters/failback.go b/pkg/filters/failback.go index 3a4fb8a..31ebded 100644 --- a/pkg/filters/failback.go +++ b/pkg/filters/failback.go @@ -1,7 +1,6 @@ package filters import ( - "context" "io" "mime" "net/http" @@ -23,12 +22,12 @@ var FilterInstFailback core.FilterInstance = func(config core.FilterParams) (cor if param.Path == "" { return nil, errors.Errorf("filter failback: path is empty") } - return func(ctx context.Context, writer http.ResponseWriter, request *http.Request, metadata *core.PageContent, next core.NextCall) error { - err := next(ctx, writer, request, metadata) + return func(ctx core.FilterContext, writer http.ResponseWriter, request *http.Request, next core.NextCall) error { + err := next(ctx, writer, request) if (err != nil && !errors.Is(err, os.ErrNotExist)) || err == nil { return err } - resp, err := metadata.NativeOpen(ctx, param.Path, nil) + resp, err := ctx.NativeOpen(ctx, param.Path, nil) if resp != nil { defer resp.Body.Close() } diff --git a/pkg/filters/proxy.go b/pkg/filters/proxy.go index 03a3823..6a4c295 100644 --- a/pkg/filters/proxy.go +++ b/pkg/filters/proxy.go @@ -1,7 +1,6 @@ package filters import ( - "context" "fmt" "net" "net/http" @@ -22,8 +21,8 @@ var FilterInstProxy core.FilterInstance = func(config core.FilterParams) (core.F if err := config.Unmarshal(¶m); err != nil { return nil, err } - return func(ctx context.Context, writer http.ResponseWriter, request *http.Request, metadata *core.PageContent, next core.NextCall) error { - proxyPath := "/" + metadata.Path + return func(ctx core.FilterContext, writer http.ResponseWriter, request *http.Request, next core.NextCall) error { + proxyPath := "/" + ctx.Path targetPath := strings.TrimPrefix(proxyPath, param.Prefix) if !strings.HasPrefix(targetPath, "/") { targetPath = "/" + targetPath @@ -38,7 +37,7 @@ var FilterInstProxy core.FilterInstance = func(config core.FilterParams) (core.F request.Header.Set("X-Real-IP", host) } request.Header.Set("X-Page-IP", utils.GetRemoteIP(request)) - request.Header.Set("X-Page-Refer", fmt.Sprintf("%s/%s/%s", metadata.Owner, metadata.Repo, metadata.Path)) + request.Header.Set("X-Page-Refer", fmt.Sprintf("%s/%s/%s", ctx.Owner, ctx.Repo, ctx.Path)) request.Header.Set("X-Page-Host", request.Host) zap.L().Debug("命中反向代理", zap.Any("prefix", param.Prefix), zap.Any("target", param.Target), zap.Any("path", proxyPath), zap.Any("target", fmt.Sprintf("%s%s", u, targetPath))) diff --git a/pkg/filters/quickjs/debug.go b/pkg/filters/quickjs/debug.go index f9ceda9..49d8708 100644 --- a/pkg/filters/quickjs/debug.go +++ b/pkg/filters/quickjs/debug.go @@ -1,9 +1,12 @@ package quickjs import ( + "errors" "net/http" "strings" "time" + + "github.com/buke/quickjs-go" ) type DebugData struct { @@ -37,3 +40,96 @@ func (w *debugResponseWriter) Write(data []byte) (int, error) { func (w *debugResponseWriter) WriteHeader(statusCode int) { w.status = statusCode } + +// renderDebugPage 渲染调试页面 +func renderDebugPage(writer http.ResponseWriter, outputBuffer, logBuffer *strings.Builder, jsError error) error { + writer.Header().Set("Content-Type", "text/html; charset=utf-8") + + html := ` + + + QuickJS Debug + + + +

QuickJS Debug Output

+ +
+
执行结果
+
+
` + + // 转义输出内容 + output := outputBuffer.String() + if output == "" { + output = "(无输出)" + } + html += htmlEscape(output) + + html += `
+
+
+ +
+
控制台日志
+
+
` + + // 转义日志内容 + logs := logBuffer.String() + if logs == "" { + logs = "(无日志)" + } + html += htmlEscape(logs) + + html += `
+
+
+ +
+
执行状态
+
` + + if jsError != nil { + html += `
Message:
` + var q *quickjs.Error + if errors.As(jsError, &q) { + html += q.Message + "

" + html += `Stack:
` + q.Stack + } else { + html += jsError.Error() + } + + html += `
` + } else { + html += `
执行成功
` + } + + html += `
+
+ +` + + _, err := writer.Write([]byte(html)) + return err +} + +// htmlEscape 转义 HTML 特殊字符 +func htmlEscape(s string) string { + return strings.NewReplacer( + "&", "&", + "<", "<", + ">", ">", + `"`, """, + "'", "'", + ).Replace(s) +} diff --git a/pkg/filters/quickjs/inject.js b/pkg/filters/quickjs/inject.js new file mode 100644 index 0000000..dba97ca --- /dev/null +++ b/pkg/filters/quickjs/inject.js @@ -0,0 +1,2 @@ +const req = request; +const resp = response; \ No newline at end of file diff --git a/pkg/filters/quickjs/quickjs.go b/pkg/filters/quickjs/quickjs.go index 9c4ee7d..2bdde96 100644 --- a/pkg/filters/quickjs/quickjs.go +++ b/pkg/filters/quickjs/quickjs.go @@ -1,19 +1,21 @@ package quickjs import ( - "context" - "fmt" + "bytes" + _ "embed" "io" - "log" "net/http" + "os" "strings" - "time" "github.com/buke/quickjs-go" "github.com/pkg/errors" "gopkg.d7z.net/gitea-pages/pkg/core" ) +//go:embed inject.js +var inject string + var FilterInstQuickJS core.FilterInstance = func(config core.FilterParams) (core.FilterCall, error) { var param struct { Exec string `json:"exec"` @@ -25,8 +27,8 @@ var FilterInstQuickJS core.FilterInstance = func(config core.FilterParams) (core if param.Exec == "" { return nil, errors.Errorf("no exec specified") } - return func(ctx context.Context, writer http.ResponseWriter, request *http.Request, metadata *core.PageContent, next core.NextCall) error { - js, err := metadata.ReadString(ctx, param.Exec) + return func(ctx core.FilterContext, writer http.ResponseWriter, request *http.Request, next core.NextCall) error { + js, err := ctx.ReadString(ctx, param.Exec) if err != nil { return err } @@ -34,10 +36,29 @@ var FilterInstQuickJS core.FilterInstance = func(config core.FilterParams) (core rt := quickjs.NewRuntime() rt.SetExecuteTimeout(5) defer rt.Close() - jsCtx := rt.NewContext() defer jsCtx.Close() - + cacheKey := "qjs/" + param.Exec + var bytecode []byte + cacheData, err := ctx.Cache.Get(ctx, cacheKey) + if err != nil { + if !errors.Is(err, os.ErrNotExist) { + return err + } + if bytecode, err = jsCtx.Compile(js, + quickjs.EvalFlagCompileOnly(true), + quickjs.EvalFileName(param.Exec)); err != nil { + return err + } + if err = ctx.Cache.Put(ctx, cacheKey, map[string]string{}, bytes.NewBuffer(bytecode)); err != nil { + return err + } + } else { + defer cacheData.Close() + if bytecode, err = io.ReadAll(cacheData); err != nil { + return err + } + } // 在 debug 模式下,我们需要拦截输出 var ( outputBuffer strings.Builder @@ -46,8 +67,7 @@ var FilterInstQuickJS core.FilterInstance = func(config core.FilterParams) (core ) global := jsCtx.Globals() - global.Set("request", createRequestObject(jsCtx, request, metadata)) - + global.Set("request", createRequestObject(jsCtx, request, ctx)) // 根据是否 debug 模式创建不同的 response 对象 if param.Debug { // debug 模式下使用虚假的 writer 来捕获输出 @@ -60,8 +80,8 @@ var FilterInstQuickJS core.FilterInstance = func(config core.FilterParams) (core global.Set("response", createResponseObject(jsCtx, writer, request)) global.Set("console", createConsoleObject(jsCtx, nil)) } - - ret := jsCtx.Eval(js) + jsCtx.Eval(inject) + ret := jsCtx.EvalBytecode(bytecode) defer ret.Free() jsCtx.Loop() @@ -78,346 +98,3 @@ var FilterInstQuickJS core.FilterInstance = func(config core.FilterParams) (core return jsError }, nil } - -// renderDebugPage 渲染调试页面 -func renderDebugPage(writer http.ResponseWriter, outputBuffer, logBuffer *strings.Builder, jsError error) error { - writer.Header().Set("Content-Type", "text/html; charset=utf-8") - - html := ` - - - QuickJS Debug - - - -

QuickJS Debug Output

- -
-
执行结果
-
-
` - - // 转义输出内容 - output := outputBuffer.String() - if output == "" { - output = "(无输出)" - } - html += htmlEscape(output) - - html += `
-
-
- -
-
控制台日志
-
-
` - - // 转义日志内容 - logs := logBuffer.String() - if logs == "" { - logs = "(无日志)" - } - html += htmlEscape(logs) - - html += `
-
-
- -
-
执行状态
-
` - - if jsError != nil { - html += `
错误: ` + htmlEscape(jsError.Error()) + `
` - } else { - html += `
执行成功
` - } - - html += `
-
- -` - - _, err := writer.Write([]byte(html)) - return err -} - -// htmlEscape 转义 HTML 特殊字符 -func htmlEscape(s string) string { - return strings.NewReplacer( - "&", "&", - "<", "<", - ">", ">", - `"`, """, - "'", "'", - ).Replace(s) -} - -// createRequestObject 创建表示 HTTP 请求的 JavaScript 对象 -func createRequestObject(ctx *quickjs.Context, req *http.Request, metadata *core.PageContent) *quickjs.Value { - obj := ctx.NewObject() - // 基本属性 - obj.Set("method", ctx.NewString(req.Method)) - url := *req.URL - url.Path = metadata.Path - obj.Set("url", ctx.NewString(url.String())) - obj.Set("path", ctx.NewString(url.Path)) - obj.Set("rawPath", ctx.NewString(req.URL.Path)) - obj.Set("query", ctx.NewString(url.RawQuery)) - obj.Set("host", ctx.NewString(req.Host)) - obj.Set("remoteAddr", ctx.NewString(req.RemoteAddr)) - obj.Set("proto", ctx.NewString(req.Proto)) - obj.Set("httpVersion", ctx.NewString(req.Proto)) - - // 解析查询参数 - queryObj := ctx.NewObject() - for key, values := range url.Query() { - if len(values) > 0 { - queryObj.Set(key, ctx.NewString(values[0])) - } - } - obj.Set("query", queryObj) - - // 添加 headers - headersObj := ctx.NewObject() - for key, values := range req.Header { - if len(values) > 0 { - headersObj.Set(key, ctx.NewString(values[0])) - } - } - obj.Set("headers", headersObj) - - // 请求方法 - obj.Set("get", ctx.NewFunction(func(c *quickjs.Context, value *quickjs.Value, args []*quickjs.Value) *quickjs.Value { - if len(args) > 0 { - key := args[0].String() - return ctx.NewString(req.Header.Get(key)) - } - return ctx.NewNull() - })) - - // 读取请求体 - obj.Set("readBody", ctx.NewFunction(func(c *quickjs.Context, value *quickjs.Value, args []*quickjs.Value) *quickjs.Value { - body, err := io.ReadAll(req.Body) - if err != nil { - return ctx.NewError(err) - } - return ctx.NewString(string(body)) - })) - - return obj -} - -// createResponseObject 创建表示 HTTP 响应的 JavaScript 对象 -func createResponseObject(ctx *quickjs.Context, writer http.ResponseWriter, req *http.Request) *quickjs.Value { - obj := ctx.NewObject() - - // 响应头操作 - obj.Set("setHeader", ctx.NewFunction(func(c *quickjs.Context, value *quickjs.Value, args []*quickjs.Value) *quickjs.Value { - if len(args) >= 2 { - key := args[0].String() - value := args[1].String() - writer.Header().Set(key, value) - } - return ctx.NewNull() - })) - - obj.Set("getHeader", ctx.NewFunction(func(c *quickjs.Context, value *quickjs.Value, args []*quickjs.Value) *quickjs.Value { - if len(args) > 0 { - key := args[0].String() - return ctx.NewString(writer.Header().Get(key)) - } - return ctx.NewNull() - })) - - obj.Set("removeHeader", ctx.NewFunction(func(c *quickjs.Context, value *quickjs.Value, args []*quickjs.Value) *quickjs.Value { - if len(args) > 0 { - key := args[0].String() - writer.Header().Del(key) - } - return ctx.NewNull() - })) - - obj.Set("hasHeader", ctx.NewFunction(func(c *quickjs.Context, value *quickjs.Value, args []*quickjs.Value) *quickjs.Value { - if len(args) > 0 { - key := args[0].String() - _, exists := writer.Header()[key] - return ctx.NewBool(exists) - } - return ctx.NewBool(false) - })) - - // 状态码操作 - obj.Set("setStatus", ctx.NewFunction(func(c *quickjs.Context, value *quickjs.Value, args []*quickjs.Value) *quickjs.Value { - if len(args) > 0 { - writer.WriteHeader(int(args[0].ToInt32())) - } - return ctx.NewNull() - })) - - obj.Set("statusCode", ctx.NewFunction(func(c *quickjs.Context, value *quickjs.Value, args []*quickjs.Value) *quickjs.Value { - if len(args) > 0 { - writer.WriteHeader(int(args[0].ToInt32())) - } - return ctx.NewNull() - })) - - // 写入响应 - obj.Set("write", ctx.NewFunction(func(c *quickjs.Context, value *quickjs.Value, args []*quickjs.Value) *quickjs.Value { - if len(args) > 0 { - data := args[0].String() - _, err := writer.Write([]byte(data)) - if err != nil { - return ctx.NewError(err) - } - } - return ctx.NewNull() - })) - obj.Set("writeHead", ctx.NewFunction(func(c *quickjs.Context, value *quickjs.Value, args []*quickjs.Value) *quickjs.Value { - if len(args) >= 1 { - statusCode := int(args[0].ToInt32()) - // 处理可选的 headers 参数 - if len(args) >= 2 && args[1].IsObject() { - headersObj := args[1] - names, err := headersObj.PropertyNames() - if err != nil { - return ctx.NewError(err) - } - for _, key := range names { - writer.Header().Set(key, headersObj.Get(key).String()) - } - } - writer.WriteHeader(statusCode) - } - return ctx.NewNull() - })) - - obj.Set("end", ctx.NewFunction(func(c *quickjs.Context, value *quickjs.Value, args []*quickjs.Value) *quickjs.Value { - if len(args) > 0 { - data := args[0].String() - _, err := writer.Write([]byte(data)) - if err != nil { - return ctx.NewError(err) - } - } - // 在实际的 HTTP 处理中,我们通常不手动结束响应 - return ctx.NewNull() - })) - - // 重定向 - obj.Set("redirect", ctx.NewFunction(func(c *quickjs.Context, value *quickjs.Value, args []*quickjs.Value) *quickjs.Value { - if len(args) > 0 { - location := args[0].String() - statusCode := 302 - if len(args) > 1 { - statusCode = int(args[1].ToInt32()) - } - http.Redirect(writer, req, location, statusCode) - } - return ctx.NewNull() - })) - - // JSON 响应 - obj.Set("json", ctx.NewFunction(func(c *quickjs.Context, value *quickjs.Value, args []*quickjs.Value) *quickjs.Value { - if len(args) > 0 { - writer.Header().Set("Content-Type", "application/json") - jsonData := args[0].String() - _, err := writer.Write([]byte(jsonData)) - if err != nil { - return ctx.NewError(err) - } - } - return ctx.NewNull() - })) - - // 设置 cookie - obj.Set("setCookie", ctx.NewFunction(func(c *quickjs.Context, value *quickjs.Value, args []*quickjs.Value) *quickjs.Value { - if len(args) >= 2 { - name := args[0].String() - value := args[1].String() - - cookie := &http.Cookie{ - Name: name, - Value: value, - Path: "/", - } - - // 处理可选参数 - if len(args) >= 3 && args[2].IsObject() { - options := args[2] - - if maxAge := options.Get("maxAge"); !maxAge.IsNull() { - cookie.MaxAge = int(maxAge.ToInt32()) - } - - if expires := options.Get("expires"); !expires.IsNull() { - if expires.IsNumber() { - cookie.Expires = time.Unix(expires.ToInt64(), 0) - } - } - - if path := options.Get("path"); !path.IsNull() { - cookie.Path = path.String() - } - - if domain := options.Get("domain"); !domain.IsNull() { - cookie.Domain = domain.String() - } - - if secure := options.Get("secure"); !secure.IsNull() { - cookie.Secure = secure.ToBool() - } - - if httpOnly := options.Get("httpOnly"); !httpOnly.IsNull() { - cookie.HttpOnly = httpOnly.ToBool() - } - } - - http.SetCookie(writer, cookie) - } - return ctx.NewNull() - })) - - return obj -} - -// createConsoleObject 创建 console 对象用于日志输出 -func createConsoleObject(ctx *quickjs.Context, buf *strings.Builder) *quickjs.Value { - console := ctx.NewObject() - - logFunc := func(level string, buffer *strings.Builder) func(*quickjs.Context, *quickjs.Value, []*quickjs.Value) *quickjs.Value { - return func(q *quickjs.Context, value *quickjs.Value, args []*quickjs.Value) *quickjs.Value { - var messages []string - for _, arg := range args { - messages = append(messages, arg.String()) - } - message := fmt.Sprintf("[%s] %s", level, strings.Join(messages, " ")) - - // 总是输出到系统日志 - log.Print(message) - - // 如果有缓冲区,也写入缓冲区 - if buffer != nil { - buffer.WriteString(message + "\n") - } - return ctx.NewNull() - } - } - - console.Set("log", ctx.NewFunction(logFunc("INFO", buf))) - console.Set("info", ctx.NewFunction(logFunc("INFO", buf))) - console.Set("warn", ctx.NewFunction(logFunc("WARN", buf))) - console.Set("error", ctx.NewFunction(logFunc("ERROR", buf))) - console.Set("debug", ctx.NewFunction(logFunc("DEBUG", buf))) - return console -} diff --git a/pkg/filters/quickjs/var_console.go b/pkg/filters/quickjs/var_console.go new file mode 100644 index 0000000..33c4915 --- /dev/null +++ b/pkg/filters/quickjs/var_console.go @@ -0,0 +1,40 @@ +package quickjs + +import ( + "fmt" + "log" + "strings" + + "github.com/buke/quickjs-go" +) + +// createConsoleObject 创建 console 对象用于日志输出 +func createConsoleObject(ctx *quickjs.Context, buf *strings.Builder) *quickjs.Value { + console := ctx.NewObject() + + logFunc := func(level string, buffer *strings.Builder) func(*quickjs.Context, *quickjs.Value, []*quickjs.Value) *quickjs.Value { + return func(q *quickjs.Context, value *quickjs.Value, args []*quickjs.Value) *quickjs.Value { + var messages []string + for _, arg := range args { + messages = append(messages, arg.String()) + } + message := fmt.Sprintf("[%s] %s", level, strings.Join(messages, " ")) + + // 总是输出到系统日志 + log.Print(message) + + // 如果有缓冲区,也写入缓冲区 + if buffer != nil { + buffer.WriteString(message + "\n") + } + return ctx.NewNull() + } + } + + console.Set("log", ctx.NewFunction(logFunc("INFO", buf))) + console.Set("info", ctx.NewFunction(logFunc("INFO", buf))) + console.Set("warn", ctx.NewFunction(logFunc("WARN", buf))) + console.Set("error", ctx.NewFunction(logFunc("ERROR", buf))) + console.Set("debug", ctx.NewFunction(logFunc("DEBUG", buf))) + return console +} diff --git a/pkg/filters/quickjs/var_kv.go b/pkg/filters/quickjs/var_kv.go new file mode 100644 index 0000000..bb9dd4d --- /dev/null +++ b/pkg/filters/quickjs/var_kv.go @@ -0,0 +1 @@ +package quickjs diff --git a/pkg/filters/quickjs/var_request.go b/pkg/filters/quickjs/var_request.go new file mode 100644 index 0000000..b0a3ad0 --- /dev/null +++ b/pkg/filters/quickjs/var_request.go @@ -0,0 +1,64 @@ +package quickjs + +import ( + "io" + "net/http" + + "github.com/buke/quickjs-go" + "gopkg.d7z.net/gitea-pages/pkg/core" +) + +// createRequestObject 创建表示 HTTP 请求的 JavaScript 对象 +func createRequestObject(ctx *quickjs.Context, req *http.Request, filterCtx core.FilterContext) *quickjs.Value { + obj := ctx.NewObject() + // 基本属性 + obj.Set("method", ctx.NewString(req.Method)) + url := *req.URL + url.Path = filterCtx.Path + obj.Set("url", ctx.NewString(url.String())) + obj.Set("path", ctx.NewString(url.Path)) + obj.Set("rawPath", ctx.NewString(req.URL.Path)) + obj.Set("query", ctx.NewString(url.RawQuery)) + obj.Set("host", ctx.NewString(req.Host)) + obj.Set("remoteAddr", ctx.NewString(req.RemoteAddr)) + obj.Set("proto", ctx.NewString(req.Proto)) + obj.Set("httpVersion", ctx.NewString(req.Proto)) + + // 解析查询参数 + queryObj := ctx.NewObject() + for key, values := range url.Query() { + if len(values) > 0 { + queryObj.Set(key, ctx.NewString(values[0])) + } + } + obj.Set("query", queryObj) + + // 添加 headers + headersObj := ctx.NewObject() + for key, values := range req.Header { + if len(values) > 0 { + headersObj.Set(key, ctx.NewString(values[0])) + } + } + obj.Set("headers", headersObj) + + // 请求方法 + obj.Set("get", ctx.NewFunction(func(c *quickjs.Context, value *quickjs.Value, args []*quickjs.Value) *quickjs.Value { + if len(args) > 0 { + key := args[0].String() + return ctx.NewString(req.Header.Get(key)) + } + return ctx.NewNull() + })) + + // 读取请求体 + obj.Set("readBody", ctx.NewFunction(func(c *quickjs.Context, value *quickjs.Value, args []*quickjs.Value) *quickjs.Value { + body, err := io.ReadAll(req.Body) + if err != nil { + return ctx.NewError(err) + } + return ctx.NewString(string(body)) + })) + + return obj +} diff --git a/pkg/filters/quickjs/var_response.go b/pkg/filters/quickjs/var_response.go new file mode 100644 index 0000000..21fedb1 --- /dev/null +++ b/pkg/filters/quickjs/var_response.go @@ -0,0 +1,181 @@ +package quickjs + +import ( + "net/http" + "time" + + "github.com/buke/quickjs-go" +) + +// createResponseObject 创建表示 HTTP 响应的 JavaScript 对象 +func createResponseObject(ctx *quickjs.Context, writer http.ResponseWriter, req *http.Request) *quickjs.Value { + obj := ctx.NewObject() + + // 响应头操作 + obj.Set("setHeader", ctx.NewFunction(func(c *quickjs.Context, value *quickjs.Value, args []*quickjs.Value) *quickjs.Value { + if len(args) >= 2 { + key := args[0].String() + value := args[1].String() + writer.Header().Set(key, value) + } + return ctx.NewNull() + })) + + obj.Set("getHeader", ctx.NewFunction(func(c *quickjs.Context, value *quickjs.Value, args []*quickjs.Value) *quickjs.Value { + if len(args) > 0 { + key := args[0].String() + return ctx.NewString(writer.Header().Get(key)) + } + return ctx.NewNull() + })) + + obj.Set("removeHeader", ctx.NewFunction(func(c *quickjs.Context, value *quickjs.Value, args []*quickjs.Value) *quickjs.Value { + if len(args) > 0 { + key := args[0].String() + writer.Header().Del(key) + } + return ctx.NewNull() + })) + + obj.Set("hasHeader", ctx.NewFunction(func(c *quickjs.Context, value *quickjs.Value, args []*quickjs.Value) *quickjs.Value { + if len(args) > 0 { + key := args[0].String() + _, exists := writer.Header()[key] + return ctx.NewBool(exists) + } + return ctx.NewBool(false) + })) + + // 状态码操作 + obj.Set("setStatus", ctx.NewFunction(func(c *quickjs.Context, value *quickjs.Value, args []*quickjs.Value) *quickjs.Value { + if len(args) > 0 { + writer.WriteHeader(int(args[0].ToInt32())) + } + return ctx.NewNull() + })) + + obj.Set("statusCode", ctx.NewFunction(func(c *quickjs.Context, value *quickjs.Value, args []*quickjs.Value) *quickjs.Value { + if len(args) > 0 { + writer.WriteHeader(int(args[0].ToInt32())) + } + return ctx.NewNull() + })) + + // 写入响应 + obj.Set("write", ctx.NewFunction(func(c *quickjs.Context, value *quickjs.Value, args []*quickjs.Value) *quickjs.Value { + if len(args) > 0 { + data := args[0].String() + _, err := writer.Write([]byte(data)) + if err != nil { + return ctx.NewError(err) + } + } + return ctx.NewNull() + })) + obj.Set("writeHead", ctx.NewFunction(func(c *quickjs.Context, value *quickjs.Value, args []*quickjs.Value) *quickjs.Value { + if len(args) >= 1 { + statusCode := int(args[0].ToInt32()) + // 处理可选的 headers 参数 + if len(args) >= 2 && args[1].IsObject() { + headersObj := args[1] + names, err := headersObj.PropertyNames() + if err != nil { + return ctx.NewError(err) + } + for _, key := range names { + writer.Header().Set(key, headersObj.Get(key).String()) + } + } + writer.WriteHeader(statusCode) + } + return ctx.NewNull() + })) + + obj.Set("end", ctx.NewFunction(func(c *quickjs.Context, value *quickjs.Value, args []*quickjs.Value) *quickjs.Value { + if len(args) > 0 { + data := args[0].String() + _, err := writer.Write([]byte(data)) + if err != nil { + return ctx.NewError(err) + } + } + // 在实际的 HTTP 处理中,我们通常不手动结束响应 + return ctx.NewNull() + })) + + // 重定向 + obj.Set("redirect", ctx.NewFunction(func(c *quickjs.Context, value *quickjs.Value, args []*quickjs.Value) *quickjs.Value { + if len(args) > 0 { + location := args[0].String() + statusCode := 302 + if len(args) > 1 { + statusCode = int(args[1].ToInt32()) + } + http.Redirect(writer, req, location, statusCode) + } + return ctx.NewNull() + })) + + // JSON 响应 + obj.Set("json", ctx.NewFunction(func(c *quickjs.Context, value *quickjs.Value, args []*quickjs.Value) *quickjs.Value { + if len(args) > 0 { + writer.Header().Set("Content-Type", "application/json") + jsonData := args[0].String() + _, err := writer.Write([]byte(jsonData)) + if err != nil { + return ctx.NewError(err) + } + } + return ctx.NewNull() + })) + + // 设置 cookie + obj.Set("setCookie", ctx.NewFunction(func(c *quickjs.Context, value *quickjs.Value, args []*quickjs.Value) *quickjs.Value { + if len(args) >= 2 { + name := args[0].String() + value := args[1].String() + + cookie := &http.Cookie{ + Name: name, + Value: value, + Path: "/", + } + + // 处理可选参数 + if len(args) >= 3 && args[2].IsObject() { + options := args[2] + + if maxAge := options.Get("maxAge"); !maxAge.IsNull() { + cookie.MaxAge = int(maxAge.ToInt32()) + } + + if expires := options.Get("expires"); !expires.IsNull() { + if expires.IsNumber() { + cookie.Expires = time.Unix(expires.ToInt64(), 0) + } + } + + if path := options.Get("path"); !path.IsNull() { + cookie.Path = path.String() + } + + if domain := options.Get("domain"); !domain.IsNull() { + cookie.Domain = domain.String() + } + + if secure := options.Get("secure"); !secure.IsNull() { + cookie.Secure = secure.ToBool() + } + + if httpOnly := options.Get("httpOnly"); !httpOnly.IsNull() { + cookie.HttpOnly = httpOnly.ToBool() + } + } + + http.SetCookie(writer, cookie) + } + return ctx.NewNull() + })) + + return obj +} diff --git a/pkg/filters/redirect.go b/pkg/filters/redirect.go index 83e1a47..edb1ae4 100644 --- a/pkg/filters/redirect.go +++ b/pkg/filters/redirect.go @@ -1,7 +1,6 @@ package filters import ( - "context" "fmt" "net/http" "net/url" @@ -33,12 +32,12 @@ var FilterInstRedirect core.FilterInstance = func(config core.FilterParams) (cor if param.Code < 300 || param.Code > 399 { return nil, fmt.Errorf("invalid code: %d", param.Code) } - return func(ctx context.Context, writer http.ResponseWriter, request *http.Request, metadata *core.PageContent, next core.NextCall) error { + return func(ctx core.FilterContext, writer http.ResponseWriter, request *http.Request, next core.NextCall) error { domain := portExp.ReplaceAllString(strings.ToLower(request.Host), "") - if len(param.Targets) > 0 && !slices.Contains(metadata.Alias, domain) { + if len(param.Targets) > 0 && !slices.Contains(ctx.Alias, domain) { // 重定向到配置的地址 zap.L().Debug("redirect", zap.Any("src", request.Host), zap.Any("dst", param.Targets[0])) - path := metadata.Path + path := ctx.Path if strings.HasSuffix(path, "/index.html") || path == "index.html" { path = strings.TrimSuffix(path, "index.html") } @@ -51,6 +50,6 @@ var FilterInstRedirect core.FilterInstance = func(config core.FilterParams) (cor http.Redirect(writer, request, target.String(), param.Code) return nil } - return next(ctx, writer, request, metadata) + return next(ctx, writer, request) }, nil } diff --git a/pkg/filters/template.go b/pkg/filters/template.go index 6537606..569f029 100644 --- a/pkg/filters/template.go +++ b/pkg/filters/template.go @@ -2,7 +2,6 @@ package filters import ( "bytes" - "context" "net/http" "strings" @@ -18,8 +17,8 @@ var FilterInstTemplate core.FilterInstance = func(config core.FilterParams) (cor return nil, err } param.Prefix = strings.Trim(param.Prefix, "/") + "/" - return func(ctx context.Context, writer http.ResponseWriter, request *http.Request, metadata *core.PageContent, next core.NextCall) error { - data, err := metadata.ReadString(ctx, param.Prefix+metadata.Path) + return func(ctx core.FilterContext, writer http.ResponseWriter, request *http.Request, next core.NextCall) error { + data, err := ctx.ReadString(ctx, param.Prefix+ctx.Path) if err != nil { return err } @@ -29,7 +28,7 @@ var FilterInstTemplate core.FilterInstance = func(config core.FilterParams) (cor out := &bytes.Buffer{} parse, err := utils.NewTemplate().Funcs(map[string]any{ "load": func(path string) (any, error) { - return metadata.ReadString(ctx, param.Prefix+path) + return ctx.ReadString(ctx, param.Prefix+path) }, }).Parse(data) if err != nil { diff --git a/pkg/providers/cache.go b/pkg/providers/cache.go index a10a2c9..f8e909e 100644 --- a/pkg/providers/cache.go +++ b/pkg/providers/cache.go @@ -21,8 +21,8 @@ import ( type ProviderCache struct { parent core.Backend - cacheRepo *tools.Cache[map[string]string] - cacheBranch *tools.Cache[map[string]*core.BranchInfo] + cacheRepo *tools.KVCache[map[string]string] + cacheBranch *tools.KVCache[map[string]*core.BranchInfo] cacheBlob cache.Cache cacheBlobLimit uint64 @@ -88,10 +88,10 @@ func (c *ProviderCache) Branches(ctx context.Context, owner, repo string) (map[s return ret, err } -func (c *ProviderCache) Open(ctx context.Context, client *http.Client, owner, repo, commit, path string, headers http.Header) (*http.Response, error) { +func (c *ProviderCache) Open(ctx context.Context, owner, repo, commit, path string, headers http.Header) (*http.Response, error) { if headers != nil && headers.Get("Range") != "" { // ignore custom header - return c.parent.Open(ctx, client, owner, repo, commit, path, headers) + return c.parent.Open(ctx, owner, repo, commit, path, headers) } key := fmt.Sprintf("%s/%s/%s/%s", owner, repo, commit, path) lastCache, err := c.cacheBlob.Get(ctx, key) @@ -125,7 +125,7 @@ func (c *ProviderCache) Open(ctx context.Context, client *http.Client, owner, re Header: respHeader, }, nil } - open, err := c.parent.Open(ctx, client, owner, repo, commit, path, http.Header{}) + open, err := c.parent.Open(ctx, owner, repo, commit, path, http.Header{}) if err != nil || open == nil { if open != nil { _ = open.Body.Close() diff --git a/pkg/providers/gitea.go b/pkg/providers/gitea.go index f37ad54..db964b3 100644 --- a/pkg/providers/gitea.go +++ b/pkg/providers/gitea.go @@ -17,10 +17,11 @@ type ProviderGitea struct { BaseURL string Token string - gitea *gitea.Client + gitea *gitea.Client + client *http.Client } -func NewGitea(url, token string) (*ProviderGitea, error) { +func NewGitea(httpClient *http.Client, url, token string) (*ProviderGitea, error) { client, err := gitea.NewClient(url, gitea.SetGiteaVersion(""), gitea.SetToken(token)) if err != nil { return nil, err @@ -29,6 +30,7 @@ func NewGitea(url, token string) (*ProviderGitea, error) { BaseURL: url, Token: token, gitea: client, + client: httpClient, }, nil } @@ -103,7 +105,7 @@ func (g *ProviderGitea) Branches(_ context.Context, owner, repo string) (map[str return result, nil } -func (g *ProviderGitea) Open(ctx context.Context, client *http.Client, owner, repo, commit, path string, headers http.Header) (*http.Response, error) { +func (g *ProviderGitea) Open(ctx context.Context, owner, repo, commit, path string, headers http.Header) (*http.Response, error) { if headers == nil { headers = make(http.Header) } @@ -122,7 +124,7 @@ func (g *ProviderGitea) Open(ctx context.Context, client *http.Client, owner, re } } req.Header.Add("Authorization", "token "+g.Token) - return client.Do(req) + return g.client.Do(req) } func (g *ProviderGitea) Close() error { diff --git a/pkg/providers/local.go b/pkg/providers/local.go new file mode 100644 index 0000000..058523a --- /dev/null +++ b/pkg/providers/local.go @@ -0,0 +1,84 @@ +package providers + +import ( + "bytes" + "context" + "errors" + "io" + "mime" + "net/http" + "net/http/httptest" + "os" + "path/filepath" + "slices" + "strconv" + "time" + + "gopkg.d7z.net/gitea-pages/pkg/core" +) + +type LocalProvider struct { + graph map[string][]string + path string +} + +func NewLocalProvider( + graph map[string][]string, + path string, +) *LocalProvider { + return &LocalProvider{ + graph: graph, + path: path, + } +} + +func (l *LocalProvider) Close() error { + return nil +} + +func (l *LocalProvider) Repos(_ context.Context, owner string) (map[string]string, error) { + item, ok := l.graph[owner] + if !ok { + return nil, os.ErrNotExist + } + result := make(map[string]string) + for _, s := range item { + result[s] = "gh-pages" + } + return result, nil +} + +func (l *LocalProvider) Branches(_ context.Context, owner, repo string) (map[string]*core.BranchInfo, error) { + item, ok := l.graph[owner] + if !ok { + return nil, os.ErrNotExist + } + if !slices.Contains(item, repo) { + return nil, os.ErrNotExist + } + return map[string]*core.BranchInfo{ + "gh-pages": { + ID: "adc83b19e793491b1c6ea0fd8b46cd9f32e592fc", + LastModified: time.Now(), + }, + }, nil +} + +func (l *LocalProvider) Open(_ context.Context, _, _, _, path string, _ http.Header) (*http.Response, error) { + open, err := os.Open(filepath.Join(l.path, path)) + if err != nil { + return nil, errors.Join(err, os.ErrNotExist) + } + defer open.Close() + all, err := io.ReadAll(open) + if err != nil { + return nil, errors.Join(err, os.ErrNotExist) + } + recorder := httptest.NewRecorder() + recorder.Body = bytes.NewBuffer(all) + recorder.Header().Add("Content-Type", mime.TypeByExtension(filepath.Ext(path))) + stat, _ := open.Stat() + recorder.Header().Add("Content-Length", strconv.FormatInt(stat.Size(), 10)) + recorder.Header().Add("Last-Modified", stat.ModTime().Format(http.TimeFormat)) + return recorder.Result(), nil +} diff --git a/pkg/server.go b/pkg/server.go index 06260d8..fc71bf9 100644 --- a/pkg/server.go +++ b/pkg/server.go @@ -15,19 +15,22 @@ import ( "go.uber.org/zap" "gopkg.d7z.net/gitea-pages/pkg/core" "gopkg.d7z.net/gitea-pages/pkg/filters" + "gopkg.d7z.net/middleware/cache" "gopkg.d7z.net/middleware/kv" + "gopkg.d7z.net/middleware/tools" ) var portExp = regexp.MustCompile(`:\d+$`) type Server struct { - backend core.Backend - meta *core.PageDomain - - errorHandler func(w http.ResponseWriter, r *http.Request, err error) - + backend core.Backend + meta *core.PageDomain + db kv.CursorPagedKV filterMgr map[string]core.FilterInstance globCache *lru.Cache[string, glob.Glob] + cacheBlob cache.Cache + + errorHandler func(w http.ResponseWriter, r *http.Request, err error) } func NewPageServer( @@ -35,27 +38,26 @@ func NewPageServer( backend core.Backend, domain string, defaultBranch string, - db kv.KV, - cache kv.KV, + db kv.CursorPagedKV, + cacheMeta kv.KV, cacheTTL time.Duration, + cacheBlob cache.Cache, errorHandler func(w http.ResponseWriter, r *http.Request, err error), ) *Server { - svcMeta := core.NewServerMeta(client, backend, domain, cache, cacheTTL) - cfgDB := db.Child("config") - pageMeta := core.NewPageDomain(svcMeta, - core.NewDomainAlias(cfgDB.Child("alias")), - cfgDB.Child("pages"), - domain, defaultBranch) - c, err := lru.New[string, glob.Glob](256) + svcMeta := core.NewServerMeta(client, backend, domain, cacheMeta, cacheTTL) + pageMeta := core.NewPageDomain(svcMeta, core.NewDomainAlias(db.Child("config").Child("alias")), domain, defaultBranch) + globCache, err := lru.New[string, glob.Glob](256) if err != nil { panic(err) } return &Server{ backend: backend, meta: pageMeta, - globCache: c, + db: db, + globCache: globCache, filterMgr: filters.DefaultFilters(), errorHandler: errorHandler, + cacheBlob: cacheBlob, } } @@ -88,6 +90,16 @@ func (s *Server) Serve(writer http.ResponseWriter, request *http.Request) error if err != nil { return err } + + filterCtx := core.FilterContext{ + PageContent: meta, + Context: request.Context(), + PageVFS: core.NewPageVFS(s.backend, meta.Owner, meta.Repo, meta.CommitID), + Cache: tools.NewTTLCache(s.cacheBlob.Child("filter").Child(meta.Owner).Child(meta.Repo).Child(meta.CommitID), time.Minute), + OrgDB: s.db.Child("org").Child(meta.Owner).(kv.CursorPagedKV), + RepoDB: s.db.Child("repo").Child(meta.Owner).Child(meta.Repo).(kv.CursorPagedKV), + } + zap.L().Debug("new request", zap.Any("request path", meta.Path)) if strings.HasSuffix(meta.Path, "/") || meta.Path == "" { @@ -133,6 +145,6 @@ func (s *Server) Serve(writer http.ResponseWriter, request *http.Request) error for i, filter := range activeFiltersCall { stack = core.NextCallWrapper(filter, stack, activeFilters[i]) } - err = stack(ctx, writer, request, meta) + err = stack(filterCtx, writer, request) return err } diff --git a/tests/core/dummy.go b/tests/core/dummy.go index c89c7e4..ada602d 100644 --- a/tests/core/dummy.go +++ b/tests/core/dummy.go @@ -61,7 +61,7 @@ func (p *ProviderDummy) Branches(_ context.Context, owner, repo string) (map[str return branches, nil } -func (p *ProviderDummy) Open(_ context.Context, _ *http.Client, owner, repo, commit, path string, _ http.Header) (*http.Response, error) { +func (p *ProviderDummy) Open(_ context.Context, owner, repo, commit, path string, _ http.Header) (*http.Response, error) { open, err := os.Open(filepath.Join(p.BaseDir, owner, repo, commit, path)) if err != nil { return nil, errors.Join(err, os.ErrNotExist) diff --git a/tests/core/test.go b/tests/core/test.go index 08176a0..274f607 100644 --- a/tests/core/test.go +++ b/tests/core/test.go @@ -7,10 +7,12 @@ import ( "net/http/httptest" "os" "path/filepath" + "time" "github.com/pkg/errors" "go.uber.org/zap" "gopkg.d7z.net/gitea-pages/pkg" + "gopkg.d7z.net/middleware/cache" "gopkg.d7z.net/middleware/kv" ) @@ -34,7 +36,10 @@ func NewTestServer(domain string) *TestServer { if err != nil { zap.S().Fatal(err) } - + memoryCache, _ := cache.NewMemoryCache(cache.MemoryCacheConfig{ + MaxCapacity: 256, + CleanupInt: time.Minute, + }) memoryKV, _ := kv.NewMemory("") server := pkg.NewPageServer( http.DefaultClient, @@ -44,6 +49,7 @@ func NewTestServer(domain string) *TestServer { memoryKV, memoryKV.Child("cache"), 0, + memoryCache, func(w http.ResponseWriter, r *http.Request, err error) { if errors.Is(err, os.ErrNotExist) { http.Error(w, "page not found.", http.StatusNotFound)