feat(releases): add release attachment upload
Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
parent
b9962f2412
commit
e9faaabc8a
4 changed files with 196 additions and 8 deletions
21
client.go
21
client.go
|
|
@ -262,7 +262,7 @@ func (c *Client) postRawText(ctx context.Context, path, body string) ([]byte, er
|
|||
return data, nil
|
||||
}
|
||||
|
||||
func (c *Client) postMultipartJSON(ctx context.Context, path string, query map[string]string, fieldName, fileName string, content io.Reader, out any) error {
|
||||
func (c *Client) postMultipartJSON(ctx context.Context, path string, query map[string]string, fields map[string]string, fieldName, fileName string, content io.Reader, out any) error {
|
||||
target, err := url.Parse(c.baseURL + path)
|
||||
if err != nil {
|
||||
return core.E("Client.PostMultipart", "forge: parse url", err)
|
||||
|
|
@ -277,13 +277,20 @@ func (c *Client) postMultipartJSON(ctx context.Context, path string, query map[s
|
|||
|
||||
var body bytes.Buffer
|
||||
writer := multipart.NewWriter(&body)
|
||||
part, err := writer.CreateFormFile(fieldName, fileName)
|
||||
if err != nil {
|
||||
return core.E("Client.PostMultipart", "forge: create multipart form file", err)
|
||||
for key, value := range fields {
|
||||
if err := writer.WriteField(key, value); err != nil {
|
||||
return core.E("Client.PostMultipart", "forge: create multipart form field", err)
|
||||
}
|
||||
}
|
||||
if content != nil {
|
||||
if _, err := io.Copy(part, content); err != nil {
|
||||
return core.E("Client.PostMultipart", "forge: write multipart form file", err)
|
||||
if fieldName != "" {
|
||||
part, err := writer.CreateFormFile(fieldName, fileName)
|
||||
if err != nil {
|
||||
return core.E("Client.PostMultipart", "forge: create multipart form file", err)
|
||||
}
|
||||
if content != nil {
|
||||
if _, err := io.Copy(part, content); err != nil {
|
||||
return core.E("Client.PostMultipart", "forge: write multipart form file", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
if err := writer.Close(); err != nil {
|
||||
|
|
|
|||
|
|
@ -273,7 +273,7 @@ func attachmentUploadQuery(opts *AttachmentUploadOptions) map[string]string {
|
|||
|
||||
func (s *IssueService) createAttachment(ctx context.Context, path string, opts *AttachmentUploadOptions, filename string, content io.Reader) (*types.Attachment, error) {
|
||||
var out types.Attachment
|
||||
if err := s.client.postMultipartJSON(ctx, path, attachmentUploadQuery(opts), "attachment", filename, content, &out); err != nil {
|
||||
if err := s.client.postMultipartJSON(ctx, path, attachmentUploadQuery(opts), nil, "attachment", filename, content, &out); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &out, nil
|
||||
|
|
|
|||
44
releases.go
44
releases.go
|
|
@ -2,6 +2,7 @@ package forge
|
|||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"iter"
|
||||
|
||||
"dappco.re/go/core/forge/types"
|
||||
|
|
@ -17,6 +18,23 @@ type ReleaseService struct {
|
|||
Resource[types.Release, types.CreateReleaseOption, types.EditReleaseOption]
|
||||
}
|
||||
|
||||
// ReleaseAttachmentUploadOptions controls metadata sent when uploading a release attachment.
|
||||
type ReleaseAttachmentUploadOptions struct {
|
||||
Name string
|
||||
ExternalURL string
|
||||
}
|
||||
|
||||
func releaseAttachmentUploadQuery(opts *ReleaseAttachmentUploadOptions) map[string]string {
|
||||
if opts == nil || opts.Name == "" {
|
||||
return nil
|
||||
}
|
||||
query := make(map[string]string, 1)
|
||||
if opts.Name != "" {
|
||||
query["name"] = opts.Name
|
||||
}
|
||||
return query
|
||||
}
|
||||
|
||||
func newReleaseService(c *Client) *ReleaseService {
|
||||
return &ReleaseService{
|
||||
Resource: *NewResource[types.Release, types.CreateReleaseOption, types.EditReleaseOption](
|
||||
|
|
@ -47,6 +65,32 @@ func (s *ReleaseService) ListAssets(ctx context.Context, owner, repo string, rel
|
|||
return ListAll[types.Attachment](ctx, s.client, path, nil)
|
||||
}
|
||||
|
||||
// CreateAttachment uploads a new attachment to a release.
|
||||
//
|
||||
// If opts.ExternalURL is set, the upload uses the external_url form field and
|
||||
// ignores filename/content.
|
||||
func (s *ReleaseService) CreateAttachment(ctx context.Context, owner, repo string, releaseID int64, opts *ReleaseAttachmentUploadOptions, filename string, content io.Reader) (*types.Attachment, error) {
|
||||
path := ResolvePath("/api/v1/repos/{owner}/{repo}/releases/{releaseID}/assets", pathParams("owner", owner, "repo", repo, "releaseID", int64String(releaseID)))
|
||||
fields := make(map[string]string, 1)
|
||||
fieldName := "attachment"
|
||||
if opts != nil && opts.ExternalURL != "" {
|
||||
fields["external_url"] = opts.ExternalURL
|
||||
fieldName = ""
|
||||
filename = ""
|
||||
content = nil
|
||||
}
|
||||
var out types.Attachment
|
||||
if err := s.client.postMultipartJSON(ctx, path, releaseAttachmentUploadQuery(opts), fields, fieldName, filename, content, &out); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &out, nil
|
||||
}
|
||||
|
||||
// CreateAsset uploads a new asset to a release.
|
||||
func (s *ReleaseService) CreateAsset(ctx context.Context, owner, repo string, releaseID int64, opts *ReleaseAttachmentUploadOptions, filename string, content io.Reader) (*types.Attachment, error) {
|
||||
return s.CreateAttachment(ctx, owner, repo, releaseID, opts, filename, content)
|
||||
}
|
||||
|
||||
// IterAssets returns an iterator over all assets for a release.
|
||||
func (s *ReleaseService) IterAssets(ctx context.Context, owner, repo string, releaseID int64) iter.Seq2[types.Attachment, error] {
|
||||
path := ResolvePath("/api/v1/repos/{owner}/{repo}/releases/{releaseID}/assets", pathParams("owner", owner, "repo", repo, "releaseID", int64String(releaseID)))
|
||||
|
|
|
|||
137
releases_test.go
137
releases_test.go
|
|
@ -1,15 +1,63 @@
|
|||
package forge
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
json "github.com/goccy/go-json"
|
||||
"io"
|
||||
"mime"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"dappco.re/go/core/forge/types"
|
||||
)
|
||||
|
||||
func readMultipartReleaseAttachment(t *testing.T, r *http.Request) (map[string]string, string, string) {
|
||||
t.Helper()
|
||||
|
||||
mediaType, params, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if mediaType != "multipart/form-data" {
|
||||
t.Fatalf("got content-type=%q", mediaType)
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
fields := make(map[string]string)
|
||||
reader := multipart.NewReader(bytes.NewReader(body), params["boundary"])
|
||||
var fileName string
|
||||
var fileContent string
|
||||
for {
|
||||
part, err := reader.NextPart()
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
data, err := io.ReadAll(part)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if part.FormName() == "attachment" {
|
||||
fileName = part.FileName()
|
||||
fileContent = string(data)
|
||||
continue
|
||||
}
|
||||
fields[part.FormName()] = string(data)
|
||||
}
|
||||
|
||||
return fields, fileName, fileContent
|
||||
}
|
||||
|
||||
func TestReleaseService_List_Good(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
|
|
@ -85,3 +133,92 @@ func TestReleaseService_GetByTag_Good(t *testing.T) {
|
|||
t.Errorf("got id=%d, want 1", release.ID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReleaseService_CreateAttachment_Good(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
t.Errorf("expected POST, got %s", r.Method)
|
||||
}
|
||||
if r.URL.Path != "/api/v1/repos/core/go-forge/releases/1/assets" {
|
||||
t.Errorf("wrong path: %s", r.URL.Path)
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
if got := r.URL.Query().Get("name"); got != "linux-amd64" {
|
||||
t.Fatalf("got name=%q", got)
|
||||
}
|
||||
fields, filename, content := readMultipartReleaseAttachment(t, r)
|
||||
if !reflect.DeepEqual(fields, map[string]string{}) {
|
||||
t.Fatalf("got fields=%#v", fields)
|
||||
}
|
||||
if filename != "release.tar.gz" {
|
||||
t.Fatalf("got filename=%q", filename)
|
||||
}
|
||||
if content != "release bytes" {
|
||||
t.Fatalf("got content=%q", content)
|
||||
}
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
json.NewEncoder(w).Encode(types.Attachment{ID: 9, Name: filename})
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
f := NewForge(srv.URL, "tok")
|
||||
attachment, err := f.Releases.CreateAttachment(
|
||||
context.Background(),
|
||||
"core",
|
||||
"go-forge",
|
||||
1,
|
||||
&ReleaseAttachmentUploadOptions{Name: "linux-amd64"},
|
||||
"release.tar.gz",
|
||||
bytes.NewBufferString("release bytes"),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if attachment.Name != "release.tar.gz" {
|
||||
t.Fatalf("got name=%q", attachment.Name)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReleaseService_CreateAttachmentExternalURL_Good(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
t.Errorf("expected POST, got %s", r.Method)
|
||||
}
|
||||
if r.URL.Path != "/api/v1/repos/core/go-forge/releases/1/assets" {
|
||||
t.Errorf("wrong path: %s", r.URL.Path)
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
if got := r.URL.Query().Get("name"); got != "docs" {
|
||||
t.Fatalf("got name=%q", got)
|
||||
}
|
||||
fields, filename, content := readMultipartReleaseAttachment(t, r)
|
||||
if !reflect.DeepEqual(fields, map[string]string{"external_url": "https://example.com/release.tar.gz"}) {
|
||||
t.Fatalf("got fields=%#v", fields)
|
||||
}
|
||||
if filename != "" || content != "" {
|
||||
t.Fatalf("unexpected file upload: filename=%q content=%q", filename, content)
|
||||
}
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
json.NewEncoder(w).Encode(types.Attachment{ID: 10, Name: "docs"})
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
f := NewForge(srv.URL, "tok")
|
||||
attachment, err := f.Releases.CreateAttachment(
|
||||
context.Background(),
|
||||
"core",
|
||||
"go-forge",
|
||||
1,
|
||||
&ReleaseAttachmentUploadOptions{Name: "docs", ExternalURL: "https://example.com/release.tar.gz"},
|
||||
"",
|
||||
nil,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if attachment.Name != "docs" {
|
||||
t.Fatalf("got name=%q", attachment.Name)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue