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)