重构项目

This commit is contained in:
ExplodingDragon
2025-11-13 23:31:23 +08:00
parent 54bbef0205
commit 02df131beb
12 changed files with 248 additions and 68 deletions

View File

@@ -21,9 +21,9 @@ dist/gitea-pages-$(GOOS)-$(GOARCH).tar.gz: $(shell find . -type f -name "*.go"
@echo Compile $@ via $(GO_DIST_NAME) && \
mkdir -p dist && \
rm -f dist/$(GO_DIST_NAME) && \
GOOS=$(GOOS) GOARCH=$(GOARCH) CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o dist/$(GO_DIST_NAME) . && \
GOOS=$(GOOS) GOARCH=$(GOARCH) CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o dist/$(GO_DIST_NAME) cmd/server && \
cd dist && \
tar zcf gitea-pages-$(GOOS)-$(GOARCH).tar.gz $(GO_DIST_NAME) ../LICENSE ../config.yaml ../errors.html.tmpl ../README.md ../README_*.md && \
tar zcf gitea-pages-$(GOOS)-$(GOARCH).tar.gz $(GO_DIST_NAME) ../LICENSE ../config.yaml ../cmd/server/errors.html.tmpl ../README.md ../README_*.md && \
rm -f $(GO_DIST_NAME)
gitea-pages: $(shell find . -type f -name "*.go" ) go.mod go.sum

29
cmd/local/main.go Normal file
View File

@@ -0,0 +1,29 @@
package main
import (
"flag"
"os"
)
var (
org = "pub"
domain = "fbi.com"
repo = org + "." + domain
path = ""
port = 8080
)
func init() {
dir, _ := os.Getwd()
path = dir
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.Parse()
}
func main() {
}

View File

@@ -16,6 +16,7 @@ type PageVFS struct {
commitID string
}
// todo: 限制最大文件加载大小
func NewPageVFS(
client *http.Client,
backend Backend,

View File

@@ -1,6 +1,9 @@
package filters
import "gopkg.d7z.net/gitea-pages/pkg/core"
import (
"gopkg.d7z.net/gitea-pages/pkg/core"
"gopkg.d7z.net/gitea-pages/pkg/filters/quickjs"
)
func DefaultFilters() map[string]core.FilterInstance {
return map[string]core.FilterInstance{
@@ -11,6 +14,6 @@ func DefaultFilters() map[string]core.FilterInstance {
"_404_": FilterInstDefaultNotFound,
"failback": FilterInstFailback,
"template": FilterInstTemplate,
"qjs": FilterInstQuickJS,
"qjs": quickjs.FilterInstQuickJS,
}
}

View File

@@ -1,7 +1,8 @@
package filters
package quickjs
import (
"context"
"fmt"
"io"
"log"
"net/http"
@@ -16,6 +17,7 @@ import (
var FilterInstQuickJS core.FilterInstance = func(config core.FilterParams) (core.FilterCall, error) {
var param struct {
Exec string `json:"exec"`
Debug bool `json:"debug"`
}
if err := config.Unmarshal(&param); err != nil {
return nil, err
@@ -29,37 +31,168 @@ var FilterInstQuickJS core.FilterInstance = func(config core.FilterParams) (core
return err
}
var rt = quickjs.NewRuntime()
rt := quickjs.NewRuntime()
rt.SetExecuteTimeout(5)
defer rt.Close()
jsCtx := rt.NewContext()
defer jsCtx.Close()
// 在 debug 模式下,我们需要拦截输出
var (
outputBuffer strings.Builder
logBuffer strings.Builder
jsError error
)
global := jsCtx.Globals()
global.Set("request", createRequestObject(jsCtx, request))
global.Set("request", createRequestObject(jsCtx, request, metadata))
// 根据是否 debug 模式创建不同的 response 对象
if param.Debug {
// debug 模式下使用虚假的 writer 来捕获输出
global.Set("response", createResponseObject(jsCtx, &debugResponseWriter{
buffer: &outputBuffer,
header: make(http.Header),
}, request))
global.Set("console", createConsoleObject(jsCtx, &logBuffer))
} else {
global.Set("response", createResponseObject(jsCtx, writer, request))
global.Set("console", createConsoleObject(jsCtx))
global.Set("console", createConsoleObject(jsCtx, nil))
}
ret := jsCtx.Eval(js)
defer ret.Free()
jsCtx.Loop()
if ret.IsException() {
err := jsCtx.Exception()
return err
jsError = err
}
return nil
// 如果在 debug 模式下,返回 HTML 调试页面
if param.Debug {
return renderDebugPage(writer, &outputBuffer, &logBuffer, jsError)
}
return jsError
}, nil
}
// createRequestObject 创建表示 HTTP 请求的 JavaScript 对象
func createRequestObject(ctx *quickjs.Context, req *http.Request) *quickjs.Value {
obj := ctx.NewObject()
// debugResponseWriter 用于在 debug 模式下捕获响应输出
type debugResponseWriter struct {
buffer *strings.Builder
header http.Header
status int
}
func (w *debugResponseWriter) Header() http.Header {
return w.header
}
func (w *debugResponseWriter) Write(data []byte) (int, error) {
return w.buffer.Write(data)
}
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 := `<!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))
obj.Set("url", ctx.NewString(req.URL.String()))
obj.Set("path", ctx.NewString(req.URL.Path))
obj.Set("query", ctx.NewString(req.URL.RawQuery))
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))
@@ -67,7 +200,7 @@ func createRequestObject(ctx *quickjs.Context, req *http.Request) *quickjs.Value
// 解析查询参数
queryObj := ctx.NewObject()
for key, values := range req.URL.Query() {
for key, values := range url.Query() {
if len(values) > 0 {
queryObj.Set(key, ctx.NewString(values[0]))
}
@@ -169,20 +302,20 @@ func createResponseObject(ctx *quickjs.Context, writer http.ResponseWriter, req
}
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]
headersObj.Properties().ForEach(func(key string, value *quickjs.Value) bool {
writer.Header().Set(key, value.String())
return true
})
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()
@@ -261,11 +394,11 @@ func createResponseObject(ctx *quickjs.Context, writer http.ResponseWriter, req
}
if secure := options.Get("secure"); !secure.IsNull() {
cookie.Secure = secure.Bool()
cookie.Secure = secure.ToBool()
}
if httpOnly := options.Get("httpOnly"); !httpOnly.IsNull() {
cookie.HttpOnly = httpOnly.Bool()
cookie.HttpOnly = httpOnly.ToBool()
}
}
@@ -278,48 +411,32 @@ func createResponseObject(ctx *quickjs.Context, writer http.ResponseWriter, req
}
// createConsoleObject 创建 console 对象用于日志输出
func createConsoleObject(ctx *quickjs.Context) *quickjs.Value {
func createConsoleObject(ctx *quickjs.Context, buf *strings.Builder) *quickjs.Value {
console := ctx.NewObject()
logFunc := func(level string) func(*quickjs.Context, *quickjs.Value, []*quickjs.Value) *quickjs.Value {
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())
}
log.Printf("[" + level + "] " + strings.Join(messages, " "))
return ctx.NewNull()
}
}
message := fmt.Sprintf("[%s] %s", level, strings.Join(messages, " "))
console.Set("log", ctx.NewFunction(logFunc("INFO")))
console.Set("info", ctx.NewFunction(logFunc("INFO")))
console.Set("warn", ctx.NewFunction(logFunc("WARN")))
console.Set("error", ctx.NewFunction(logFunc("ERROR")))
console.Set("debug", ctx.NewFunction(logFunc("DEBUG")))
// 总是输出到系统日志
log.Print(message)
// 添加 time 和 timeEnd 方法用于性能测量
timers := make(map[string]time.Time)
console.Set("time", ctx.NewFunction(func(c *quickjs.Context, value *quickjs.Value, args []*quickjs.Value) *quickjs.Value {
if len(args) > 0 {
label := args[0].String()
timers[label] = time.Now()
// 如果有缓冲区,也写入缓冲区
if buffer != nil {
buffer.WriteString(message + "\n")
}
return ctx.NewNull()
}))
console.Set("timeEnd", ctx.NewFunction(func(c *quickjs.Context, value *quickjs.Value, args []*quickjs.Value) *quickjs.Value {
if len(args) > 0 {
label := args[0].String()
if start, exists := timers[label]; exists {
elapsed := time.Since(start)
log.Printf("[TIMER] %s: %v", label, elapsed)
delete(timers, label)
}
}
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

@@ -9,6 +9,7 @@ import (
"slices"
"strings"
"github.com/pkg/errors"
"go.uber.org/zap"
"gopkg.d7z.net/gitea-pages/pkg/core"
)
@@ -24,7 +25,7 @@ var FilterInstRedirect core.FilterInstance = func(config core.FilterParams) (cor
return nil, err
}
if len(param.Targets) == 0 {
return nil, fmt.Errorf("no targets")
return nil, errors.New("no targets")
}
if param.Code == 0 {
param.Code = http.StatusFound
@@ -49,8 +50,7 @@ var FilterInstRedirect core.FilterInstance = func(config core.FilterParams) (cor
http.Redirect(writer, request, target.String(), param.Code)
return nil
} else {
return next(ctx, writer, request, metadata)
}
return next(ctx, writer, request, metadata)
}, nil
}

View File

@@ -73,8 +73,12 @@ func (t *TestServer) AddFile(path, data string, args ...interface{}) {
}
func (t *TestServer) OpenFile(url string) ([]byte, *http.Response, error) {
return t.OpenRequest(http.MethodGet, url, nil)
}
func (t *TestServer) OpenRequest(method, url string, body io.Reader) ([]byte, *http.Response, error) {
recorder := httptest.NewRecorder()
t.server.ServeHTTP(recorder, httptest.NewRequest(http.MethodGet, url, nil))
t.server.ServeHTTP(recorder, httptest.NewRequest(method, url, body))
response := recorder.Result()
if response.Body != nil {
defer response.Body.Close()

View File

@@ -7,7 +7,8 @@ import (
"gopkg.d7z.net/gitea-pages/tests/core"
)
func test_proxy(t *testing.T) {
func TestProxy(t *testing.T) {
t.Skip()
server := core.NewDefaultTestServer()
hs := core.NewServer()
defer server.Close()
@@ -40,7 +41,8 @@ proxy:
assert.Equal(t, 404, resp.StatusCode)
}
func test_cname_proxy(t *testing.T) {
func TestCnameProxy(t *testing.T) {
t.Skip()
server := core.NewDefaultTestServer()
hs := core.NewServer()
defer server.Close()

View File

@@ -1,6 +1,7 @@
package tests
import (
"net/http"
"testing"
"github.com/stretchr/testify/assert"
@@ -15,7 +16,7 @@ func Test_JS(t *testing.T) {
function get(a,b) {
return a + b;
}
request.Write()
response.write('512 + 512 = ' + get(512,512))
`)
server.AddFile("org1/repo1/gh-pages/.pages.yaml", `
routes:
@@ -30,5 +31,28 @@ routes:
data, _, err = server.OpenFile("https://org1.example.com/repo1/api/v1/get")
assert.NoError(t, err)
assert.Equal(t, "512 + 512 = 1024", string(data))
}
func Test_JS_Request(t *testing.T) {
server := core.NewDefaultTestServer()
defer server.Close()
server.AddFile("org1/repo1/gh-pages/index.html", "hello world")
server.AddFile("org1/repo1/gh-pages/index.js", `response.write(request.method+' /'+request.path)`)
server.AddFile("org1/repo1/gh-pages/.pages.yaml", `
routes:
- path: "api/v1/**"
qjs:
exec: "index.js"
`)
data, _, err := server.OpenFile("https://org1.example.com/repo1/")
assert.NoError(t, err)
assert.Equal(t, "hello world", string(data))
data, _, err = server.OpenFile("https://org1.example.com/repo1/api/v1/fetch")
assert.NoError(t, err)
assert.Equal(t, "GET /api/v1/fetch", string(data))
data, _, err = server.OpenRequest(http.MethodPost, "https://org1.example.com/repo1/api/v1/fetch", nil)
assert.NoError(t, err)
assert.Equal(t, "POST /api/v1/fetch", string(data))
}