From 47a913d40d3417858f2ee51a7dbed64ca84eff60 Mon Sep 17 00:00:00 2001 From: KN4CK3R Date: Sat, 2 Mar 2024 18:02:01 +0100 Subject: [PATCH] Add support for API blob upload of release attachments (#29507) Fixes #29502 Our endpoint is not Github compatible. https://docs.github.com/en/rest/releases/assets?apiVersion=2022-11-28#upload-a-release-asset --------- Co-authored-by: Giteabot (cherry picked from commit 70c126e6184872a6ac63cae2f327fc745b25d1d7) --- routers/api/v1/repo/release_attachment.go | 41 ++++++++++---- services/attachment/attachment.go | 6 +- templates/swagger/v1_json.tmpl | 6 +- tests/integration/api_releases_test.go | 68 +++++++++++++++++------ 4 files changed, 88 insertions(+), 33 deletions(-) diff --git a/routers/api/v1/repo/release_attachment.go b/routers/api/v1/repo/release_attachment.go index a29bce66a4..59fd83e3a2 100644 --- a/routers/api/v1/repo/release_attachment.go +++ b/routers/api/v1/repo/release_attachment.go @@ -4,7 +4,9 @@ package repo import ( + "io" "net/http" + "strings" repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/modules/log" @@ -154,6 +156,7 @@ func CreateReleaseAttachment(ctx *context.APIContext) { // - application/json // consumes: // - multipart/form-data + // - application/octet-stream // parameters: // - name: owner // in: path @@ -180,7 +183,7 @@ func CreateReleaseAttachment(ctx *context.APIContext) { // in: formData // description: attachment to upload // type: file - // required: true + // required: false // responses: // "201": // "$ref": "#/responses/Attachment" @@ -202,20 +205,36 @@ func CreateReleaseAttachment(ctx *context.APIContext) { } // Get uploaded file from request - file, header, err := ctx.Req.FormFile("attachment") - if err != nil { - ctx.Error(http.StatusInternalServerError, "GetFile", err) - return - } - defer file.Close() + var content io.ReadCloser + var filename string + var size int64 = -1 - filename := header.Filename - if query := ctx.FormString("name"); query != "" { - filename = query + if strings.HasPrefix(strings.ToLower(ctx.Req.Header.Get("Content-Type")), "multipart/form-data") { + file, header, err := ctx.Req.FormFile("attachment") + if err != nil { + ctx.Error(http.StatusInternalServerError, "GetFile", err) + return + } + defer file.Close() + + content = file + size = header.Size + filename = header.Filename + if name := ctx.FormString("name"); name != "" { + filename = name + } + } else { + content = ctx.Req.Body + filename = ctx.FormString("name") + } + + if filename == "" { + ctx.Error(http.StatusBadRequest, "CreateReleaseAttachment", "Could not determine name of attachment.") + return } // Create a new attachment and save the file - attach, err := attachment.UploadAttachment(ctx, file, setting.Repository.Release.AllowedTypes, header.Size, &repo_model.Attachment{ + attach, err := attachment.UploadAttachment(ctx, content, setting.Repository.Release.AllowedTypes, size, &repo_model.Attachment{ Name: filename, UploaderID: ctx.Doer.ID, RepoID: ctx.Repo.Repository.ID, diff --git a/services/attachment/attachment.go b/services/attachment/attachment.go index 4d4fdd4f83..4481966b4a 100644 --- a/services/attachment/attachment.go +++ b/services/attachment/attachment.go @@ -44,14 +44,14 @@ func NewAttachment(ctx context.Context, attach *repo_model.Attachment, file io.R } // UploadAttachment upload new attachment into storage and update database -func UploadAttachment(ctx context.Context, file io.Reader, allowedTypes string, fileSize int64, opts *repo_model.Attachment) (*repo_model.Attachment, error) { +func UploadAttachment(ctx context.Context, file io.Reader, allowedTypes string, fileSize int64, attach *repo_model.Attachment) (*repo_model.Attachment, error) { buf := make([]byte, 1024) n, _ := util.ReadAtMost(file, buf) buf = buf[:n] - if err := upload.Verify(buf, opts.Name, allowedTypes); err != nil { + if err := upload.Verify(buf, attach.Name, allowedTypes); err != nil { return nil, err } - return NewAttachment(ctx, opts, io.MultiReader(bytes.NewReader(buf), file), fileSize) + return NewAttachment(ctx, attach, io.MultiReader(bytes.NewReader(buf), file), fileSize) } diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index ef292f2d65..c825afcc6f 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -12828,7 +12828,8 @@ }, "post": { "consumes": [ - "multipart/form-data" + "multipart/form-data", + "application/octet-stream" ], "produces": [ "application/json" @@ -12871,8 +12872,7 @@ "type": "file", "description": "attachment to upload", "name": "attachment", - "in": "formData", - "required": true + "in": "formData" } ], "responses": { diff --git a/tests/integration/api_releases_test.go b/tests/integration/api_releases_test.go index 5b1ab76ce9..49aa4c4e1b 100644 --- a/tests/integration/api_releases_test.go +++ b/tests/integration/api_releases_test.go @@ -262,24 +262,60 @@ func TestAPIUploadAssetRelease(t *testing.T) { filename := "image.png" buff := generateImg() - body := &bytes.Buffer{} - writer := multipart.NewWriter(body) - part, err := writer.CreateFormFile("attachment", filename) - assert.NoError(t, err) - _, err = io.Copy(part, &buff) - assert.NoError(t, err) - err = writer.Close() - assert.NoError(t, err) + assetURL := fmt.Sprintf("/api/v1/repos/%s/%s/releases/%d/assets", owner.Name, repo.Name, r.ID) - req := NewRequestWithBody(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/releases/%d/assets?name=test-asset", owner.Name, repo.Name, r.ID), body). - AddTokenAuth(token) - req.Header.Add("Content-Type", writer.FormDataContentType()) - resp := MakeRequest(t, req, http.StatusCreated) + t.Run("multipart/form-data", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() - var attachment *api.Attachment - DecodeJSON(t, resp, &attachment) + body := &bytes.Buffer{} - assert.EqualValues(t, "test-asset", attachment.Name) - assert.EqualValues(t, 104, attachment.Size) + writer := multipart.NewWriter(body) + part, err := writer.CreateFormFile("attachment", filename) + assert.NoError(t, err) + _, err = io.Copy(part, bytes.NewReader(buff.Bytes())) + assert.NoError(t, err) + err = writer.Close() + assert.NoError(t, err) + + req := NewRequestWithBody(t, http.MethodPost, assetURL, bytes.NewReader(body.Bytes())). + AddTokenAuth(token). + SetHeader("Content-Type", writer.FormDataContentType()) + resp := MakeRequest(t, req, http.StatusCreated) + + var attachment *api.Attachment + DecodeJSON(t, resp, &attachment) + + assert.EqualValues(t, filename, attachment.Name) + assert.EqualValues(t, 104, attachment.Size) + + req = NewRequestWithBody(t, http.MethodPost, assetURL+"?name=test-asset", bytes.NewReader(body.Bytes())). + AddTokenAuth(token). + SetHeader("Content-Type", writer.FormDataContentType()) + resp = MakeRequest(t, req, http.StatusCreated) + + var attachment2 *api.Attachment + DecodeJSON(t, resp, &attachment2) + + assert.EqualValues(t, "test-asset", attachment2.Name) + assert.EqualValues(t, 104, attachment2.Size) + }) + + t.Run("application/octet-stream", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + req := NewRequestWithBody(t, http.MethodPost, assetURL, bytes.NewReader(buff.Bytes())). + AddTokenAuth(token) + MakeRequest(t, req, http.StatusBadRequest) + + req = NewRequestWithBody(t, http.MethodPost, assetURL+"?name=stream.bin", bytes.NewReader(buff.Bytes())). + AddTokenAuth(token) + resp := MakeRequest(t, req, http.StatusCreated) + + var attachment *api.Attachment + DecodeJSON(t, resp, &attachment) + + assert.EqualValues(t, "stream.bin", attachment.Name) + assert.EqualValues(t, 104, attachment.Size) + }) }