diff --git a/README.md b/README.md index 255b82f..dc19e82 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,23 @@ make gitea-pages 具体配置可查看 [`config.yaml`](./config.yaml)。 + +### Page Config + +在项目的默认分支创建 `.pages.yaml`,填入如下内容 + +```yaml +v-route: true +alias: + - "example.com" + - "example2.com" +renders: + gotemplate: '**/*.tmpl,**/index.html' +proxy: + /api: https://github.com/api + +``` + ### Render 说明: **不会**将文件系统 引入到渲染器中,复杂的渲染流程应该采用更加灵活轻便的方案 @@ -40,6 +57,10 @@ gotemplate **/*.tmpl - [x] 内容缓存 - [x] CNAME 自定义域名 +- [x] 模板渲染 +- [ ] 反向代理请求 + - [ ] HTTP + - [ ] Websocket - [ ] OAuth2 授权访问私有页面 - [ ] ~~http01 自动签发证书~~: 交由 Caddy 完成 - [ ] ~~Web 钩子触发更新~~: 对实时性需求不大 diff --git a/config.go b/config.go index 887166f..aa9c868 100644 --- a/config.go +++ b/config.go @@ -28,6 +28,9 @@ type Config struct { Cache ConfigCache `yaml:"cache"` // 缓存配置 Page ConfigPage `yaml:"page"` // 页面配置 + Render ConfigRender `yaml:"render"` // 渲染配置 + Proxy ConfigProxy `yaml:"proxy"` // 反向代理配置 + pageErrNotFound, pageErrUnknown *template.Template } @@ -77,6 +80,7 @@ func (c *Config) NewPageServerOptions() (*pkg.ServerOptions, error) { DefaultBranch: c.Page.DefaultBranch, MaxCacheSize: int(cacheSize), HttpClient: http.DefaultClient, + MetaTTL: time.Minute, DefaultErrorHandler: c.ErrorHandler, Cache: utils.NewCacheMemory(int(cacheMaxSize), int(cacheMaxSize)), } @@ -87,9 +91,9 @@ func (c *Config) NewPageServerOptions() (*pkg.ServerOptions, error) { } memory, err := utils.NewConfigMemory(c.Cache.Storage) if err != nil { - return nil, err + return nil, errors.Wrapf(err, "failed to init config memory") } - rel.Config = memory + rel.KVConfig = memory return &rel, nil } @@ -130,6 +134,15 @@ type ConfigPage struct { ErrUnknownPage string `yaml:"500"` } +type ConfigProxy struct { + Enable bool `yaml:"enable"` // 是否允许反向代理模型 + DenyList []string `yaml:"deny"` // 反向代理黑名单 +} + +type ConfigRender struct { + Enable bool `yaml:"enable"` // 开启渲染器 +} + type ConfigCache struct { Storage string `yaml:"storage"` // 缓存归档位置 diff --git a/main.go b/main.go index 411a7c5..432c3d6 100644 --- a/main.go +++ b/main.go @@ -35,7 +35,7 @@ func main() { } options, err := config.NewPageServerOptions() if err != nil { - log.Fatalf("fail to create page server: %v", err) + zap.L().Fatal("fail to load options", zap.Error(err)) } gitea, err := providers.NewGitea(config.Auth.Server, config.Auth.Token) if err != nil { diff --git a/pkg/core/alias.go b/pkg/core/alias.go index ecfc017..f03307e 100644 --- a/pkg/core/alias.go +++ b/pkg/core/alias.go @@ -2,6 +2,7 @@ package core import ( "encoding/json" + "fmt" "gopkg.d7z.net/gitea-pages/pkg/utils" ) @@ -13,10 +14,10 @@ type Alias struct { } type DomainAlias struct { - config utils.Config + config utils.KVConfig } -func NewDomainAlias(config utils.Config) *DomainAlias { +func NewDomainAlias(config utils.KVConfig) *DomainAlias { return &DomainAlias{config: config} } @@ -32,17 +33,34 @@ func (a *DomainAlias) Query(domain string) (*Alias, error) { return rel, nil } -func (a *DomainAlias) Bind(domain, owner, repo, branch string) error { - save := &Alias{ +func (a *DomainAlias) Bind(domains []string, owner, repo, branch string) error { + oldDomains := make([]string, 0) + rKey := fmt.Sprintf("domain/r-alias/%s/%s/%s", owner, repo, branch) + if oldStr, err := a.config.Get(rKey); err == nil { + _ = json.Unmarshal([]byte(oldStr), &oldDomains) + } + for _, oldDomain := range oldDomains { + if err := a.Unbind(oldDomain); err != nil { + return err + } + } + if domains == nil || len(domains) == 0 { + return nil + } + aliasMeta := &Alias{ Owner: owner, Repo: repo, Branch: branch, } - saveB, err := json.Marshal(save) - if err != nil { - return err + aliasMetaRaw, _ := json.Marshal(aliasMeta) + domainsRaw, _ := json.Marshal(domains) + _ = a.config.Put(rKey, string(domainsRaw), utils.TtlKeep) + for _, domain := range domains { + if err := a.config.Put("domain/alias/"+domain, string(aliasMetaRaw), utils.TtlKeep); err != nil { + return err + } } - return a.config.Put("domain/alias/"+domain, string(saveB), utils.TtlKeep) + return nil } func (a *DomainAlias) Unbind(domain string) error { diff --git a/pkg/core/backend.go b/pkg/core/backend.go index 2fee3bd..e38e62b 100644 --- a/pkg/core/backend.go +++ b/pkg/core/backend.go @@ -23,6 +23,7 @@ type BranchInfo struct { } type Backend interface { + Close() error // Repos return repo name + default branch Repos(owner string) (map[string]string, error) // Branches return branch + commit id @@ -33,11 +34,15 @@ type Backend interface { type CacheBackend struct { backend Backend - config utils.Config + config utils.KVConfig ttl time.Duration } -func NewCacheBackend(backend Backend, config utils.Config, ttl time.Duration) *CacheBackend { +func (c *CacheBackend) Close() error { + return c.backend.Close() +} + +func NewCacheBackend(backend Backend, config utils.KVConfig, ttl time.Duration) *CacheBackend { return &CacheBackend{backend: backend, config: config, ttl: ttl} } diff --git a/pkg/core/domain.go b/pkg/core/domain.go index a64c7d7..d52fd1c 100644 --- a/pkg/core/domain.go +++ b/pkg/core/domain.go @@ -1,9 +1,7 @@ package core import ( - "fmt" "os" - "regexp" "strings" "gopkg.d7z.net/gitea-pages/pkg/utils" @@ -12,8 +10,6 @@ import ( "go.uber.org/zap" ) -var portExp = regexp.MustCompile(`:\d+$`) - type PageDomain struct { *ServerMeta @@ -22,7 +18,7 @@ type PageDomain struct { defaultBranch string } -func NewPageDomain(meta *ServerMeta, config utils.Config, baseDomain, defaultBranch string) *PageDomain { +func NewPageDomain(meta *ServerMeta, config utils.KVConfig, baseDomain, defaultBranch string) *PageDomain { return &PageDomain{ baseDomain: baseDomain, defaultBranch: defaultBranch, @@ -39,46 +35,40 @@ type PageDomainContent struct { Path string } -func (m *PageDomainContent) CacheKey() string { - return fmt.Sprintf("%s/%s/%s/%s", m.Owner, m.Repo, m.CommitID, m.Path) -} - func (p *PageDomain) ParseDomainMeta(domain, path, branch string) (*PageDomainContent, error) { if branch == "" { branch = p.defaultBranch } - domain = portExp.ReplaceAllString(strings.ToLower(domain), "") - pathS := strings.Split(strings.TrimPrefix(path, "/"), "/") - + pathArr := strings.Split(strings.TrimPrefix(path, "/"), "/") if !strings.HasSuffix(domain, "."+p.baseDomain) { - alias, err := p.alias.Query(domain) + alias, err := p.alias.Query(domain) // 确定 alias 是否存在内容 if err != nil { zap.L().Warn("未知域名", zap.String("base", p.baseDomain), zap.String("domain", domain), zap.Error(err)) return nil, os.ErrNotExist } zap.L().Debug("命中别名", zap.String("domain", domain), zap.Any("alias", alias)) - return p.ReturnMeta(alias.Owner, alias.Repo, alias.Branch, pathS) + return p.ReturnMeta(alias.Owner, alias.Repo, alias.Branch, pathArr) } owner := strings.TrimSuffix(domain, "."+p.baseDomain) - repo := pathS[0] + repo := pathArr[0] if repo == "" { // 回退到默认仓库 repo = p.baseDomain zap.L().Debug("fail back to default repo", zap.String("repo", repo)) } - returnMeta, err := p.ReturnMeta(owner, repo, branch, pathS[1:]) + returnMeta, err := p.ReturnMeta(owner, repo, branch, pathArr[1:]) if err != nil && !errors.Is(err, os.ErrNotExist) { return nil, err } else if err == nil { return returnMeta, nil } // 回退到默认页面 - return p.ReturnMeta(owner, repo, domain, pathS) + return p.ReturnMeta(owner, repo, domain, pathArr) } func (p *PageDomain) ReturnMeta(owner string, repo string, branch string, path []string) (*PageDomainContent, error) { rel := &PageDomainContent{} - if meta, err := p.GetMeta(p.baseDomain, owner, repo, branch); err == nil { + if meta, err := p.GetMeta(owner, repo, branch); err == nil { rel.PageMetaContent = meta rel.Owner = owner rel.Repo = repo @@ -86,13 +76,10 @@ func (p *PageDomain) ReturnMeta(owner string, repo string, branch string, path [ if strings.HasSuffix(rel.Path, "/") || rel.Path == "" { rel.Path = rel.Path + "index.html" } - if meta.Domain != "" { - err = p.alias.Bind(meta.Domain, rel.Owner, rel.Repo, branch) - if err != nil { - zap.L().Warn("别名绑定失败", zap.Error(err)) - } + if err = p.alias.Bind(meta.Alias, rel.Owner, rel.Repo, branch); err != nil { + zap.L().Warn("别名绑定失败", zap.Error(err)) + return nil, err } - return rel, nil } else { zap.L().Debug("查询错误", zap.Error(err)) diff --git a/pkg/core/meta.go b/pkg/core/meta.go index d5a2d7e..c9af074 100644 --- a/pkg/core/meta.go +++ b/pkg/core/meta.go @@ -5,17 +5,17 @@ import ( "fmt" "io" "net/http" + "net/url" "os" "regexp" "strings" "time" - "gopkg.d7z.net/gitea-pages/pkg/renders" + "go.uber.org/zap" + "gopkg.in/yaml.v3" "github.com/gobwas/glob" - "go.uber.org/zap" - "github.com/pkg/errors" "gopkg.d7z.net/gitea-pages/pkg/utils" @@ -26,8 +26,10 @@ var regexpHostname = regexp.MustCompile(`^(?:([a-z0-9-]+|\*)\.)?([a-z0-9-]{1,61} type ServerMeta struct { Backend + Domain string + client *http.Client - cache utils.Config + cache utils.KVConfig ttl time.Duration locker *utils.Locker @@ -35,17 +37,28 @@ type ServerMeta struct { type renderCompiler struct { regex glob.Glob - renders.Render + Render +} + +// PageConfig 配置 +type PageConfig struct { + Alias []string `yaml:"required"` // 重定向地址 + Renders map[string]string `yaml:"templates"` // 渲染器地址 + + VirtualRoute bool `yaml:"v-route"` // 是否使用虚拟路由(任何路径均使用 /index.html 返回 200 响应) + ReverseProxy map[string]string `yaml:"proxy"` // 反向代理路由 } type PageMetaContent struct { - CommitID string `json:"id"` // 提交 COMMIT ID - IsPage bool `json:"pg"` // 是否为 Page - Domain string `json:"domain"` // 匹配的域名 - HistoryRouteMode bool `json:"historyRouteMode"` // 路由模式 - CustomNotFound bool `json:"404"` // 注册了自定义 404 页面 - LastModified time.Time `json:"up"` // 上次更新时间 - Renders map[string][]string `json:"renders"` // 配置的渲染器 + CommitID string `json:"commit-id"` // 提交 COMMIT ID + LastModified time.Time `json:"last-modified"` // 上次更新时间 + IsPage bool `json:"is-page"` // 是否为 Page + ErrorMsg string `json:"error"` // 错误消息 + + VRoute bool `yaml:"v-route"` // 虚拟路由 + Alias []string `yaml:"aliases"` // 重定向 + Proxy map[string]string `yaml:"proxy"` // 反向代理 + Renders map[string][]string `json:"renders"` // 配置的渲染器 rendersL []*renderCompiler } @@ -57,14 +70,14 @@ func (m *PageMetaContent) From(data string) error { for _, g := range gs { m.rendersL = append(m.rendersL, &renderCompiler{ regex: glob.MustCompile(g), - Render: renders.GetRender(key), + Render: GetRender(key), }) } } return err } -func (m *PageMetaContent) TryRender(path ...string) renders.Render { +func (m *PageMetaContent) TryRender(path ...string) Render { for _, s := range path { for _, compiler := range m.rendersL { if compiler.regex.Match(s) { @@ -80,13 +93,15 @@ func (m *PageMetaContent) String() string { return string(marshal) } -func NewServerMeta(client *http.Client, backend Backend, config utils.Config, ttl time.Duration) *ServerMeta { - return &ServerMeta{backend, client, config, ttl, utils.NewLocker()} +func NewServerMeta(client *http.Client, backend Backend, kv utils.KVConfig, domain string, ttl time.Duration) *ServerMeta { + return &ServerMeta{backend, domain, client, kv, ttl, utils.NewLocker()} } -func (s *ServerMeta) GetMeta(baseDomain, owner, repo, branch string) (*PageMetaContent, error) { +func (s *ServerMeta) GetMeta(owner, repo, branch string) (*PageMetaContent, error) { rel := &PageMetaContent{ IsPage: false, + Proxy: make(map[string]string), + Alias: make([]string, 0), Renders: make(map[string][]string), } if repos, err := s.Repos(owner); err != nil { @@ -144,41 +159,67 @@ func (s *ServerMeta) GetMeta(baseDomain, owner, repo, branch string) (*PageMetaC } else { rel.IsPage = true } - rel.CustomNotFound, _ = s.FileExists(owner, repo, rel.CommitID, "404.html") - if cname, err := s.ReadString(owner, repo, rel.CommitID, "CNAME"); err == nil { - cname = strings.TrimSpace(cname) - if regexpHostname.MatchString(cname) && !strings.HasSuffix(strings.ToLower(cname), strings.ToLower(baseDomain)) { - rel.Domain = cname - } else { - zap.L().Debug("指定的 CNAME 不合法", zap.String("cname", cname)) - } + errFunc := func(err error) (*PageMetaContent, error) { + rel.IsPage = false + rel.ErrorMsg = err.Error() + _ = s.cache.Put(key, rel.String(), s.ttl) + return nil, err } - if r, err := s.ReadString(owner, repo, rel.CommitID, ".render"); err == nil { - for _, render := range strings.Split(r, "\n") { - render = strings.TrimSpace(render) - if strings.HasPrefix(render, "#") { - continue + + if data, err := s.ReadString(owner, repo, rel.CommitID, ".pages.yaml"); err == nil { + cfg := new(PageConfig) + if err = yaml.Unmarshal([]byte(data), cfg); err != nil { + return errFunc(err) + } + rel.Alias = cfg.Alias + rel.VRoute = cfg.VirtualRoute + for _, cname := range cfg.Alias { + cname = strings.TrimSpace(cname) + 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)) } - before, after, found := strings.Cut(render, " ") - before = strings.TrimSpace(before) - after = strings.TrimSpace(after) - if found { - if r := renders.GetRender(before); r != nil { - if g, err := glob.Compile(after); err == nil { - rel.Renders[before] = append(rel.Renders[before], after) + } + for sType, patterns := range cfg.Renders { + if r := GetRender(sType); r != nil { + for _, pattern := range strings.Split(patterns, ",") { + rel.Renders[sType] = append(rel.Renders[sType], pattern) + if g, err := glob.Compile(strings.TrimSpace(pattern)); err == nil { rel.rendersL = append(rel.rendersL, &renderCompiler{ regex: g, Render: r, }) + } else { + return errFunc(err) } } } - } + for path, backend := range cfg.ReverseProxy { + path = strings.TrimSpace(path) + path = strings.ReplaceAll(path, "//", "/") + path = strings.ReplaceAll(path, "//", "/") + if !strings.HasPrefix(path, "/") { + path = "/" + path + } + if strings.HasSuffix(path, "/") { + path = path[:len(path)-1] + } + var rUrl *url.URL + if rUrl, err = url.Parse(backend); err != nil { + return errFunc(err) + } + if rUrl.Scheme != "http" && rUrl.Scheme != "https" { + return errFunc(errors.New("invalid backend url " + backend)) + } + rel.Proxy[path] = rUrl.String() + } + } else { + // 不存在配置,但也可以重定向 + zap.L().Debug("failed to read meta data", zap.String("error", err.Error())) } - if find, _ := s.FileExists(owner, repo, rel.CommitID, ".history"); find { - rel.HistoryRouteMode = true - } + _ = s.cache.Put(key, rel.String(), s.ttl) return rel, nil } diff --git a/pkg/renders/render.go b/pkg/core/render.go similarity index 96% rename from pkg/renders/render.go rename to pkg/core/render.go index 7d2433e..73eab32 100644 --- a/pkg/renders/render.go +++ b/pkg/core/render.go @@ -1,4 +1,4 @@ -package renders +package core import ( "io" diff --git a/pkg/providers/gitea.go b/pkg/providers/gitea.go index 5dee36d..17313b8 100644 --- a/pkg/providers/gitea.go +++ b/pkg/providers/gitea.go @@ -122,3 +122,7 @@ func (g *ProviderGitea) Open(client *http.Client, owner, repo, commit, path stri req.Header.Add("Authorization", "token "+g.Token) return client.Do(req) } + +func (g *ProviderGitea) Close() error { + return nil +} diff --git a/pkg/renders/gotemplate.go b/pkg/renders/gotemplate.go index d4bf5da..d4803c2 100644 --- a/pkg/renders/gotemplate.go +++ b/pkg/renders/gotemplate.go @@ -5,13 +5,15 @@ import ( "io" "net/http" + "gopkg.d7z.net/gitea-pages/pkg/core" + "gopkg.d7z.net/gitea-pages/pkg/utils" ) type GoTemplate struct{} func init() { - RegisterRender("gotemplate", &GoTemplate{}) + core.RegisterRender("gotemplate", &GoTemplate{}) } func (g GoTemplate) Render(w http.ResponseWriter, r *http.Request, input io.Reader) error { diff --git a/pkg/server.go b/pkg/server.go index 20c2ad9..fd4ba14 100644 --- a/pkg/server.go +++ b/pkg/server.go @@ -5,8 +5,12 @@ import ( "io" "mime" "net/http" + "net/http/httputil" + "net/url" "os" "path/filepath" + "regexp" + "slices" "strconv" "strings" "time" @@ -19,19 +23,25 @@ import ( "github.com/pbnjay/memory" "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 DefaultBranch string - Config utils.Config - Cache utils.Cache + KVConfig utils.KVConfig + Cache utils.Cache MaxCacheSize int HttpClient *http.Client + MetaTTL time.Duration + DefaultErrorHandler func(w http.ResponseWriter, r *http.Request, err error) } @@ -40,10 +50,11 @@ func DefaultOptions(domain string) ServerOptions { return ServerOptions{ Domain: domain, DefaultBranch: "gh-pages", - Config: configMemory, + KVConfig: configMemory, Cache: utils.NewCacheMemory(1024*1024*10, int(memory.FreeMemory()/3*2)), MaxCacheSize: 1024 * 1024 * 10, HttpClient: http.DefaultClient, + MetaTTL: time.Minute, DefaultErrorHandler: func(w http.ResponseWriter, r *http.Request, err error) { if errors.Is(err, os.ErrNotExist) { http.Error(w, "page not found.", http.StatusNotFound) @@ -58,14 +69,16 @@ type Server struct { options *ServerOptions meta *core.PageDomain reader *core.CacheBackendBlobReader + backend core.Backend } func NewPageServer(backend core.Backend, options ServerOptions) *Server { - backend = core.NewCacheBackend(backend, options.Config, time.Minute) - svcMeta := core.NewServerMeta(options.HttpClient, backend, options.Config, time.Minute) - pageMeta := core.NewPageDomain(svcMeta, options.Config, options.Domain, options.DefaultBranch) + backend = core.NewCacheBackend(backend, options.KVConfig, options.MetaTTL) + svcMeta := core.NewServerMeta(options.HttpClient, backend, options.KVConfig, options.Domain, options.MetaTTL) + pageMeta := core.NewPageDomain(svcMeta, options.KVConfig, options.Domain, options.DefaultBranch) reader := core.NewCacheBackendBlobReader(options.HttpClient, backend, options.Cache, options.MaxCacheSize) return &Server{ + backend: backend, options: &options, meta: pageMeta, reader: reader, @@ -91,24 +104,43 @@ func (s *Server) ServeHTTP(writer http.ResponseWriter, request *http.Request) { } func (s *Server) Serve(writer http.ResponseWriter, request *http.Request) error { - if request.Method != "GET" { - return os.ErrNotExist - } - meta, err := s.meta.ParseDomainMeta(request.Host, request.URL.Path, request.URL.Query().Get("branch")) + domainHost := portExp.ReplaceAllString(strings.ToLower(request.Host), "") + meta, err := s.meta.ParseDomainMeta( + domainHost, + request.URL.Path, + request.URL.Query().Get("branch")) if err != nil { return err } zap.L().Debug("获取请求", zap.Any("request", meta.Path)) - // todo(feat) : 支持 http range - if meta.Domain != "" && meta.Domain != request.Host { - zap.L().Debug("重定向地址", zap.Any("src", request.Host), zap.Any("dst", meta.Domain)) - http.Redirect(writer, request, fmt.Sprintf("https://%s/%s", meta.Domain, meta.Path), http.StatusFound) + if len(meta.Alias) > 0 && !slices.Contains(meta.Alias, domainHost) { + zap.L().Debug("重定向地址", 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 } + for prefix, backend := range meta.Proxy { + if strings.HasPrefix(meta.Path, prefix) { + targetPath := strings.TrimPrefix(meta.Path, prefix) + if !strings.HasPrefix(targetPath, "/") { + targetPath = "/" + targetPath + } + zap.L().Debug("命中反向代理", zap.Any("prefix", prefix), zap.Any("backend", backend), + zap.Any("path", meta.Path), zap.Any("target", targetPath)) + request.URL.Path = targetPath + request.RequestURI = request.URL.RequestURI() + u, _ := url.Parse(backend) + httputil.NewSingleHostReverseProxy(u).ServeHTTP(writer, request) + return nil + } + } + // 如果不是反向代理路由则跳过任何配置 + if request.Method != "GET" { + return os.ErrNotExist + } result, err := s.reader.Open(meta.Owner, meta.Repo, meta.CommitID, meta.Path) if err != nil { if errors.Is(err, os.ErrNotExist) { - if meta.HistoryRouteMode { + if meta.VRoute { // 回退 abc => index.html result, err = s.reader.Open(meta.Owner, meta.Repo, meta.CommitID, "index.html") if err == nil { @@ -127,8 +159,7 @@ func (s *Server) Serve(writer http.ResponseWriter, request *http.Request) error } // 处理请求错误 if err != nil { - if meta.CustomNotFound && errors.Is(err, os.ErrNotExist) { - // 存在 404 页面的情况 + if errors.Is(err, os.ErrNotExist) { result, err = s.reader.Open(meta.Owner, meta.Repo, meta.CommitID, "404.html") if err != nil { return err @@ -185,11 +216,12 @@ func (s *Server) Serve(writer http.ResponseWriter, request *http.Request) error } func (s *Server) Close() error { - if err := s.options.Config.Close(); err != nil { + if err := s.options.KVConfig.Close(); err != nil { return err } if err := s.options.Cache.Close(); err != nil { return err } + return nil } diff --git a/pkg/utils/config.go b/pkg/utils/config.go index c7de0f1..433e7b8 100644 --- a/pkg/utils/config.go +++ b/pkg/utils/config.go @@ -12,7 +12,7 @@ import ( const TtlKeep = -1 -type Config interface { +type KVConfig interface { Put(key string, value string, ttl time.Duration) error Get(key string) (string, error) Delete(key string) error @@ -25,7 +25,7 @@ type ConfigMemory struct { store string } -func NewConfigMemory(store string) (Config, error) { +func NewConfigMemory(store string) (KVConfig, error) { ret := &ConfigMemory{ store: store, data: sync.Map{}, @@ -36,9 +36,11 @@ func NewConfigMemory(store string) (Config, error) { if err != nil && !os.IsNotExist(err) { return nil, err } - err = json.Unmarshal(data, &item) - if err != nil { - return nil, err + if err == nil { + err = json.Unmarshal(data, &item) + if err != nil { + return nil, err + } } for key, content := range item { if content.Ttl == nil || time.Now().Before(*content.Ttl) { diff --git a/tests/core/dummy.go b/tests/core/dummy.go new file mode 100644 index 0000000..e4b8d63 --- /dev/null +++ b/tests/core/dummy.go @@ -0,0 +1,80 @@ +package core + +import ( + "bytes" + "io" + "mime" + "net/http" + "net/http/httptest" + "os" + "path/filepath" + "strconv" + "time" + + "gopkg.d7z.net/gitea-pages/pkg/core" +) + +type ProviderDummy struct { + BaseDir string `yaml:"workdir"` +} + +func NewDummy() (*ProviderDummy, error) { + temp, err := os.MkdirTemp("", "dummy") + if err != nil { + return nil, err + } + return &ProviderDummy{ + BaseDir: temp, + }, nil +} + +func (p *ProviderDummy) Repos(owner string) (map[string]string, error) { + dir, err := os.ReadDir(filepath.Join(p.BaseDir, owner)) + if err != nil { + return nil, err + } + repos := make(map[string]string) + for _, d := range dir { + if d.IsDir() { + repos[d.Name()] = "main" + } + } + return repos, nil +} + +func (p *ProviderDummy) Branches(owner, repo string) (map[string]*core.BranchInfo, error) { + dir, err := os.ReadDir(filepath.Join(p.BaseDir, owner, repo)) + if err != nil { + return nil, err + } + branches := make(map[string]*core.BranchInfo) + for _, d := range dir { + if d.IsDir() { + branches[d.Name()] = &core.BranchInfo{ + ID: d.Name(), + LastModified: time.Time{}, + } + } + } + return branches, nil +} + +func (p *ProviderDummy) Open(_ *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 + } + all, err := io.ReadAll(open) + defer open.Close() + 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 +} + +func (p *ProviderDummy) Close() error { + return os.RemoveAll(p.BaseDir) +} diff --git a/tests/core/test.go b/tests/core/test.go new file mode 100644 index 0000000..5f26fa4 --- /dev/null +++ b/tests/core/test.go @@ -0,0 +1,78 @@ +package core + +import ( + "fmt" + "io" + "net/http" + "net/http/httptest" + "os" + "path/filepath" + + "go.uber.org/zap" + "gopkg.d7z.net/gitea-pages/pkg" +) + +type TestServer struct { + server *pkg.Server + dummy *ProviderDummy +} + +type SvcOpts func(options *pkg.ServerOptions) + +func NewDefaultTestServer() *TestServer { + return NewTestServer("example.com") +} + +func NewTestServer(domain string, opts ...SvcOpts) *TestServer { + atom := zap.NewAtomicLevel() + atom.SetLevel(zap.DebugLevel) + cfg := zap.NewProductionConfig() + cfg.Level = atom + logger, _ := cfg.Build() + zap.ReplaceGlobals(logger) + options := pkg.DefaultOptions(domain) + for _, opt := range opts { + opt(&options) + } + dummy, err := NewDummy() + if err != nil { + zap.S().Fatal(err) + } + + server := pkg.NewPageServer(dummy, options) + + return &TestServer{ + dummy: dummy, + server: server, + } +} + +func (t *TestServer) AddFile(path, data string) { + join := filepath.Join(t.dummy.BaseDir, path) + err := os.MkdirAll(filepath.Dir(join), 0o755) + if err != nil { + panic(err) + } + err = os.WriteFile(join, []byte(data), 0o644) + if err != nil { + panic(err) + } +} + +func (t *TestServer) OpenFile(url string) ([]byte, *http.Response, error) { + recorder := httptest.NewRecorder() + t.server.ServeHTTP(recorder, httptest.NewRequest("GET", url, nil)) + response := recorder.Result() + if response.Body != nil { + defer response.Body.Close() + } + if response.StatusCode != http.StatusOK { + return nil, response, fmt.Errorf(response.Status) + } + all, _ := io.ReadAll(response.Body) + return all, response, nil +} + +func (t *TestServer) Close() error { + return t.server.Close() +} diff --git a/tests/get_meta_test.go b/tests/get_meta_test.go new file mode 100644 index 0000000..21203ed --- /dev/null +++ b/tests/get_meta_test.go @@ -0,0 +1,18 @@ +package tests + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "gopkg.d7z.net/gitea-pages/tests/core" +) + +func Test_get_simple_html(t *testing.T) { + server := core.NewDefaultTestServer() + defer server.Close() + + server.AddFile("org1/repo1/gh-pages/index.html", "hello world") + data, _, err := server.OpenFile("https://org1.example.com/repo1/") + assert.NoError(t, err) + assert.Equal(t, "hello world", string(data)) +}