重构项目

This commit is contained in:
ExplodingDragon
2025-11-15 01:09:25 +08:00
parent d67dbf88ae
commit 3492dead8d
34 changed files with 715 additions and 444 deletions

View File

@@ -54,3 +54,8 @@ lint:
lint-fix: lint-fix:
@(test -f "$(GOPATH)/bin/golangci-lint" || go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.6.0) && \ @(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 "$(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 $@

View File

@@ -1,8 +1,20 @@
package main package main
import ( import (
"context"
"flag" "flag"
"fmt"
"io"
"net/http"
"os" "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 ( var (
@@ -11,19 +23,68 @@ var (
repo = org + "." + domain repo = org + "." + domain
path = "" path = ""
port = 8080 port = ":8080"
) )
func init() { func init() {
atom := zap.NewAtomicLevel()
atom.SetLevel(zap.DebugLevel)
cfg := zap.NewProductionConfig()
cfg.Level = atom
logger, _ := cfg.Build()
zap.ReplaceGlobals(logger)
dir, _ := os.Getwd() dir, _ := os.Getwd()
path = dir path = dir
zap.L().Info("exec workdir", zap.String("path", path))
flag.StringVar(&org, "org", org, "org") flag.StringVar(&org, "org", org, "org")
flag.StringVar(&repo, "repo", repo, "repo") flag.StringVar(&repo, "repo", repo, "repo")
flag.StringVar(&domain, "domain", domain, "domain") flag.StringVar(&domain, "domain", domain, "domain")
flag.StringVar(&path, "path", path, "path") flag.StringVar(&path, "path", path, "path")
flag.IntVar(&port, "port", port, "port") flag.StringVar(&port, "port", port, "port")
flag.Parse() flag.Parse()
} }
func main() { 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
} }

View File

@@ -2,6 +2,7 @@ package main
import ( import (
"context" "context"
"errors"
"flag" "flag"
"fmt" "fmt"
"log" "log"
@@ -36,7 +37,7 @@ func main() {
log.Fatalf("fail to load config file: %v", err) 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 { if err != nil {
log.Fatalln(err) log.Fatalln(err)
} }
@@ -51,7 +52,7 @@ func main() {
} }
defer cacheBlob.Close() defer cacheBlob.Close()
backend := providers.NewProviderCache(gitea, cacheMeta, config.Cache.MetaTTL, backend := providers.NewProviderCache(gitea, cacheMeta, config.Cache.MetaTTL,
cacheBlob, uint64(config.Cache.BlobLimit), cacheBlob.Child("backend"), uint64(config.Cache.BlobLimit),
) )
defer backend.Close() defer backend.Close()
db, err := kv.NewKVFromURL(config.Database.URL) db, err := kv.NewKVFromURL(config.Database.URL)
@@ -59,14 +60,19 @@ func main() {
log.Fatalln(err) log.Fatalln(err)
} }
defer db.Close() defer db.Close()
cdb, ok := db.(kv.RawKV).Raw().(kv.CursorPagedKV)
if !ok {
log.Fatalln(errors.New("database not support cursor"))
}
pageServer := pkg.NewPageServer( pageServer := pkg.NewPageServer(
http.DefaultClient, http.DefaultClient,
backend, backend,
config.Domain, config.Domain,
config.Page.DefaultBranch, config.Page.DefaultBranch,
db, cdb,
cacheMeta, cacheMeta,
config.Cache.MetaTTL, config.Cache.MetaTTL,
cacheBlob.Child("filter"),
config.ErrorHandler, config.ErrorHandler,
) )
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM, syscall.SIGQUIT) ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM, syscall.SIGQUIT)

View File

@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>qjs 验证</title>
</head>
<body>
<p>Hello World</p>
</body>
</html>

View File

@@ -0,0 +1,5 @@
routes:
- path: "api/v1/**"
qjs:
exec: "index.js"
debug: true

View File

@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
</body>
</html>

View File

@@ -0,0 +1,7 @@
response.write("hello world")
console.log("hello world")
console.log(req.methaaod)
function aaa(){
throw Error("Method not implemented")
}
aaa()

2
go.mod
View File

@@ -11,7 +11,7 @@ require (
github.com/pkg/errors v0.9.1 github.com/pkg/errors v0.9.1
github.com/stretchr/testify v1.11.1 github.com/stretchr/testify v1.11.1
go.uber.org/zap v1.27.0 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 gopkg.in/yaml.v3 v3.0.1
) )

6
go.sum
View File

@@ -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-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 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-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 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 h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=

View File

@@ -19,5 +19,5 @@ type Backend interface {
// Branches return branch + commit id // Branches return branch + commit id
Branches(ctx context.Context, owner, repo string) (map[string]*BranchInfo, error) Branches(ctx context.Context, owner, repo string) (map[string]*BranchInfo, error)
// Open return file or error (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)
} }

View File

@@ -7,36 +7,31 @@ import (
"github.com/pkg/errors" "github.com/pkg/errors"
"go.uber.org/zap" "go.uber.org/zap"
"gopkg.d7z.net/middleware/kv"
) )
type PageDomain struct { type PageDomain struct {
*ServerMeta *ServerMeta
alias *DomainAlias alias *DomainAlias
pageDB kv.KV
baseDomain string baseDomain string
defaultBranch 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{ return &PageDomain{
baseDomain: baseDomain, baseDomain: baseDomain,
defaultBranch: defaultBranch, defaultBranch: defaultBranch,
ServerMeta: meta, ServerMeta: meta,
alias: alias, alias: alias,
pageDB: pageDB,
} }
} }
type PageContent struct { type PageContent struct {
*PageMetaContent *PageMetaContent
*PageVFS
OrgDB kv.KV Owner string
RepoDB kv.KV Repo string
Owner string Path string
Repo string
Path string
} }
func (p *PageDomain) ParseDomainMeta(ctx context.Context, domain, path, branch string) (*PageContent, error) { 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.PageMetaContent = meta
result.Owner = owner result.Owner = owner
result.Repo = repo 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, "/") result.Path = strings.Join(path, "/")
if err = p.alias.Bind(ctx, meta.Alias, result.Owner, result.Repo, branch); err != nil { if err = p.alias.Bind(ctx, meta.Alias, result.Owner, result.Repo, branch); err != nil {

View File

@@ -9,8 +9,19 @@ import (
"strings" "strings"
"go.uber.org/zap" "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 type FilterParams map[string]any
func (f FilterParams) String() string { func (f FilterParams) String() string {
@@ -33,30 +44,28 @@ type Filter struct {
} }
func NextCallWrapper(call FilterCall, parentCall NextCall, stack Filter) NextCall { 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)) 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)) zap.L().Debug(fmt.Sprintf("call filter(%s) after", stack.Type), zap.Any("filter", stack), zap.Error(err))
return err return err
} }
} }
type NextCall func( type NextCall func(
ctx context.Context, ctx FilterContext,
writer http.ResponseWriter, writer http.ResponseWriter,
request *http.Request, request *http.Request,
metadata *PageContent,
) error ) 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 return os.ErrNotExist
} }
type FilterCall func( type FilterCall func(
ctx context.Context, ctx FilterContext,
writer http.ResponseWriter, writer http.ResponseWriter,
request *http.Request, request *http.Request,
metadata *PageContent,
next NextCall, next NextCall,
) error ) error

View File

@@ -38,8 +38,7 @@ type PageMetaContent struct {
IsPage bool `json:"is_page"` // 是否为 Page IsPage bool `json:"is_page"` // 是否为 Page
ErrorMsg string `json:"error"` // 错误消息 (作为 500 错误日志暴露至前端) ErrorMsg string `json:"error"` // 错误消息 (作为 500 错误日志暴露至前端)
Alias []string `json:"alias"` // alias Alias []string `json:"alias"` // alias
Filters []Filter `json:"filters"` // 路由消息 Filters []Filter `json:"filters"` // 路由消息
} }
@@ -127,7 +126,7 @@ func (s *ServerMeta) GetMeta(ctx context.Context, owner, repo, branch string) (*
} }
rel := NewEmptyPageMetaContent() 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.CommitID = info.ID
rel.LastModified = info.LastModified rel.LastModified = info.LastModified

View File

@@ -9,7 +9,6 @@ import (
type PageVFS struct { type PageVFS struct {
backend Backend backend Backend
client *http.Client
org string org string
repo string repo string
@@ -18,14 +17,12 @@ type PageVFS struct {
// todo: 限制最大文件加载大小 // todo: 限制最大文件加载大小
func NewPageVFS( func NewPageVFS(
client *http.Client,
backend Backend, backend Backend,
org string, org string,
repo string, repo string,
commitID string, commitID string,
) *PageVFS { ) *PageVFS {
return &PageVFS{ return &PageVFS{
client: client,
backend: backend, backend: backend,
org: org, org: org,
repo: repo, repo: repo,
@@ -34,7 +31,7 @@ func NewPageVFS(
} }
func (p *PageVFS) NativeOpen(ctx context.Context, path string, headers http.Header) (*http.Response, error) { 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) { func (p *PageVFS) Exists(ctx context.Context, path string) (bool, error) {

View File

@@ -1,7 +1,6 @@
package filters package filters
import ( import (
"context"
"net/http" "net/http"
"gopkg.d7z.net/gitea-pages/pkg/core" "gopkg.d7z.net/gitea-pages/pkg/core"
@@ -21,7 +20,7 @@ var FilterInstBlock core.FilterInstance = func(config core.FilterParams) (core.F
if param.Message == "" { if param.Message == "" {
param.Message = http.StatusText(param.Code) 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) writer.WriteHeader(param.Code)
if param.Message != "" { if param.Message != "" {
_, _ = writer.Write([]byte(param.Message)) _, _ = writer.Write([]byte(param.Message))

View File

@@ -1,7 +1,6 @@
package filters package filters
import ( import (
"context"
"io" "io"
"net/http" "net/http"
"os" "os"
@@ -11,10 +10,10 @@ import (
) )
var FilterInstDefaultNotFound core.FilterInstance = func(config core.FilterParams) (core.FilterCall, error) { 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 { return func(ctx core.FilterContext, writer http.ResponseWriter, request *http.Request, next core.NextCall) error {
err := next(ctx, writer, request, metadata) err := next(ctx, writer, request)
if err != nil && errors.Is(err, os.ErrNotExist) { 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 { if open != nil {
defer open.Body.Close() defer open.Body.Close()
} }

View File

@@ -1,7 +1,6 @@
package filters package filters
import ( import (
"context"
"io" "io"
"mime" "mime"
"net/http" "net/http"
@@ -23,8 +22,8 @@ var FilterInstDirect core.FilterInstance = func(config core.FilterParams) (core.
return nil, err return nil, err
} }
param.Prefix = strings.Trim(param.Prefix, "/") + "/" param.Prefix = strings.Trim(param.Prefix, "/") + "/"
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 {
err := next(ctx, writer, request, metadata) err := next(ctx, writer, request)
if (err != nil && !errors.Is(err, os.ErrNotExist)) || err == nil { if (err != nil && !errors.Is(err, os.ErrNotExist)) || err == nil {
return err return err
} }
@@ -34,10 +33,10 @@ var FilterInstDirect core.FilterInstance = func(config core.FilterParams) (core.
} }
var resp *http.Response var resp *http.Response
var path string 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"} { for _, p := range []string{defaultPath, defaultPath + "/index.html"} {
zap.L().Debug("direct fetch", zap.String("path", p)) 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 err != nil {
if resp != nil { if resp != nil {
resp.Body.Close() resp.Body.Close()

View File

@@ -1,7 +1,6 @@
package filters package filters
import ( import (
"context"
"io" "io"
"mime" "mime"
"net/http" "net/http"
@@ -23,12 +22,12 @@ var FilterInstFailback core.FilterInstance = func(config core.FilterParams) (cor
if param.Path == "" { if param.Path == "" {
return nil, errors.Errorf("filter failback: path is empty") 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 { return func(ctx core.FilterContext, writer http.ResponseWriter, request *http.Request, next core.NextCall) error {
err := next(ctx, writer, request, metadata) err := next(ctx, writer, request)
if (err != nil && !errors.Is(err, os.ErrNotExist)) || err == nil { if (err != nil && !errors.Is(err, os.ErrNotExist)) || err == nil {
return err return err
} }
resp, err := metadata.NativeOpen(ctx, param.Path, nil) resp, err := ctx.NativeOpen(ctx, param.Path, nil)
if resp != nil { if resp != nil {
defer resp.Body.Close() defer resp.Body.Close()
} }

View File

@@ -1,7 +1,6 @@
package filters package filters
import ( import (
"context"
"fmt" "fmt"
"net" "net"
"net/http" "net/http"
@@ -22,8 +21,8 @@ var FilterInstProxy core.FilterInstance = func(config core.FilterParams) (core.F
if err := config.Unmarshal(&param); err != nil { if err := config.Unmarshal(&param); err != nil {
return nil, err return nil, err
} }
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 {
proxyPath := "/" + metadata.Path proxyPath := "/" + ctx.Path
targetPath := strings.TrimPrefix(proxyPath, param.Prefix) targetPath := strings.TrimPrefix(proxyPath, param.Prefix)
if !strings.HasPrefix(targetPath, "/") { if !strings.HasPrefix(targetPath, "/") {
targetPath = "/" + 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-Real-IP", host)
} }
request.Header.Set("X-Page-IP", utils.GetRemoteIP(request)) 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) request.Header.Set("X-Page-Host", request.Host)
zap.L().Debug("命中反向代理", zap.Any("prefix", param.Prefix), zap.Any("target", param.Target), 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))) zap.Any("path", proxyPath), zap.Any("target", fmt.Sprintf("%s%s", u, targetPath)))

View File

@@ -1,9 +1,12 @@
package quickjs package quickjs
import ( import (
"errors"
"net/http" "net/http"
"strings" "strings"
"time" "time"
"github.com/buke/quickjs-go"
) )
type DebugData struct { type DebugData struct {
@@ -37,3 +40,96 @@ func (w *debugResponseWriter) Write(data []byte) (int, error) {
func (w *debugResponseWriter) WriteHeader(statusCode int) { func (w *debugResponseWriter) WriteHeader(statusCode int) {
w.status = statusCode 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 := `<!DOCTYPE html>
<html>
<head>
<title>QuickJS Debug</title>
<style>
body { font-family: Arial, sans-serif; margin: 0; padding: 20px; }
.section { margin-bottom: 30px; border: 1px solid #ddd; border-radius: 5px; }
.section-header { background: #f5f5f5; padding: 10px 15px; border-bottom: 1px solid #ddd; font-weight: bold; }
.section-content { padding: 15px; background: white; }
.output { white-space: pre-wrap; font-family: monospace; }
.log { white-space: pre-wrap; font-family: monospace; background: #f8f8f8; }
.error { color: #d00; background: #fee; padding: 10px; border-radius: 3px; }
.success { color: #080; background: #efe; padding: 10px; border-radius: 3px; }
</style>
</head>
<body>
<h1>QuickJS Debug Output</h1>
<div class="section">
<div class="section-header">执行结果</div>
<div class="section-content">
<div class="output">`
// 转义输出内容
output := outputBuffer.String()
if output == "" {
output = "(无输出)"
}
html += htmlEscape(output)
html += `</div>
</div>
</div>
<div class="section">
<div class="section-header">控制台日志</div>
<div class="section-content">
<div class="log">`
// 转义日志内容
logs := logBuffer.String()
if logs == "" {
logs = "(无日志)"
}
html += htmlEscape(logs)
html += `</div>
</div>
</div>
<div class="section">
<div class="section-header">执行状态</div>
<div class="section-content">`
if jsError != nil {
html += `<div class="error"><pre><code><strong>Message:</strong></br>`
var q *quickjs.Error
if errors.As(jsError, &q) {
html += q.Message + "</br></br>"
html += `<strong>Stack:</strong></br>` + q.Stack
} else {
html += jsError.Error()
}
html += `</pre></code></div>`
} else {
html += `<div class="success">执行成功</div>`
}
html += `</div>
</div>
</body>
</html>`
_, err := writer.Write([]byte(html))
return err
}
// htmlEscape 转义 HTML 特殊字符
func htmlEscape(s string) string {
return strings.NewReplacer(
"&", "&amp;",
"<", "&lt;",
">", "&gt;",
`"`, "&quot;",
"'", "&#39;",
).Replace(s)
}

View File

@@ -0,0 +1,2 @@
const req = request;
const resp = response;

View File

@@ -1,19 +1,21 @@
package quickjs package quickjs
import ( import (
"context" "bytes"
"fmt" _ "embed"
"io" "io"
"log"
"net/http" "net/http"
"os"
"strings" "strings"
"time"
"github.com/buke/quickjs-go" "github.com/buke/quickjs-go"
"github.com/pkg/errors" "github.com/pkg/errors"
"gopkg.d7z.net/gitea-pages/pkg/core" "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 FilterInstQuickJS core.FilterInstance = func(config core.FilterParams) (core.FilterCall, error) {
var param struct { var param struct {
Exec string `json:"exec"` Exec string `json:"exec"`
@@ -25,8 +27,8 @@ var FilterInstQuickJS core.FilterInstance = func(config core.FilterParams) (core
if param.Exec == "" { if param.Exec == "" {
return nil, errors.Errorf("no exec specified") 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 { return func(ctx core.FilterContext, writer http.ResponseWriter, request *http.Request, next core.NextCall) error {
js, err := metadata.ReadString(ctx, param.Exec) js, err := ctx.ReadString(ctx, param.Exec)
if err != nil { if err != nil {
return err return err
} }
@@ -34,10 +36,29 @@ var FilterInstQuickJS core.FilterInstance = func(config core.FilterParams) (core
rt := quickjs.NewRuntime() rt := quickjs.NewRuntime()
rt.SetExecuteTimeout(5) rt.SetExecuteTimeout(5)
defer rt.Close() defer rt.Close()
jsCtx := rt.NewContext() jsCtx := rt.NewContext()
defer jsCtx.Close() 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 模式下,我们需要拦截输出 // 在 debug 模式下,我们需要拦截输出
var ( var (
outputBuffer strings.Builder outputBuffer strings.Builder
@@ -46,8 +67,7 @@ var FilterInstQuickJS core.FilterInstance = func(config core.FilterParams) (core
) )
global := jsCtx.Globals() global := jsCtx.Globals()
global.Set("request", createRequestObject(jsCtx, request, metadata)) global.Set("request", createRequestObject(jsCtx, request, ctx))
// 根据是否 debug 模式创建不同的 response 对象 // 根据是否 debug 模式创建不同的 response 对象
if param.Debug { if param.Debug {
// debug 模式下使用虚假的 writer 来捕获输出 // debug 模式下使用虚假的 writer 来捕获输出
@@ -60,8 +80,8 @@ var FilterInstQuickJS core.FilterInstance = func(config core.FilterParams) (core
global.Set("response", createResponseObject(jsCtx, writer, request)) global.Set("response", createResponseObject(jsCtx, writer, request))
global.Set("console", createConsoleObject(jsCtx, nil)) global.Set("console", createConsoleObject(jsCtx, nil))
} }
jsCtx.Eval(inject)
ret := jsCtx.Eval(js) ret := jsCtx.EvalBytecode(bytecode)
defer ret.Free() defer ret.Free()
jsCtx.Loop() jsCtx.Loop()
@@ -78,346 +98,3 @@ var FilterInstQuickJS core.FilterInstance = func(config core.FilterParams) (core
return jsError return jsError
}, nil }, 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 := `<!DOCTYPE html>
<html>
<head>
<title>QuickJS Debug</title>
<style>
body { font-family: Arial, sans-serif; margin: 0; padding: 20px; }
.section { margin-bottom: 30px; border: 1px solid #ddd; border-radius: 5px; }
.section-header { background: #f5f5f5; padding: 10px 15px; border-bottom: 1px solid #ddd; font-weight: bold; }
.section-content { padding: 15px; background: white; }
.output { white-space: pre-wrap; font-family: monospace; }
.log { white-space: pre-wrap; font-family: monospace; background: #f8f8f8; }
.error { color: #d00; background: #fee; padding: 10px; border-radius: 3px; }
.success { color: #080; background: #efe; padding: 10px; border-radius: 3px; }
</style>
</head>
<body>
<h1>QuickJS Debug Output</h1>
<div class="section">
<div class="section-header">执行结果</div>
<div class="section-content">
<div class="output">`
// 转义输出内容
output := outputBuffer.String()
if output == "" {
output = "(无输出)"
}
html += htmlEscape(output)
html += `</div>
</div>
</div>
<div class="section">
<div class="section-header">控制台日志</div>
<div class="section-content">
<div class="log">`
// 转义日志内容
logs := logBuffer.String()
if logs == "" {
logs = "(无日志)"
}
html += htmlEscape(logs)
html += `</div>
</div>
</div>
<div class="section">
<div class="section-header">执行状态</div>
<div class="section-content">`
if jsError != nil {
html += `<div class="error"><strong>错误:</strong> ` + htmlEscape(jsError.Error()) + `</div>`
} else {
html += `<div class="success">执行成功</div>`
}
html += `</div>
</div>
</body>
</html>`
_, err := writer.Write([]byte(html))
return err
}
// htmlEscape 转义 HTML 特殊字符
func htmlEscape(s string) string {
return strings.NewReplacer(
"&", "&amp;",
"<", "&lt;",
">", "&gt;",
`"`, "&quot;",
"'", "&#39;",
).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
}

View File

@@ -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
}

View File

@@ -0,0 +1 @@
package quickjs

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -1,7 +1,6 @@
package filters package filters
import ( import (
"context"
"fmt" "fmt"
"net/http" "net/http"
"net/url" "net/url"
@@ -33,12 +32,12 @@ var FilterInstRedirect core.FilterInstance = func(config core.FilterParams) (cor
if param.Code < 300 || param.Code > 399 { if param.Code < 300 || param.Code > 399 {
return nil, fmt.Errorf("invalid code: %d", param.Code) 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), "") 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])) 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" { if strings.HasSuffix(path, "/index.html") || path == "index.html" {
path = strings.TrimSuffix(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) http.Redirect(writer, request, target.String(), param.Code)
return nil return nil
} }
return next(ctx, writer, request, metadata) return next(ctx, writer, request)
}, nil }, nil
} }

View File

@@ -2,7 +2,6 @@ package filters
import ( import (
"bytes" "bytes"
"context"
"net/http" "net/http"
"strings" "strings"
@@ -18,8 +17,8 @@ var FilterInstTemplate core.FilterInstance = func(config core.FilterParams) (cor
return nil, err return nil, err
} }
param.Prefix = strings.Trim(param.Prefix, "/") + "/" param.Prefix = strings.Trim(param.Prefix, "/") + "/"
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 {
data, err := metadata.ReadString(ctx, param.Prefix+metadata.Path) data, err := ctx.ReadString(ctx, param.Prefix+ctx.Path)
if err != nil { if err != nil {
return err return err
} }
@@ -29,7 +28,7 @@ var FilterInstTemplate core.FilterInstance = func(config core.FilterParams) (cor
out := &bytes.Buffer{} out := &bytes.Buffer{}
parse, err := utils.NewTemplate().Funcs(map[string]any{ parse, err := utils.NewTemplate().Funcs(map[string]any{
"load": func(path string) (any, error) { "load": func(path string) (any, error) {
return metadata.ReadString(ctx, param.Prefix+path) return ctx.ReadString(ctx, param.Prefix+path)
}, },
}).Parse(data) }).Parse(data)
if err != nil { if err != nil {

View File

@@ -21,8 +21,8 @@ import (
type ProviderCache struct { type ProviderCache struct {
parent core.Backend parent core.Backend
cacheRepo *tools.Cache[map[string]string] cacheRepo *tools.KVCache[map[string]string]
cacheBranch *tools.Cache[map[string]*core.BranchInfo] cacheBranch *tools.KVCache[map[string]*core.BranchInfo]
cacheBlob cache.Cache cacheBlob cache.Cache
cacheBlobLimit uint64 cacheBlobLimit uint64
@@ -88,10 +88,10 @@ func (c *ProviderCache) Branches(ctx context.Context, owner, repo string) (map[s
return ret, err 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") != "" { if headers != nil && headers.Get("Range") != "" {
// ignore custom header // 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) key := fmt.Sprintf("%s/%s/%s/%s", owner, repo, commit, path)
lastCache, err := c.cacheBlob.Get(ctx, key) 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, Header: respHeader,
}, nil }, 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 err != nil || open == nil {
if open != nil { if open != nil {
_ = open.Body.Close() _ = open.Body.Close()

View File

@@ -17,10 +17,11 @@ type ProviderGitea struct {
BaseURL string BaseURL string
Token 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)) client, err := gitea.NewClient(url, gitea.SetGiteaVersion(""), gitea.SetToken(token))
if err != nil { if err != nil {
return nil, err return nil, err
@@ -29,6 +30,7 @@ func NewGitea(url, token string) (*ProviderGitea, error) {
BaseURL: url, BaseURL: url,
Token: token, Token: token,
gitea: client, gitea: client,
client: httpClient,
}, nil }, nil
} }
@@ -103,7 +105,7 @@ func (g *ProviderGitea) Branches(_ context.Context, owner, repo string) (map[str
return result, nil 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 { if headers == nil {
headers = make(http.Header) 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) req.Header.Add("Authorization", "token "+g.Token)
return client.Do(req) return g.client.Do(req)
} }
func (g *ProviderGitea) Close() error { func (g *ProviderGitea) Close() error {

84
pkg/providers/local.go Normal file
View File

@@ -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
}

View File

@@ -15,19 +15,22 @@ import (
"go.uber.org/zap" "go.uber.org/zap"
"gopkg.d7z.net/gitea-pages/pkg/core" "gopkg.d7z.net/gitea-pages/pkg/core"
"gopkg.d7z.net/gitea-pages/pkg/filters" "gopkg.d7z.net/gitea-pages/pkg/filters"
"gopkg.d7z.net/middleware/cache"
"gopkg.d7z.net/middleware/kv" "gopkg.d7z.net/middleware/kv"
"gopkg.d7z.net/middleware/tools"
) )
var portExp = regexp.MustCompile(`:\d+$`) var portExp = regexp.MustCompile(`:\d+$`)
type Server struct { type Server struct {
backend core.Backend backend core.Backend
meta *core.PageDomain meta *core.PageDomain
db kv.CursorPagedKV
errorHandler func(w http.ResponseWriter, r *http.Request, err error)
filterMgr map[string]core.FilterInstance filterMgr map[string]core.FilterInstance
globCache *lru.Cache[string, glob.Glob] globCache *lru.Cache[string, glob.Glob]
cacheBlob cache.Cache
errorHandler func(w http.ResponseWriter, r *http.Request, err error)
} }
func NewPageServer( func NewPageServer(
@@ -35,27 +38,26 @@ func NewPageServer(
backend core.Backend, backend core.Backend,
domain string, domain string,
defaultBranch string, defaultBranch string,
db kv.KV, db kv.CursorPagedKV,
cache kv.KV, cacheMeta kv.KV,
cacheTTL time.Duration, cacheTTL time.Duration,
cacheBlob cache.Cache,
errorHandler func(w http.ResponseWriter, r *http.Request, err error), errorHandler func(w http.ResponseWriter, r *http.Request, err error),
) *Server { ) *Server {
svcMeta := core.NewServerMeta(client, backend, domain, cache, cacheTTL) svcMeta := core.NewServerMeta(client, backend, domain, cacheMeta, cacheTTL)
cfgDB := db.Child("config") pageMeta := core.NewPageDomain(svcMeta, core.NewDomainAlias(db.Child("config").Child("alias")), domain, defaultBranch)
pageMeta := core.NewPageDomain(svcMeta, globCache, err := lru.New[string, glob.Glob](256)
core.NewDomainAlias(cfgDB.Child("alias")),
cfgDB.Child("pages"),
domain, defaultBranch)
c, err := lru.New[string, glob.Glob](256)
if err != nil { if err != nil {
panic(err) panic(err)
} }
return &Server{ return &Server{
backend: backend, backend: backend,
meta: pageMeta, meta: pageMeta,
globCache: c, db: db,
globCache: globCache,
filterMgr: filters.DefaultFilters(), filterMgr: filters.DefaultFilters(),
errorHandler: errorHandler, errorHandler: errorHandler,
cacheBlob: cacheBlob,
} }
} }
@@ -88,6 +90,16 @@ func (s *Server) Serve(writer http.ResponseWriter, request *http.Request) error
if err != nil { if err != nil {
return err 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)) zap.L().Debug("new request", zap.Any("request path", meta.Path))
if strings.HasSuffix(meta.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 { for i, filter := range activeFiltersCall {
stack = core.NextCallWrapper(filter, stack, activeFilters[i]) stack = core.NextCallWrapper(filter, stack, activeFilters[i])
} }
err = stack(ctx, writer, request, meta) err = stack(filterCtx, writer, request)
return err return err
} }

View File

@@ -61,7 +61,7 @@ func (p *ProviderDummy) Branches(_ context.Context, owner, repo string) (map[str
return branches, nil 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)) open, err := os.Open(filepath.Join(p.BaseDir, owner, repo, commit, path))
if err != nil { if err != nil {
return nil, errors.Join(err, os.ErrNotExist) return nil, errors.Join(err, os.ErrNotExist)

View File

@@ -7,10 +7,12 @@ import (
"net/http/httptest" "net/http/httptest"
"os" "os"
"path/filepath" "path/filepath"
"time"
"github.com/pkg/errors" "github.com/pkg/errors"
"go.uber.org/zap" "go.uber.org/zap"
"gopkg.d7z.net/gitea-pages/pkg" "gopkg.d7z.net/gitea-pages/pkg"
"gopkg.d7z.net/middleware/cache"
"gopkg.d7z.net/middleware/kv" "gopkg.d7z.net/middleware/kv"
) )
@@ -34,7 +36,10 @@ func NewTestServer(domain string) *TestServer {
if err != nil { if err != nil {
zap.S().Fatal(err) zap.S().Fatal(err)
} }
memoryCache, _ := cache.NewMemoryCache(cache.MemoryCacheConfig{
MaxCapacity: 256,
CleanupInt: time.Minute,
})
memoryKV, _ := kv.NewMemory("") memoryKV, _ := kv.NewMemory("")
server := pkg.NewPageServer( server := pkg.NewPageServer(
http.DefaultClient, http.DefaultClient,
@@ -44,6 +49,7 @@ func NewTestServer(domain string) *TestServer {
memoryKV, memoryKV,
memoryKV.Child("cache"), memoryKV.Child("cache"),
0, 0,
memoryCache,
func(w http.ResponseWriter, r *http.Request, err error) { func(w http.ResponseWriter, r *http.Request, err error) {
if errors.Is(err, os.ErrNotExist) { if errors.Is(err, os.ErrNotExist) {
http.Error(w, "page not found.", http.StatusNotFound) http.Error(w, "page not found.", http.StatusNotFound)