Add Gitea read operations to the forge boundary
This commit is contained in:
164
internal/gitea/backend.go
Normal file
164
internal/gitea/backend.go
Normal file
@@ -0,0 +1,164 @@
|
||||
package gitea
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"go.maximumdirect.net/releasekit/internal/forge"
|
||||
)
|
||||
|
||||
// Backend adapts a configured Gitea destination to the hosted delivery
|
||||
// operations. Its destination and client are private so callers cannot bypass
|
||||
// Gitea's validation or replace opaque handles.
|
||||
type Backend struct {
|
||||
destination Destination
|
||||
client Client
|
||||
}
|
||||
|
||||
// NewBackend creates a Gitea backend with an optional HTTP client.
|
||||
func NewBackend(destination Destination, client *http.Client) *Backend {
|
||||
return &Backend{destination: destination, client: Client{HTTPClient: client}}
|
||||
}
|
||||
|
||||
type releaseHandle struct {
|
||||
id int64
|
||||
}
|
||||
|
||||
type assetHandle struct {
|
||||
id int64
|
||||
downloadURL string
|
||||
}
|
||||
|
||||
func (b *Backend) TagCommit(ctx context.Context, tag string) (string, error) {
|
||||
if strings.TrimSpace(tag) == "" {
|
||||
return "", &forge.Failure{Kind: forge.FailureInvalid, Err: errors.New("full tag is required")}
|
||||
}
|
||||
_, apiRoot, err := b.destinationAPI("get hosted tag")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
var observed tagResponse
|
||||
endpoint := apiRoot + "/tags/" + url.PathEscape(tag)
|
||||
if err := b.client.getJSON(ctx, b.client.httpClient(), b.destination.Token, "get hosted tag", endpoint, &observed); err != nil {
|
||||
return "", neutralFailure(err)
|
||||
}
|
||||
if observed.Commit.SHA == "" {
|
||||
return "", &forge.Failure{Kind: forge.FailureMalformed, Err: errors.New("tag response does not identify a commit")}
|
||||
}
|
||||
return observed.Commit.SHA, nil
|
||||
}
|
||||
|
||||
func (b *Backend) FindRelease(ctx context.Context, tag string) (forge.Release, error) {
|
||||
if strings.TrimSpace(tag) == "" {
|
||||
return forge.Release{}, &forge.Failure{Kind: forge.FailureInvalid, Err: errors.New("full tag is required")}
|
||||
}
|
||||
_, apiRoot, err := b.destinationAPI("find release")
|
||||
if err != nil {
|
||||
return forge.Release{}, err
|
||||
}
|
||||
endpoint := apiRoot + "/releases/tags/" + url.PathEscape(tag)
|
||||
release, _, err := b.client.getRelease(ctx, b.client.httpClient(), b.destination.Token, endpoint)
|
||||
if err != nil {
|
||||
return forge.Release{}, neutralFailure(err)
|
||||
}
|
||||
return forge.Release{
|
||||
URL: release.URL,
|
||||
Tag: release.TagName,
|
||||
Title: release.Title,
|
||||
Body: release.Body,
|
||||
Draft: release.Draft,
|
||||
Prerelease: release.Prerelease,
|
||||
Handle: releaseHandle{id: release.ID},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (b *Backend) ListAssets(ctx context.Context, handle any) ([]forge.Asset, error) {
|
||||
release, ok := handle.(releaseHandle)
|
||||
if !ok {
|
||||
return nil, &forge.Failure{Kind: forge.FailureInvalid, Err: errors.New("release handle was not created by the Gitea backend")}
|
||||
}
|
||||
_, apiRoot, err := b.destinationAPI("list release assets")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
remote, err := b.client.listAssets(ctx, b.client.httpClient(), b.destination.Token, release.id, apiRoot)
|
||||
if err != nil {
|
||||
return nil, neutralFailure(err)
|
||||
}
|
||||
assets := make([]forge.Asset, 0, len(remote))
|
||||
for _, item := range remote {
|
||||
assets = append(assets, forge.Asset{
|
||||
Filename: item.Filename,
|
||||
Size: item.Size,
|
||||
Handle: assetHandle{id: item.ID, downloadURL: item.DownloadURL},
|
||||
})
|
||||
}
|
||||
return assets, nil
|
||||
}
|
||||
|
||||
func (b *Backend) OpenAsset(ctx context.Context, handle any) (io.ReadCloser, error) {
|
||||
asset, ok := handle.(assetHandle)
|
||||
if !ok {
|
||||
return nil, &forge.Failure{Kind: forge.FailureInvalid, Err: errors.New("asset handle was not created by the Gitea backend")}
|
||||
}
|
||||
base, _, err := b.destinationAPI("download release asset")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
stream, err := b.client.openAsset(ctx, b.client.httpClient(), base, b.destination.Token, asset.downloadURL)
|
||||
if err != nil {
|
||||
return nil, neutralFailure(err)
|
||||
}
|
||||
return stream, nil
|
||||
}
|
||||
|
||||
func (b *Backend) destinationAPI(operation string) (*url.URL, string, error) {
|
||||
base, err := parseBaseURL(b.destination.BaseURL)
|
||||
if err != nil {
|
||||
return nil, "", neutralFailure(failure(FailureInvalid, operation, 0, err))
|
||||
}
|
||||
if strings.TrimSpace(b.destination.Token) == "" {
|
||||
return nil, "", neutralFailure(failure(FailureInvalid, operation, 0, errors.New("Gitea token is required")))
|
||||
}
|
||||
if strings.TrimSpace(b.destination.Owner) == "" || strings.ContainsAny(b.destination.Owner, "/\\") {
|
||||
return nil, "", neutralFailure(failure(FailureInvalid, operation, 0, errors.New("Gitea owner must be one path segment")))
|
||||
}
|
||||
if strings.TrimSpace(b.destination.Repository) == "" || strings.ContainsAny(b.destination.Repository, "/\\") {
|
||||
return nil, "", neutralFailure(failure(FailureInvalid, operation, 0, errors.New("Gitea repository must be one path segment")))
|
||||
}
|
||||
apiRoot := strings.TrimRight(base.String(), "/") + "/api/v1/repos/" + url.PathEscape(b.destination.Owner) + "/" + url.PathEscape(b.destination.Repository)
|
||||
return base, apiRoot, nil
|
||||
}
|
||||
|
||||
func neutralFailure(err error) error {
|
||||
var giteaFailure *Failure
|
||||
if !errors.As(err, &giteaFailure) {
|
||||
return err
|
||||
}
|
||||
kind := forge.FailureUnknown
|
||||
switch giteaFailure.Kind {
|
||||
case FailureInvalid:
|
||||
kind = forge.FailureInvalid
|
||||
case FailureNotFound:
|
||||
kind = forge.FailureNotFound
|
||||
case FailureUnauthorized:
|
||||
kind = forge.FailureUnauthorized
|
||||
case FailureConflict:
|
||||
kind = forge.FailureConflict
|
||||
case FailureMalformed:
|
||||
kind = forge.FailureMalformed
|
||||
case FailureUnverifiable:
|
||||
kind = forge.FailureUnverifiable
|
||||
case FailureTransport:
|
||||
kind = forge.FailureTransport
|
||||
case FailureRejected:
|
||||
kind = forge.FailureRejected
|
||||
case FailureUnknown:
|
||||
kind = forge.FailureUnknown
|
||||
}
|
||||
return &forge.Failure{Kind: kind, Err: err}
|
||||
}
|
||||
Reference in New Issue
Block a user