364 lines
7.4 KiB
Go
364 lines
7.4 KiB
Go
package gitea
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"io"
|
|
"io/fs"
|
|
"net/http"
|
|
"net/url"
|
|
"strings"
|
|
"sync"
|
|
|
|
gclient "code.gitea.io/sdk/gitea"
|
|
"github.com/spf13/viper"
|
|
)
|
|
|
|
type Client struct {
|
|
serverURL string
|
|
token string
|
|
giteapages string
|
|
giteapagesAllowAll string
|
|
gc *gclient.Client
|
|
}
|
|
|
|
type dirEntry struct {
|
|
name string
|
|
isDir bool
|
|
content []byte
|
|
}
|
|
|
|
func (d *dirEntry) Name() string { return d.name }
|
|
func (d *dirEntry) IsDir() bool { return d.isDir }
|
|
func (d *dirEntry) Type() fs.FileMode { return fs.ModePerm }
|
|
func (d *dirEntry) Info() (fs.FileInfo, error) { return nil, fs.ErrNotExist }
|
|
|
|
type openDir struct {
|
|
entries []fs.DirEntry
|
|
name string
|
|
pos int
|
|
}
|
|
|
|
func (d *openDir) Read([]byte) (int, error) {
|
|
return 0, fmt.Errorf("cannot Read() a directory")
|
|
}
|
|
|
|
func (d *openDir) Close() error {
|
|
return nil
|
|
}
|
|
|
|
func (d *openDir) Stat() (fs.FileInfo, error) {
|
|
return nil, fs.ErrNotExist
|
|
}
|
|
|
|
func (d *openDir) ReadDir(count int) ([]fs.DirEntry, error) {
|
|
n := len(d.entries) - d.pos
|
|
if count > 0 && n > count {
|
|
n = count
|
|
}
|
|
if n == 0 {
|
|
if count <= 0 {
|
|
return nil, nil
|
|
}
|
|
return nil, io.EOF
|
|
}
|
|
entries := d.entries[d.pos : d.pos+n]
|
|
d.pos += n
|
|
return entries, nil
|
|
}
|
|
|
|
func (d *openDir) ReadDirFile(count int) ([]fs.DirEntry, error) {
|
|
return d.ReadDir(count)
|
|
}
|
|
|
|
func NewClient(serverURL, token, giteapages, giteapagesAllowAll string) (*Client, error) {
|
|
if giteapages == "" {
|
|
giteapages = "gitea-pages"
|
|
}
|
|
|
|
if giteapagesAllowAll == "" {
|
|
giteapagesAllowAll = "gitea-pages-allowall"
|
|
}
|
|
|
|
gc, err := gclient.NewClient(serverURL, gclient.SetToken(token), gclient.SetGiteaVersion(""))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &Client{
|
|
serverURL: serverURL,
|
|
token: token,
|
|
gc: gc,
|
|
giteapages: giteapages,
|
|
giteapagesAllowAll: giteapagesAllowAll,
|
|
}, nil
|
|
}
|
|
|
|
const defaultNotFoundPage = `<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<title>404 - Not Found</title>
|
|
<style>
|
|
body { font-family: sans-serif; text-align: center; padding: 50px; }
|
|
h1 { font-size: 48px; margin-bottom: 20px; }
|
|
p { color: #666; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<h1>404</h1>
|
|
<p>The page you're looking for could not be found.</p>
|
|
</body>
|
|
</html>`
|
|
|
|
func (c *Client) serveNotFound(owner, repo string) fs.File {
|
|
if repo != "" {
|
|
custom404, err := c.getRawFileOrLFS(owner, repo, "404.html", "main")
|
|
if err == nil {
|
|
return &openFile{
|
|
content: custom404,
|
|
name: "404.html",
|
|
}
|
|
}
|
|
}
|
|
|
|
return &openFile{
|
|
content: []byte(defaultNotFoundPage),
|
|
name: "404.html",
|
|
}
|
|
}
|
|
|
|
func (c *Client) Open(name, ref string) (fs.File, error) {
|
|
owner, repo, filepath := splitName(name)
|
|
|
|
if repo == "" {
|
|
repo = owner + ".fluffy.pw"
|
|
filepath = "index.html"
|
|
}
|
|
|
|
isFluffyPagesRepo := strings.HasSuffix(repo, ".fluffy.pw")
|
|
|
|
_, resp, err := c.gc.GetRepo(owner, repo)
|
|
if err != nil {
|
|
if resp != nil && resp.StatusCode == http.StatusNotFound {
|
|
return c.serveNotFound(owner, repo), nil
|
|
}
|
|
return nil, fs.ErrNotExist
|
|
}
|
|
|
|
limited, allowall := c.allowsPages(owner, repo)
|
|
if !limited && !allowall && !isFluffyPagesRepo {
|
|
return c.serveNotFound(owner, repo), nil
|
|
}
|
|
|
|
hasConfig := true
|
|
if err := c.readConfig(owner, repo); err != nil {
|
|
if !isFluffyPagesRepo && !allowall {
|
|
return c.serveNotFound(owner, repo), nil
|
|
}
|
|
hasConfig = false
|
|
}
|
|
|
|
if !hasConfig && !validRefs(ref, allowall || isFluffyPagesRepo) {
|
|
return c.serveNotFound(owner, repo), nil
|
|
}
|
|
|
|
entries, err := c.getDirectoryContents(owner, repo, filepath, ref)
|
|
if err == nil {
|
|
return &openDir{
|
|
entries: entries,
|
|
name: filepath,
|
|
}, nil
|
|
}
|
|
|
|
res, err := c.getRawFileOrLFS(owner, repo, filepath, ref)
|
|
if err != nil {
|
|
if err == fs.ErrNotExist {
|
|
return c.serveNotFound(owner, repo), nil
|
|
}
|
|
return nil, err
|
|
}
|
|
|
|
if strings.HasSuffix(filepath, ".md") {
|
|
res, err = handleMD(res)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
return &openFile{
|
|
content: res,
|
|
name: filepath,
|
|
}, nil
|
|
}
|
|
|
|
func (c *Client) getRawFileOrLFS(owner, repo, filepath, ref string) ([]byte, error) {
|
|
var (
|
|
giteaURL string
|
|
err error
|
|
)
|
|
|
|
// TODO: make pr for go-sdk
|
|
// gitea sdk doesn't support "media" type for lfs/non-lfs
|
|
giteaURL, err = url.JoinPath(c.serverURL+"/api/v1/repos/", owner, repo, "media", filepath)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
giteaURL += "?ref=" + url.QueryEscape(ref)
|
|
|
|
req, err := http.NewRequest(http.MethodGet, giteaURL, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
req.Header.Add("Authorization", "token "+c.token)
|
|
|
|
resp, err := http.DefaultClient.Do(req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
switch resp.StatusCode {
|
|
case http.StatusNotFound:
|
|
return nil, fs.ErrNotExist
|
|
case http.StatusOK:
|
|
default:
|
|
return nil, fmt.Errorf("unexpected status code '%d'", resp.StatusCode)
|
|
}
|
|
|
|
res, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
defer resp.Body.Close()
|
|
|
|
return res, nil
|
|
}
|
|
|
|
var bufPool = sync.Pool{
|
|
New: func() any {
|
|
return new(bytes.Buffer)
|
|
},
|
|
}
|
|
|
|
func handleMD(res []byte) ([]byte, error) {
|
|
meta, resbody, err := extractFrontMatter(string(res))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
resmd, err := markdown([]byte(resbody))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
title, _ := meta["title"].(string)
|
|
|
|
res = append([]byte("<!DOCTYPE html>\n<html>\n<body>\n<h1>"), []byte(title)...)
|
|
res = append(res, []byte("</h1>")...)
|
|
res = append(res, resmd...)
|
|
res = append(res, []byte("</body></html>")...)
|
|
|
|
return res, nil
|
|
}
|
|
|
|
func (c *Client) repoTopics(owner, repo string) ([]string, error) {
|
|
repos, _, err := c.gc.ListRepoTopics(owner, repo, gclient.ListRepoTopicsOptions{})
|
|
return repos, err
|
|
}
|
|
|
|
func (c *Client) hasRepoBranch(owner, repo, branch string) bool {
|
|
b, _, err := c.gc.GetRepoBranch(owner, repo, branch)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
|
|
return b.Name == branch
|
|
}
|
|
|
|
func (c *Client) allowsPages(owner, repo string) (bool, bool) {
|
|
if strings.HasSuffix(repo, ".fluffy.pw") {
|
|
return true, true
|
|
}
|
|
|
|
topics, err := c.repoTopics(owner, repo)
|
|
if err != nil {
|
|
return false, false
|
|
}
|
|
|
|
for _, topic := range topics {
|
|
if topic == c.giteapagesAllowAll {
|
|
return true, true
|
|
}
|
|
}
|
|
|
|
for _, topic := range topics {
|
|
if topic == c.giteapages {
|
|
return true, false
|
|
}
|
|
}
|
|
|
|
return false, false
|
|
}
|
|
|
|
func (c *Client) readConfig(owner, repo string) error {
|
|
cfg, err := c.getRawFileOrLFS(owner, repo, c.giteapages+".toml", c.giteapages)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
viper.SetConfigType("toml")
|
|
|
|
return viper.ReadConfig(bytes.NewBuffer(cfg))
|
|
}
|
|
|
|
func splitName(name string) (string, string, string) {
|
|
parts := strings.Split(name, "/")
|
|
|
|
// parts contains: ["owner", "repo", "filepath"]
|
|
switch len(parts) {
|
|
case 1:
|
|
return parts[0], "", ""
|
|
case 2:
|
|
return parts[0], parts[1], ""
|
|
default:
|
|
return parts[0], parts[1], strings.Join(parts[2:], "/")
|
|
}
|
|
}
|
|
|
|
func validRefs(ref string, allowall bool) bool {
|
|
if allowall {
|
|
return true
|
|
}
|
|
|
|
validrefs := viper.GetStringSlice("allowedrefs")
|
|
for _, r := range validrefs {
|
|
if r == ref {
|
|
return true
|
|
}
|
|
|
|
if r == "*" {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
func (c *Client) getDirectoryContents(owner, repo, dirPath, ref string) ([]fs.DirEntry, error) {
|
|
entries, _, err := c.gc.ListContents(owner, repo, ref, dirPath)
|
|
if err != nil {
|
|
return nil, fs.ErrNotExist
|
|
}
|
|
|
|
var result []fs.DirEntry
|
|
for _, entry := range entries {
|
|
result = append(result, &dirEntry{
|
|
name: entry.Name,
|
|
isDir: entry.Type == "dir",
|
|
})
|
|
}
|
|
|
|
return result, nil
|
|
}
|