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 = ` 404 - Not Found

404

The page you're looking for could not be found.

` 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 } res, err := c.getRawFileOrLFS(owner, repo, filepath, ref) if err == nil { if strings.HasSuffix(filepath, ".md") { res, err = handleMD(res) if err != nil { return nil, err } } return &openFile{ content: res, name: filepath, }, nil } // If file not found, try as directory entries, err := c.getDirectoryContents(owner, repo, filepath, ref) if err == nil { // Check if this is a directory and the request doesn't end with / // If so, look for index.html in this directory if !strings.HasSuffix(filepath, "/") { indexContent, err := c.getRawFileOrLFS(owner, repo, filepath+"/index.html", ref) if err == nil { return &openFile{ content: indexContent, name: "index.html", }, nil } } return &openDir{ entries: entries, name: filepath, }, nil } // Neither file nor directory found return c.serveNotFound(owner, repo), 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("\n\n\n

"), []byte(title)...) res = append(res, []byte("

")...) res = append(res, resmd...) res = append(res, []byte("")...) 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 }