pages-server/forgejo/client.go
hazycora ce3a7dfa7f
All checks were successful
Deploy to VPS / build_site (push) Successful in 3s
re-fetch if value of file is suddenly empty
2024-04-05 22:11:37 -05:00

253 lines
6.1 KiB
Go

package forgejo
import (
"bytes"
"encoding/json"
"fmt"
"io"
"mime"
"net/http"
"net/url"
"os"
"path"
"strconv"
"time"
"code.gitea.io/sdk/gitea"
"github.com/rs/zerolog/log"
"git.gay/gitgay/pages/cache"
"git.gay/gitgay/pages/errors"
"git.gay/gitgay/pages/utils"
)
var (
FORGEJO_URL = os.Getenv("FORGEJO_URL")
FORGEJO_TOKEN = os.Getenv("FORGEJO_API_TOKEN")
FORGEJO_AUTH_HEADER = fmt.Sprintf("token %s", FORGEJO_TOKEN)
Client *gitea.Client
stdClient = http.Client{
Timeout: 10 * time.Second,
Transport: &http.Transport{
MaxIdleConns: 10,
IdleConnTimeout: 30 * time.Second,
DisableCompression: true,
},
}
repoCache = cache.NewRedisCache[*Repository]("repo", time.Minute*15)
fileCache = cache.NewRedisCache[[]byte]("file")
)
func init() {
var err error
Client, err = newClient()
if err != nil {
log.Fatal().Err(err)
}
}
func newClient() (*gitea.Client, error) {
options := []gitea.ClientOption{
gitea.SetHTTPClient(&stdClient),
}
if FORGEJO_TOKEN != "" {
options = append(options, gitea.SetToken(FORGEJO_TOKEN))
}
sdk, err := gitea.NewClient(FORGEJO_URL, options...)
return sdk, err
}
type Branch struct {
Name string
SHA string
}
type Repository struct {
Private bool
Owner string
Name string
DefaultBranch string
Branches []*Branch
}
type RefInfo struct {
Repository *Repository
SHA string
}
func GetRepo(owner string, repo string) (repoInfo *Repository, err error) {
repoInfo, err = repoCache.Get(fmt.Sprintf("%s:%s", owner, repo))
if err == nil {
return repoInfo, nil
}
repository, _, err := Client.GetRepo(owner, repo)
if err != nil {
errText := err.Error()
switch errText {
case "GetUserByName":
err = errors.NewErrorGetUserByName(owner)
}
return
}
branches, _, err := Client.ListRepoBranches(repository.Owner.UserName, repository.Name, gitea.ListRepoBranchesOptions{})
if err != nil {
return nil, err
}
repoInfo = &Repository{
Private: repository.Private,
Owner: repository.Owner.UserName,
Name: repository.Name,
DefaultBranch: repository.DefaultBranch,
Branches: make([]*Branch, len(branches)),
}
for i, branch := range branches {
repoInfo.Branches[i] = &Branch{
Name: branch.Name,
SHA: branch.Commit.ID,
}
}
err = repoCache.Set(fmt.Sprintf("%s:%s", owner, repo), repoInfo)
return
}
func getTreeResponse(owner string, repo string, ref string, page int) (tree *gitea.GitTreeResponse, err error) {
path := fmt.Sprintf("%s/api/v1/repos/%s/%s/git/trees/%s?recursive=true&page=%s", FORGEJO_URL, url.PathEscape(owner), url.PathEscape(repo), url.PathEscape(ref), strconv.Itoa(page))
req, err := http.NewRequest("GET", path, nil)
req.Header.Add("Authorization", FORGEJO_AUTH_HEADER)
resp, err := stdClient.Do(req)
if err != nil {
return
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
err = fmt.Errorf("unexpected status code: %d", resp.StatusCode)
return
}
bytes, err := io.ReadAll(resp.Body)
if err != nil {
return
}
tree = new(gitea.GitTreeResponse)
err = json.Unmarshal(bytes, tree)
if err != nil {
return
}
return
}
type TreeFile struct {
Type string `json:"type"`
Info *RefInfo `json:"info"`
Path string `json:"path"`
Size int64 `json:"size"`
SHA string `json:"sha"`
Mime string `json:"mime"`
}
func (f TreeFile) Bytes() ([]byte, error) {
return GetFile(f.Info.Repository.Owner, f.Info.Repository.Name, f.Info.SHA, f.Path)
}
func (f TreeFile) Reader() (io.ReadCloser, error) {
return GetFileReader(f.Info.Repository.Owner, f.Info.Repository.Name, f.Info.SHA, f.Path)
}
func GetTree(owner string, repo string, ref string) (files map[string]TreeFile, err error) {
info := &RefInfo{
Repository: &Repository{
Owner: owner,
Name: repo,
},
SHA: ref,
}
treeResponse, err := getTreeResponse(owner, repo, ref, 1)
if err != nil {
return
}
totalCount := treeResponse.TotalCount
handledCount := 0
files = make(map[string]TreeFile)
for _, entry := range treeResponse.Entries {
var mimeType string
if entry.Type == "blob" {
mimeType = mime.TypeByExtension(path.Ext(entry.Path))
}
file := TreeFile{
Info: info,
Type: entry.Type,
Path: entry.Path,
Size: entry.Size,
SHA: entry.SHA,
Mime: mimeType,
}
files[entry.Path] = file
}
handledCount += len(treeResponse.Entries)
page := treeResponse.Page
for handledCount < totalCount {
page++
treeResponse, err := getTreeResponse(owner, repo, ref, page)
if err != nil {
return nil, err
}
for _, entry := range treeResponse.Entries {
var mimeType string
if entry.Type == "blob" {
mimeType = mime.TypeByExtension(path.Ext(entry.Path))
}
file := TreeFile{
Info: info,
Type: entry.Type,
Path: entry.Path,
Size: entry.Size,
SHA: entry.SHA,
Mime: mimeType,
}
files[entry.Path] = file
}
handledCount += len(treeResponse.Entries)
}
return
}
func GetFile(owner string, repo string, ref string, filepath string) (bytes []byte, err error) {
bytes, _, err = Client.GetFile(owner, repo, ref, filepath)
return
}
func GetFileReader(owner string, repo string, ref string, filepath string) (reader io.ReadCloser, err error) {
fileCached, err := fileCache.Get(fmt.Sprintf("%s:%s:%s:%s", owner, repo, ref, filepath))
if err == nil {
if len(fileCached) != 0 {
reader = io.NopCloser(bytes.NewBuffer(fileCached))
return
} else {
log.Debug().Str("owner", owner).Str("repo", repo).Str("filepath", filepath).Msg("Cached file cleared? Re-fetching")
}
}
reader, resp, err := Client.GetFileReader(owner, repo, ref, filepath, true)
if err != nil {
return
}
if resp.StatusCode != http.StatusOK {
err = fmt.Errorf("unexpected status code: %d", resp.StatusCode)
return
}
if resp.ContentLength <= 1000000 {
dupBuffer := new(bytes.Buffer)
reader = utils.TeeReadCloser(reader, dupBuffer, func() (err error) {
go func() {
body := dupBuffer.Bytes()
if err == nil {
err = fileCache.Set(fmt.Sprintf("%s:%s:%s:%s", owner, repo, ref, filepath), body)
} else {
log.Err(err).Stack().Send()
}
}()
return
})
}
return
}