253 lines
6.1 KiB
Go
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
|
|
}
|