切换存储方式
This commit is contained in:
2
.github/workflows/go-test.yml
vendored
2
.github/workflows/go-test.yml
vendored
@@ -28,7 +28,7 @@ jobs:
|
||||
run: go mod download
|
||||
|
||||
- name: Run tests with coverage
|
||||
run: go test -v -coverprofile=coverage.txt ./...
|
||||
run: make test lint
|
||||
|
||||
- uses: codecov/codecov-action@v5
|
||||
with:
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -63,4 +63,5 @@ fabric.properties
|
||||
config-local.yaml
|
||||
gitea-pages
|
||||
*.zip
|
||||
dist/
|
||||
dist/
|
||||
coverage.txt
|
||||
183
.golangci.yml
Normal file
183
.golangci.yml
Normal file
@@ -0,0 +1,183 @@
|
||||
# fork from gitea https://github.com/go-gitea/gitea/blob/main/.golangci.yml
|
||||
version: "2"
|
||||
output:
|
||||
sort-order:
|
||||
- file
|
||||
linters:
|
||||
default: none
|
||||
enable:
|
||||
- bidichk
|
||||
- depguard
|
||||
- dupl
|
||||
- errcheck
|
||||
- forbidigo
|
||||
- gocritic
|
||||
- govet
|
||||
- ineffassign
|
||||
- mirror
|
||||
- nakedret
|
||||
- nolintlint
|
||||
- perfsprint
|
||||
- revive
|
||||
- staticcheck
|
||||
- testifylint
|
||||
- unconvert
|
||||
- unparam
|
||||
- unused
|
||||
- usestdlibvars
|
||||
- usetesting
|
||||
- wastedassign
|
||||
settings:
|
||||
depguard:
|
||||
rules:
|
||||
main:
|
||||
deny:
|
||||
- pkg: github.com/unknwon/com
|
||||
desc: use gitea's util and replacements
|
||||
- pkg: io/ioutil
|
||||
desc: use os or io instead
|
||||
- pkg: golang.org/x/exp
|
||||
desc: it's experimental and unreliable
|
||||
- pkg: code.gitea.io/gitea/modules/git/internal
|
||||
desc: do not use the internal package, use AddXxx function instead
|
||||
- pkg: gopkg.in/ini.v1
|
||||
desc: do not use the ini package, use gitea's config system instead
|
||||
- pkg: gitea.com/go-chi/cache
|
||||
desc: do not use the go-chi cache package, use gitea's cache system
|
||||
nolintlint:
|
||||
allow-unused: false
|
||||
require-explanation: true
|
||||
require-specific: true
|
||||
gocritic:
|
||||
enabled-checks:
|
||||
- equalFold
|
||||
disabled-checks:
|
||||
- ifElseChain
|
||||
- singleCaseSwitch # Every time this occurred in the code, there was no other way.
|
||||
revive:
|
||||
severity: error
|
||||
rules:
|
||||
- name: atomic
|
||||
- name: bare-return
|
||||
- name: blank-imports
|
||||
- name: constant-logical-expr
|
||||
- name: context-as-argument
|
||||
- name: context-keys-type
|
||||
- name: dot-imports
|
||||
- name: duplicated-imports
|
||||
- name: empty-lines
|
||||
- name: error-naming
|
||||
- name: error-return
|
||||
- name: error-strings
|
||||
- name: errorf
|
||||
- name: exported
|
||||
- name: identical-branches
|
||||
- name: if-return
|
||||
- name: increment-decrement
|
||||
- name: indent-error-flow
|
||||
- name: modifies-value-receiver
|
||||
- name: package-comments
|
||||
- name: range
|
||||
- name: receiver-naming
|
||||
- name: redefines-builtin-id
|
||||
- name: string-of-int
|
||||
- name: superfluous-else
|
||||
- name: time-naming
|
||||
- name: unconditional-recursion
|
||||
- name: unexported-return
|
||||
- name: unreachable-code
|
||||
- name: var-declaration
|
||||
- name: var-naming
|
||||
arguments:
|
||||
- [] # AllowList - do not remove as args for the rule are positional and won't work without lists first
|
||||
- [] # DenyList
|
||||
- - skip-package-name-checks: true # supress errors from underscore in migration packages
|
||||
staticcheck:
|
||||
checks:
|
||||
- all
|
||||
- -ST1003
|
||||
- -ST1005
|
||||
- -QF1001
|
||||
- -QF1006
|
||||
- -QF1008
|
||||
testifylint:
|
||||
disable:
|
||||
- go-require
|
||||
- require-error
|
||||
usetesting:
|
||||
os-temp-dir: true
|
||||
exclusions:
|
||||
generated: lax
|
||||
presets:
|
||||
- comments
|
||||
- common-false-positives
|
||||
- legacy
|
||||
- std-error-handling
|
||||
rules:
|
||||
- linters:
|
||||
- dupl
|
||||
- errcheck
|
||||
- gocyclo
|
||||
- gosec
|
||||
- staticcheck
|
||||
- unparam
|
||||
path: _test\.go
|
||||
- linters:
|
||||
- dupl
|
||||
- errcheck
|
||||
- gocyclo
|
||||
- gosec
|
||||
path: models/migrations/v
|
||||
- linters:
|
||||
- forbidigo
|
||||
path: cmd
|
||||
- linters:
|
||||
- dupl
|
||||
text: (?i)webhook
|
||||
- linters:
|
||||
- gocritic
|
||||
text: (?i)`ID' should not be capitalized
|
||||
- linters:
|
||||
- deadcode
|
||||
- unused
|
||||
text: (?i)swagger
|
||||
- linters:
|
||||
- staticcheck
|
||||
text: (?i)argument x is overwritten before first use
|
||||
- linters:
|
||||
- gocritic
|
||||
text: '(?i)commentFormatting: put a space between `//` and comment text'
|
||||
- linters:
|
||||
- gocritic
|
||||
text: '(?i)exitAfterDefer:'
|
||||
paths:
|
||||
- node_modules
|
||||
- .venv
|
||||
- public
|
||||
- web_src
|
||||
- third_party$
|
||||
- builtin$
|
||||
- examples$
|
||||
issues:
|
||||
max-issues-per-linter: 0
|
||||
max-same-issues: 0
|
||||
formatters:
|
||||
enable:
|
||||
- gofmt
|
||||
- gofumpt
|
||||
settings:
|
||||
gofumpt:
|
||||
extra-rules: true
|
||||
exclusions:
|
||||
generated: lax
|
||||
paths:
|
||||
- node_modules
|
||||
- .venv
|
||||
- public
|
||||
- web_src
|
||||
- third_party$
|
||||
- builtin$
|
||||
- examples$
|
||||
|
||||
run:
|
||||
timeout: 10m
|
||||
12
Makefile
12
Makefile
@@ -35,7 +35,7 @@ debug: gitea-pages
|
||||
|
||||
.PHONY: test
|
||||
test:
|
||||
@go test -v ./...
|
||||
@go test -v -coverprofile=coverage.txt ./...
|
||||
|
||||
|
||||
.PHONY: releases
|
||||
@@ -44,3 +44,13 @@ releases:
|
||||
make release GOOS=linux GOARCH=arm64 && \
|
||||
make release GOOS=linux GOARCH=loong64 && \
|
||||
make release GOOS=windows GOARCH=amd64
|
||||
|
||||
.PHONY: lint
|
||||
lint:
|
||||
@(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
|
||||
|
||||
.PHONY: lint-fix
|
||||
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
|
||||
|
||||
@@ -10,7 +10,7 @@ This project focuses on functional implementation and does not consider any perf
|
||||
|
||||
## Get Started
|
||||
|
||||
Install `go1.24` or later, along with the `Make` tool, and then execute the following command:
|
||||
Install `go1.25` or later, along with the `Make` tool, and then execute the following command:
|
||||
|
||||
```bash
|
||||
make gitea-pages
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
|
||||
## Get Started
|
||||
|
||||
安装 `go1.24` 或更高版本,同时安装 `Make` 工具 ,然后执行如下命令:
|
||||
安装 `go1.25` 或更高版本,同时安装 `Make` 工具 ,然后执行如下命令:
|
||||
|
||||
```bash
|
||||
make gitea-pages
|
||||
|
||||
12
config.go
12
config.go
@@ -24,7 +24,7 @@ type Config struct {
|
||||
Bind string `yaml:"bind"` // HTTP 绑定
|
||||
Domain string `yaml:"domain"` // 基础域名
|
||||
|
||||
Config string `yaml:"config"` // 配置
|
||||
Database ConfigDatabase `yaml:"database"` // 配置
|
||||
|
||||
Auth ConfigAuth `yaml:"auth"` // 后端认证配置
|
||||
|
||||
@@ -46,7 +46,7 @@ func (c *Config) NewPageServerOptions() (*pkg.ServerOptions, error) {
|
||||
}
|
||||
var err error
|
||||
|
||||
if c.Config == "" {
|
||||
if c.Database.URL == "" {
|
||||
return nil, errors.New("config is required")
|
||||
}
|
||||
if c.StaticDir != "" {
|
||||
@@ -88,7 +88,7 @@ func (c *Config) NewPageServerOptions() (*pkg.ServerOptions, error) {
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "create cache")
|
||||
}
|
||||
alias, err := kv.NewKVFromURL(c.Config)
|
||||
alias, err := kv.NewKVFromURL(c.Database.URL)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to init alias config")
|
||||
}
|
||||
@@ -105,7 +105,7 @@ func (c *Config) NewPageServerOptions() (*pkg.ServerOptions, error) {
|
||||
CacheBlob: memoryCache,
|
||||
CacheBlobTTL: c.Cache.BlobTTL,
|
||||
CacheBlobLimit: uint64(c.Cache.BlobLimit),
|
||||
HttpClient: http.DefaultClient,
|
||||
HTTPClient: http.DefaultClient,
|
||||
EnableRender: c.Render.Enable,
|
||||
EnableProxy: c.Proxy.Enable,
|
||||
StaticDir: c.StaticDir,
|
||||
@@ -151,6 +151,10 @@ type ConfigPage struct {
|
||||
ErrUnknownPage string `yaml:"500"`
|
||||
}
|
||||
|
||||
type ConfigDatabase struct {
|
||||
URL string `yaml:"url"`
|
||||
}
|
||||
|
||||
type ConfigProxy struct {
|
||||
Enable bool `yaml:"enable"` // 是否允许反向代理
|
||||
}
|
||||
|
||||
24
config.yaml
24
config.yaml
@@ -2,20 +2,26 @@
|
||||
bind: 127.0.0.1:18080
|
||||
# 基础域名
|
||||
domain: example.com
|
||||
# 持久化存储
|
||||
database:
|
||||
# 持久化存储配置
|
||||
url: "memory://"
|
||||
auth:
|
||||
type: gitea
|
||||
server: https://gitea.com
|
||||
# 需要 user , org , repo 的 read 权限
|
||||
token: token
|
||||
cache:
|
||||
# 配置存储
|
||||
storage: /path/to/config.json
|
||||
# 配置缓存时长
|
||||
ttl: 10m
|
||||
|
||||
# 单个文件最大缓存大小
|
||||
size: 10MB
|
||||
# 总缓存大小
|
||||
max: 1GB
|
||||
# 元数据缓存
|
||||
meta: "memory://"
|
||||
# 元数据缓存时长
|
||||
meta_ttl: 1m
|
||||
# 响应数据缓存
|
||||
blob: "memory://"
|
||||
# 响应数据缓存时长
|
||||
blob_ttl: 1m
|
||||
# 最大单个响应数据缓存大小
|
||||
blob_limit: 10MB
|
||||
page:
|
||||
# 默认页面分支
|
||||
default_branch: gh-pages
|
||||
|
||||
31
main.go
31
main.go
@@ -10,25 +10,48 @@ import (
|
||||
"syscall"
|
||||
|
||||
"go.uber.org/zap"
|
||||
"gopkg.in/yaml.v3"
|
||||
|
||||
"gopkg.d7z.net/gitea-pages/pkg"
|
||||
"gopkg.d7z.net/gitea-pages/pkg/providers"
|
||||
_ "gopkg.d7z.net/gitea-pages/pkg/renders"
|
||||
)
|
||||
|
||||
var (
|
||||
configPath = "config-local.yaml"
|
||||
debug = false
|
||||
generate = false
|
||||
)
|
||||
|
||||
func init() {
|
||||
flag.StringVar(&configPath, "conf", configPath, "config file path")
|
||||
flag.BoolVar(&generate, "generate", debug, "generate config file")
|
||||
flag.BoolVar(&debug, "debug", debug, "debug mode")
|
||||
flag.Parse()
|
||||
}
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
if generate {
|
||||
var cfg Config
|
||||
file, err := os.ReadFile(configPath)
|
||||
if err == nil {
|
||||
_ = yaml.Unmarshal(file, &cfg)
|
||||
}
|
||||
out, err := yaml.Marshal(&cfg)
|
||||
if err != nil {
|
||||
log.Fatal("marshal config file failed", zap.Error(err))
|
||||
}
|
||||
err = os.WriteFile(configPath, out, 0o644)
|
||||
if err != nil {
|
||||
log.Fatal("write config file failed", zap.Error(err))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
call := logInject()
|
||||
defer call()
|
||||
defer func() {
|
||||
_ = call()
|
||||
}()
|
||||
config, err := LoadConfig(configPath)
|
||||
if err != nil {
|
||||
log.Fatalf("fail to load config file: %v", err)
|
||||
@@ -48,9 +71,7 @@ func main() {
|
||||
|
||||
svc := http.Server{Addr: config.Bind, Handler: giteaServer}
|
||||
go func() {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
}
|
||||
<-ctx.Done()
|
||||
zap.L().Debug("shutdown gracefully")
|
||||
_ = svc.Close()
|
||||
}()
|
||||
|
||||
@@ -45,7 +45,7 @@ func (a *DomainAlias) Bind(ctx context.Context, domains []string, owner, repo, b
|
||||
return err
|
||||
}
|
||||
}
|
||||
if domains == nil || len(domains) == 0 {
|
||||
if len(domains) == 0 {
|
||||
return nil
|
||||
}
|
||||
aliasMeta := &Alias{
|
||||
|
||||
@@ -2,6 +2,7 @@ package core
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
@@ -12,7 +13,7 @@ type BranchInfo struct {
|
||||
}
|
||||
|
||||
type Backend interface {
|
||||
Close() error
|
||||
io.Closer
|
||||
// Repos return repo name + default branch
|
||||
Repos(ctx context.Context, owner string) (map[string]string, error)
|
||||
// Branches return branch + commit id
|
||||
|
||||
@@ -57,7 +57,6 @@ func (c *CacheBackend) Repos(ctx context.Context, owner string) (map[string]stri
|
||||
}
|
||||
|
||||
func (c *CacheBackend) Branches(ctx context.Context, owner, repo string) (map[string]*BranchInfo, error) {
|
||||
ret := make(map[string]*BranchInfo)
|
||||
key := fmt.Sprintf("%s/%s", owner, repo)
|
||||
if load, b := c.cacheBranch.Load(ctx, key); b {
|
||||
return load, nil
|
||||
|
||||
@@ -70,24 +70,24 @@ func (p *PageDomain) ParseDomainMeta(ctx context.Context, domain, path, branch s
|
||||
return p.ReturnMeta(ctx, owner, domain, branch, pathArr)
|
||||
}
|
||||
|
||||
func (p *PageDomain) ReturnMeta(ctx context.Context, owner string, repo string, branch string, path []string) (*PageDomainContent, error) {
|
||||
func (p *PageDomain) ReturnMeta(ctx context.Context, owner, repo, branch string, path []string) (*PageDomainContent, error) {
|
||||
rel := &PageDomainContent{}
|
||||
if meta, err := p.GetMeta(ctx, owner, repo, branch); err == nil {
|
||||
rel.PageMetaContent = meta
|
||||
rel.Owner = owner
|
||||
rel.Repo = repo
|
||||
rel.Path = strings.Join(path, "/")
|
||||
if err = p.alias.Bind(ctx, meta.Alias, rel.Owner, rel.Repo, branch); err != nil {
|
||||
zap.L().Warn("别名绑定失败", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
return rel, nil
|
||||
} else {
|
||||
meta, err := p.GetMeta(ctx, owner, repo, branch)
|
||||
if err != nil {
|
||||
zap.L().Debug("查询错误", zap.Error(err))
|
||||
if meta != nil {
|
||||
// 解析错误汇报
|
||||
return nil, errors.New(meta.ErrorMsg)
|
||||
}
|
||||
return nil, errors.Wrap(os.ErrNotExist, strings.Join(path, "/"))
|
||||
}
|
||||
return nil, errors.Wrap(os.ErrNotExist, strings.Join(path, "/"))
|
||||
rel.PageMetaContent = meta
|
||||
rel.Owner = owner
|
||||
rel.Repo = repo
|
||||
rel.Path = strings.Join(path, "/")
|
||||
if err = p.alias.Bind(ctx, meta.Alias, rel.Owner, rel.Repo, branch); err != nil {
|
||||
zap.L().Warn("别名绑定失败", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
return rel, nil
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ package core
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
@@ -31,7 +30,7 @@ type ServerMeta struct {
|
||||
Domain string
|
||||
|
||||
client *http.Client
|
||||
|
||||
|
||||
cache kv.KV
|
||||
ttl time.Duration
|
||||
|
||||
@@ -44,29 +43,29 @@ func NewServerMeta(client *http.Client, backend Backend, kv kv.KV, domain string
|
||||
|
||||
func (s *ServerMeta) GetMeta(ctx context.Context, owner, repo, branch string) (*PageMetaContent, error) {
|
||||
rel := NewPageMetaContent()
|
||||
if repos, err := s.Repos(ctx, owner); err != nil {
|
||||
repos, err := s.Repos(ctx, owner)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
defBranch := repos[repo]
|
||||
if defBranch == "" {
|
||||
return nil, os.ErrNotExist
|
||||
}
|
||||
if branch == "" {
|
||||
branch = defBranch
|
||||
}
|
||||
}
|
||||
if branches, err := s.Branches(ctx, owner, repo); err != nil {
|
||||
defBranch := repos[repo]
|
||||
if defBranch == "" {
|
||||
return nil, os.ErrNotExist
|
||||
}
|
||||
if branch == "" {
|
||||
branch = defBranch
|
||||
}
|
||||
branches, err := s.Branches(ctx, owner, repo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
info := branches[branch]
|
||||
if info == nil {
|
||||
return nil, os.ErrNotExist
|
||||
}
|
||||
rel.CommitID = info.ID
|
||||
rel.LastModified = info.LastModified
|
||||
}
|
||||
info := branches[branch]
|
||||
if info == nil {
|
||||
return nil, os.ErrNotExist
|
||||
}
|
||||
rel.CommitID = info.ID
|
||||
rel.LastModified = info.LastModified
|
||||
|
||||
key := fmt.Sprintf("meta/%s/%s/%s", owner, repo, branch)
|
||||
key := s.cache.WithKey("meta", owner, repo, branch)
|
||||
cache, err := s.cache.Get(ctx, key)
|
||||
if err != nil && !errors.Is(err, os.ErrNotExist) {
|
||||
return nil, err
|
||||
@@ -97,14 +96,13 @@ func (s *ServerMeta) GetMeta(ctx context.Context, owner, repo, branch string) (*
|
||||
rel.IsPage = false
|
||||
_ = s.cache.Put(ctx, key, rel.String(), s.ttl)
|
||||
return nil, os.ErrNotExist
|
||||
} else {
|
||||
rel.IsPage = true
|
||||
}
|
||||
errFunc := func(err error) (*PageMetaContent, error) {
|
||||
rel.IsPage = true
|
||||
errCall := func(err error) error {
|
||||
rel.IsPage = false
|
||||
rel.ErrorMsg = err.Error()
|
||||
_ = s.cache.Put(ctx, key, rel.String(), s.ttl)
|
||||
return nil, err
|
||||
return err
|
||||
}
|
||||
// 添加默认跳过的内容
|
||||
for _, defIgnore := range rel.Ignore {
|
||||
@@ -114,7 +112,7 @@ func (s *ServerMeta) GetMeta(ctx context.Context, owner, repo, branch string) (*
|
||||
if data, err := s.ReadString(ctx, owner, repo, rel.CommitID, ".pages.yaml"); err == nil {
|
||||
cfg := new(PageConfig)
|
||||
if err = yaml.Unmarshal([]byte(data), cfg); err != nil {
|
||||
return errFunc(err)
|
||||
return nil, errCall(err)
|
||||
}
|
||||
rel.VRoute = cfg.VirtualRoute
|
||||
// 处理 CNAME
|
||||
@@ -123,14 +121,14 @@ func (s *ServerMeta) GetMeta(ctx context.Context, owner, repo, branch string) (*
|
||||
if regexpHostname.MatchString(cname) && !strings.HasSuffix(strings.ToLower(cname), strings.ToLower(s.Domain)) {
|
||||
rel.Alias = append(rel.Alias, cname)
|
||||
} else {
|
||||
return errFunc(errors.New("invalid alias name " + cname))
|
||||
return nil, errCall(errors.New("invalid alias name " + cname))
|
||||
}
|
||||
}
|
||||
// 处理渲染器
|
||||
for sType, pattern := range cfg.Renders() {
|
||||
var r Render
|
||||
if r = GetRender(sType); r == nil {
|
||||
return errFunc(errors.Errorf("render not found %s", sType))
|
||||
return nil, errCall(errors.Errorf("render not found %s", sType))
|
||||
}
|
||||
if g, err := glob.Compile(strings.TrimSpace(pattern)); err == nil {
|
||||
rel.rendersL = append(rel.rendersL, &renderCompiler{
|
||||
@@ -138,7 +136,7 @@ func (s *ServerMeta) GetMeta(ctx context.Context, owner, repo, branch string) (*
|
||||
Render: r,
|
||||
})
|
||||
} else {
|
||||
return errFunc(err)
|
||||
return nil, errCall(err)
|
||||
}
|
||||
rel.Renders[sType] = append(rel.Renders[sType], pattern)
|
||||
}
|
||||
@@ -147,7 +145,7 @@ func (s *ServerMeta) GetMeta(ctx context.Context, owner, repo, branch string) (*
|
||||
if g, err := glob.Compile(pattern); err == nil {
|
||||
rel.ignoreL = append(rel.ignoreL, g)
|
||||
} else {
|
||||
return errFunc(err)
|
||||
return nil, errCall(err)
|
||||
}
|
||||
rel.Ignore = append(rel.Ignore, pattern)
|
||||
}
|
||||
@@ -157,17 +155,15 @@ func (s *ServerMeta) GetMeta(ctx context.Context, owner, repo, branch string) (*
|
||||
if !strings.HasPrefix(path, "/") {
|
||||
path = "/" + path
|
||||
}
|
||||
if strings.HasSuffix(path, "/") {
|
||||
path = path[:len(path)-1]
|
||||
path = strings.TrimSuffix(path, "/")
|
||||
var rURL *url.URL
|
||||
if rURL, err = url.Parse(backend); err != nil {
|
||||
return nil, errCall(err)
|
||||
}
|
||||
var rUrl *url.URL
|
||||
if rUrl, err = url.Parse(backend); err != nil {
|
||||
return errFunc(err)
|
||||
if rURL.Scheme != "http" && rURL.Scheme != "https" {
|
||||
return nil, errCall(errors.New("invalid backend url " + backend))
|
||||
}
|
||||
if rUrl.Scheme != "http" && rUrl.Scheme != "https" {
|
||||
return errFunc(errors.New("invalid backend url " + backend))
|
||||
}
|
||||
rel.Proxy[path] = rUrl.String()
|
||||
rel.Proxy[path] = rURL.String()
|
||||
}
|
||||
} else {
|
||||
// 不存在配置,但也可以重定向
|
||||
|
||||
@@ -14,25 +14,25 @@ import (
|
||||
const GiteaMaxCount = 9999
|
||||
|
||||
type ProviderGitea struct {
|
||||
BaseUrl string
|
||||
BaseURL string
|
||||
Token string
|
||||
|
||||
gitea *gitea.Client
|
||||
}
|
||||
|
||||
func NewGitea(url string, token string) (*ProviderGitea, error) {
|
||||
func NewGitea(url, token string) (*ProviderGitea, error) {
|
||||
client, err := gitea.NewClient(url, gitea.SetGiteaVersion(""), gitea.SetToken(token))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &ProviderGitea{
|
||||
BaseUrl: url,
|
||||
BaseURL: url,
|
||||
Token: token,
|
||||
gitea: client,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (g *ProviderGitea) Repos(ctx context.Context, owner string) (map[string]string, error) {
|
||||
func (g *ProviderGitea) Repos(_ context.Context, owner string) (map[string]string, error) {
|
||||
result := make(map[string]string)
|
||||
if repos, resp, err := g.gitea.ListOrgRepos(owner, gitea.ListOrgReposOptions{
|
||||
ListOptions: gitea.ListOptions{
|
||||
@@ -75,26 +75,26 @@ func (g *ProviderGitea) Repos(ctx context.Context, owner string) (map[string]str
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (g *ProviderGitea) Branches(ctx context.Context, owner, repo string) (map[string]*core.BranchInfo, error) {
|
||||
func (g *ProviderGitea) Branches(_ context.Context, owner, repo string) (map[string]*core.BranchInfo, error) {
|
||||
result := make(map[string]*core.BranchInfo)
|
||||
if branches, resp, err := g.gitea.ListRepoBranches(owner, repo, gitea.ListRepoBranchesOptions{
|
||||
branches, resp, err := g.gitea.ListRepoBranches(owner, repo, gitea.ListRepoBranchesOptions{
|
||||
ListOptions: gitea.ListOptions{
|
||||
PageSize: GiteaMaxCount,
|
||||
},
|
||||
}); err != nil {
|
||||
})
|
||||
if err != nil {
|
||||
if resp != nil {
|
||||
_ = resp.Body.Close()
|
||||
}
|
||||
return nil, err
|
||||
} else {
|
||||
if resp != nil {
|
||||
_ = resp.Body.Close()
|
||||
}
|
||||
for _, branch := range branches {
|
||||
result[branch.Name] = &core.BranchInfo{
|
||||
ID: branch.Commit.ID,
|
||||
LastModified: branch.Commit.Timestamp,
|
||||
}
|
||||
}
|
||||
if resp != nil {
|
||||
_ = resp.Body.Close()
|
||||
}
|
||||
for _, branch := range branches {
|
||||
result[branch.Name] = &core.BranchInfo{
|
||||
ID: branch.Commit.ID,
|
||||
LastModified: branch.Commit.Timestamp,
|
||||
}
|
||||
}
|
||||
if len(result) == 0 {
|
||||
@@ -104,7 +104,10 @@ func (g *ProviderGitea) Branches(ctx context.Context, owner, repo string) (map[s
|
||||
}
|
||||
|
||||
func (g *ProviderGitea) Open(ctx context.Context, client *http.Client, owner, repo, commit, path string, headers http.Header) (*http.Response, error) {
|
||||
giteaURL, err := url.JoinPath(g.BaseUrl, "api/v1/repos", owner, repo, "media", path)
|
||||
if headers == nil {
|
||||
headers = make(http.Header)
|
||||
}
|
||||
giteaURL, err := url.JoinPath(g.BaseURL, "api/v1/repos", owner, repo, "media", path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -113,11 +116,9 @@ func (g *ProviderGitea) Open(ctx context.Context, client *http.Client, owner, re
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if headers != nil {
|
||||
for key, values := range headers {
|
||||
for _, value := range values {
|
||||
req.Header.Add(key, value)
|
||||
}
|
||||
for key, values := range headers {
|
||||
for _, value := range values {
|
||||
req.Header.Add(key, value)
|
||||
}
|
||||
}
|
||||
req.Header.Add("Authorization", "token "+g.Token)
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -19,19 +20,19 @@ import (
|
||||
"gopkg.d7z.net/middleware/cache"
|
||||
"gopkg.d7z.net/middleware/kv"
|
||||
|
||||
stdErr "errors"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"gopkg.d7z.net/gitea-pages/pkg/core"
|
||||
"gopkg.d7z.net/gitea-pages/pkg/utils"
|
||||
|
||||
_ "gopkg.d7z.net/gitea-pages/pkg/renders"
|
||||
)
|
||||
|
||||
var portExp = regexp.MustCompile(`:\d+$`)
|
||||
|
||||
type ServerOptions struct {
|
||||
Domain string //默认域名
|
||||
Domain string // 默认域名
|
||||
DefaultBranch string // 默认分支
|
||||
|
||||
Alias kv.KV // 配置映射关系
|
||||
@@ -43,7 +44,7 @@ type ServerOptions struct {
|
||||
CacheBlobTTL time.Duration // 配置缓存时长
|
||||
CacheBlobLimit uint64 // blob最大缓存大小
|
||||
|
||||
HttpClient *http.Client //自定义客户端
|
||||
HTTPClient *http.Client // 自定义客户端
|
||||
|
||||
EnableRender bool // 允许渲染
|
||||
EnableProxy bool // 允许代理
|
||||
@@ -68,7 +69,7 @@ func DefaultOptions(domain string) ServerOptions {
|
||||
CacheBlobTTL: time.Minute,
|
||||
CacheBlobLimit: 1024 * 1024 * 10,
|
||||
|
||||
HttpClient: http.DefaultClient,
|
||||
HTTPClient: http.DefaultClient,
|
||||
|
||||
EnableRender: true,
|
||||
EnableProxy: true,
|
||||
@@ -95,9 +96,9 @@ var staticPrefix = "/.well-known/page-server/"
|
||||
|
||||
func NewPageServer(backend core.Backend, options ServerOptions) *Server {
|
||||
backend = core.NewCacheBackend(backend, options.CacheMeta, options.CacheMetaTTL)
|
||||
svcMeta := core.NewServerMeta(options.HttpClient, backend, options.CacheMeta, options.Domain, options.CacheMetaTTL)
|
||||
svcMeta := core.NewServerMeta(options.HTTPClient, backend, options.CacheMeta, options.Domain, options.CacheMetaTTL)
|
||||
pageMeta := core.NewPageDomain(svcMeta, options.Alias, options.Domain, options.DefaultBranch)
|
||||
reader := core.NewCacheBackendBlobReader(options.HttpClient, backend, options.CacheBlob, options.CacheBlobLimit)
|
||||
reader := core.NewCacheBackendBlobReader(options.HTTPClient, backend, options.CacheBlob, options.CacheBlobLimit)
|
||||
var fs http.Handler
|
||||
if options.StaticDir != "" {
|
||||
fs = http.StripPrefix(staticPrefix, http.FileServer(http.Dir(options.StaticDir)))
|
||||
@@ -112,15 +113,15 @@ func NewPageServer(backend core.Backend, options ServerOptions) *Server {
|
||||
}
|
||||
|
||||
func (s *Server) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
|
||||
sessionId, _ := uuid.NewRandom()
|
||||
request.Header.Set("Session-ID", sessionId.String())
|
||||
sessionID, _ := uuid.NewRandom()
|
||||
request.Header.Set("Session-ID", sessionID.String())
|
||||
if s.fs != nil && strings.HasPrefix(request.URL.Path, staticPrefix) {
|
||||
s.fs.ServeHTTP(writer, request)
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
if e := recover(); e != nil {
|
||||
zap.L().Error("panic!", zap.Any("error", e), zap.Any("id", sessionId))
|
||||
zap.L().Error("panic!", zap.Any("error", e), zap.Any("id", sessionID))
|
||||
if err, ok := e.(error); ok {
|
||||
s.options.DefaultErrorHandler(writer, request, err)
|
||||
}
|
||||
@@ -128,7 +129,7 @@ func (s *Server) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
|
||||
}()
|
||||
err := s.Serve(writer, request)
|
||||
if err != nil {
|
||||
zap.L().Debug("错误请求", zap.Error(err), zap.Any("request", request.RequestURI), zap.Any("id", sessionId))
|
||||
zap.L().Debug("错误请求", zap.Error(err), zap.Any("request", request.RequestURI), zap.Any("id", sessionID))
|
||||
s.options.DefaultErrorHandler(writer, request, err)
|
||||
}
|
||||
}
|
||||
@@ -144,9 +145,9 @@ func (s *Server) Serve(writer http.ResponseWriter, request *http.Request) error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
zap.L().Debug("获取请求", zap.Any("request", meta.Path))
|
||||
zap.L().Debug("new request", zap.Any("request path", meta.Path))
|
||||
if len(meta.Alias) > 0 && !slices.Contains(meta.Alias, domainHost) {
|
||||
zap.L().Debug("重定向地址", zap.Any("src", request.Host), zap.Any("dst", meta.Alias[0]))
|
||||
zap.L().Debug("redirect", zap.Any("src", request.Host), zap.Any("dst", meta.Alias[0]))
|
||||
http.Redirect(writer, request, fmt.Sprintf("https://%s/%s", meta.Alias[0], meta.Path), http.StatusFound)
|
||||
return nil
|
||||
}
|
||||
@@ -163,7 +164,7 @@ func (s *Server) Serve(writer http.ResponseWriter, request *http.Request) error
|
||||
request.URL.Path = targetPath
|
||||
request.RequestURI = request.URL.RequestURI()
|
||||
proxy := httputil.NewSingleHostReverseProxy(u)
|
||||
proxy.Transport = s.options.HttpClient.Transport
|
||||
proxy.Transport = s.options.HTTPClient.Transport
|
||||
|
||||
if host, _, err := net.SplitHostPort(request.RemoteAddr); err == nil {
|
||||
request.Header.Set("X-Real-IP", host)
|
||||
@@ -181,11 +182,11 @@ func (s *Server) Serve(writer http.ResponseWriter, request *http.Request) error
|
||||
}
|
||||
// 在非反向代理时处理目录访问
|
||||
if strings.HasSuffix(meta.Path, "/") || meta.Path == "" {
|
||||
meta.Path = meta.Path + "index.html"
|
||||
meta.Path += "index.html"
|
||||
}
|
||||
|
||||
// 如果不是反向代理路由则跳过任何配置
|
||||
if request.Method != "GET" {
|
||||
if request.Method != http.MethodGet {
|
||||
return os.ErrNotExist
|
||||
}
|
||||
var result io.ReadCloser
|
||||
@@ -225,18 +226,13 @@ func (s *Server) Serve(writer http.ResponseWriter, request *http.Request) error
|
||||
writer.WriteHeader(http.StatusNotFound)
|
||||
if render := meta.TryRender(meta.Path, "/404.html"); render != nil && s.options.EnableRender {
|
||||
defer result.Close()
|
||||
if err = render.Render(writer, request, result); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
} else {
|
||||
_, _ = io.Copy(writer, result)
|
||||
_ = result.Close()
|
||||
return render.Render(writer, request, result)
|
||||
}
|
||||
_, _ = io.Copy(writer, result)
|
||||
_ = result.Close()
|
||||
return nil
|
||||
} else {
|
||||
return err
|
||||
}
|
||||
return err
|
||||
}
|
||||
fileName := filepath.Base(meta.Path)
|
||||
render := meta.TryRender(meta.Path)
|
||||
@@ -257,7 +253,7 @@ func (s *Server) Serve(writer http.ResponseWriter, request *http.Request) error
|
||||
}
|
||||
} else {
|
||||
if reader, ok := result.(*utils.SizeReadCloser); ok && render == nil {
|
||||
writer.Header().Add("Content-Length", fmt.Sprintf("%d", reader.Size))
|
||||
writer.Header().Add("Content-Length", strconv.FormatUint(reader.Size, 10))
|
||||
}
|
||||
// todo(bug) : 直连模式下告知数据长度
|
||||
writer.Header().Add("X-CacheBlob", "MISS")
|
||||
@@ -276,11 +272,10 @@ func (s *Server) Serve(writer http.ResponseWriter, request *http.Request) error
|
||||
}
|
||||
|
||||
func (s *Server) Close() error {
|
||||
if err := s.options.Alias.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.options.CacheBlob.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
return s.backend.Close()
|
||||
return stdErr.Join(
|
||||
s.options.CacheBlob.Close(),
|
||||
s.options.CacheMeta.Close(),
|
||||
s.options.Alias.Close(),
|
||||
s.backend.Close(),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package core
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"mime"
|
||||
"net/http"
|
||||
@@ -60,13 +61,16 @@ func (p *ProviderDummy) Branches(ctx context.Context, owner, repo string) (map[s
|
||||
return branches, nil
|
||||
}
|
||||
|
||||
func (p *ProviderDummy) Open(ctx context.Context, _ *http.Client, owner, repo, commit, path string, _ http.Header) (*http.Response, error) {
|
||||
func (p *ProviderDummy) Open(_ context.Context, _ *http.Client, 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, err
|
||||
return nil, errors.Join(err, os.ErrNotExist)
|
||||
}
|
||||
all, err := io.ReadAll(open)
|
||||
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)))
|
||||
|
||||
@@ -64,7 +64,7 @@ func (t *TestServer) AddFile(path, data string, args ...interface{}) {
|
||||
|
||||
func (t *TestServer) OpenFile(url string) ([]byte, *http.Response, error) {
|
||||
recorder := httptest.NewRecorder()
|
||||
t.server.ServeHTTP(recorder, httptest.NewRequest("GET", url, nil))
|
||||
t.server.ServeHTTP(recorder, httptest.NewRequest(http.MethodGet, url, nil))
|
||||
response := recorder.Result()
|
||||
if response.Body != nil {
|
||||
defer response.Body.Close()
|
||||
|
||||
@@ -15,9 +15,9 @@ func Test_get_simple_html(t *testing.T) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "hello world", string(data))
|
||||
|
||||
_, resp, err := server.OpenFile("https://org1.example.com/repo1/404")
|
||||
_, resp, _ := server.OpenFile("https://org1.example.com/repo1/404")
|
||||
assert.NotNil(t, resp)
|
||||
assert.Equal(t, resp.StatusCode, 404)
|
||||
assert.Equal(t, 404, resp.StatusCode)
|
||||
}
|
||||
|
||||
func Test_get_alias(t *testing.T) {
|
||||
@@ -28,13 +28,13 @@ func Test_get_alias(t *testing.T) {
|
||||
alias:
|
||||
- www.example.org
|
||||
`)
|
||||
data, resp, err := server.OpenFile("https://www.example.org")
|
||||
assert.Equal(t, resp.StatusCode, 404)
|
||||
_, resp, _ := server.OpenFile("https://www.example.org")
|
||||
assert.Equal(t, 404, resp.StatusCode)
|
||||
|
||||
data, resp, err = server.OpenFile("https://org1.example.com/repo1/")
|
||||
assert.Equal(t, resp.StatusCode, 302)
|
||||
assert.Equal(t, resp.Header.Get("Location"), "https://www.example.org/")
|
||||
data, resp, err = server.OpenFile("https://www.example.org")
|
||||
_, resp, _ = server.OpenFile("https://org1.example.com/repo1/")
|
||||
assert.Equal(t, 302, resp.StatusCode)
|
||||
assert.Equal(t, "https://www.example.org/", resp.Header.Get("Location"))
|
||||
data, _, err := server.OpenFile("https://www.example.org")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "hello world", string(data))
|
||||
|
||||
@@ -42,20 +42,20 @@ alias:
|
||||
alias:
|
||||
- zzz.example.top
|
||||
`)
|
||||
data, resp, err = server.OpenFile("https://www.example.org")
|
||||
assert.Equal(t, resp.StatusCode, 302)
|
||||
assert.Equal(t, resp.Header.Get("Location"), "https://zzz.example.top/")
|
||||
_, resp, _ = server.OpenFile("https://www.example.org")
|
||||
assert.Equal(t, 302, resp.StatusCode)
|
||||
assert.Equal(t, "https://zzz.example.top/", resp.Header.Get("Location"))
|
||||
|
||||
data, resp, err = server.OpenFile("https://www.example.org")
|
||||
assert.Equal(t, resp.StatusCode, 404)
|
||||
_, resp, _ = server.OpenFile("https://www.example.org")
|
||||
assert.Equal(t, 404, resp.StatusCode)
|
||||
|
||||
data, resp, err = server.OpenFile("https://org1.example.com/repo1/")
|
||||
assert.Equal(t, resp.StatusCode, 302)
|
||||
assert.Equal(t, resp.Header.Get("Location"), "https://zzz.example.top/")
|
||||
_, resp, _ = server.OpenFile("https://org1.example.com/repo1/")
|
||||
assert.Equal(t, 302, resp.StatusCode)
|
||||
assert.Equal(t, "https://zzz.example.top/", resp.Header.Get("Location"))
|
||||
|
||||
data, resp, err = server.OpenFile("https://org1.example.com/repo1/get/some")
|
||||
assert.Equal(t, resp.StatusCode, 302)
|
||||
assert.Equal(t, resp.Header.Get("Location"), "https://zzz.example.top/get/some")
|
||||
_, resp, _ = server.OpenFile("https://org1.example.com/repo1/get/some")
|
||||
assert.Equal(t, 302, resp.StatusCode)
|
||||
assert.Equal(t, "https://zzz.example.top/get/some", resp.Header.Get("Location"))
|
||||
}
|
||||
|
||||
func Test_fail_back(t *testing.T) {
|
||||
|
||||
@@ -32,8 +32,8 @@ proxy:
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "hello data", string(data))
|
||||
|
||||
_, resp, err := server.OpenFile("https://org1.example.com/repo1/abi/data")
|
||||
assert.Equal(t, resp.StatusCode, 404)
|
||||
_, resp, _ := server.OpenFile("https://org1.example.com/repo1/abi/data")
|
||||
assert.Equal(t, 404, resp.StatusCode)
|
||||
}
|
||||
|
||||
func Test_cname_proxy(t *testing.T) {
|
||||
@@ -50,18 +50,18 @@ alias:
|
||||
proxy:
|
||||
/api: %s/test
|
||||
`, hs.URL)
|
||||
_, resp, err := server.OpenFile("https://org1.example.com/repo1/")
|
||||
assert.Equal(t, resp.StatusCode, 302)
|
||||
assert.Equal(t, resp.Header.Get("Location"), "https://www.example.org/")
|
||||
data, resp, err := server.OpenFile("https://www.example.org")
|
||||
_, resp, _ := server.OpenFile("https://org1.example.com/repo1/")
|
||||
assert.Equal(t, 302, resp.StatusCode)
|
||||
assert.Equal(t, "https://www.example.org/", resp.Header.Get("Location"))
|
||||
data, _, err := server.OpenFile("https://www.example.org")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "hello world", string(data))
|
||||
|
||||
_, resp, err = server.OpenFile("https://org1.example.com/repo1/api")
|
||||
assert.Equal(t, resp.StatusCode, 302)
|
||||
assert.Equal(t, resp.Header.Get("Location"), "https://www.example.org/api")
|
||||
_, resp, _ = server.OpenFile("https://org1.example.com/repo1/api")
|
||||
assert.Equal(t, 302, resp.StatusCode)
|
||||
assert.Equal(t, "https://www.example.org/api", resp.Header.Get("Location"))
|
||||
|
||||
data, resp, err = server.OpenFile("https://www.example.org/api")
|
||||
data, _, err = server.OpenFile("https://www.example.org/api")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "hello proxy", string(data))
|
||||
}
|
||||
|
||||
@@ -5,6 +5,8 @@ import (
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"gopkg.d7z.net/gitea-pages/tests/core"
|
||||
|
||||
_ "gopkg.d7z.net/gitea-pages/pkg/renders"
|
||||
)
|
||||
|
||||
func Test_get_render(t *testing.T) {
|
||||
|
||||
Reference in New Issue
Block a user