diff --git a/pkg/connectors/microcks_client.go b/pkg/connectors/microcks_client.go index f76884b..f06b513 100644 --- a/pkg/connectors/microcks_client.go +++ b/pkg/connectors/microcks_client.go @@ -425,31 +425,41 @@ func (c *microcksClient) UploadArtifact(specificationFilePath string, mainArtifa } defer file.Close() - // Create a multipart request body, reading the file. - body := &bytes.Buffer{} - writer := multipart.NewWriter(body) - part, err := writer.CreateFormFile("file", filepath.Base(specificationFilePath)) - if err != nil { - return "", err - } - _, err = io.Copy(part, file) - if err != nil { - panic(err.Error()) - } + // Use io.Pipe to stream the multipart form data directly to the HTTP + // request without buffering the entire file in memory. + pr, pw := io.Pipe() + writer := multipart.NewWriter(pw) + + // Write the multipart form data in a background goroutine so the pipe + // reader can be consumed concurrently by the HTTP request. + errCh := make(chan error, 1) + go func() { + defer pw.Close() + + part, err := writer.CreateFormFile("file", filepath.Base(specificationFilePath)) + if err != nil { + errCh <- err + return + } + if _, err = io.Copy(part, file); err != nil { + errCh <- err + return + } - // Add the mainArtifact flag to request. - _ = writer.WriteField("mainArtifact", strconv.FormatBool(mainArtifact)) + // Add the mainArtifact flag to request. + if err = writer.WriteField("mainArtifact", strconv.FormatBool(mainArtifact)); err != nil { + errCh <- err + return + } - err = writer.Close() - if err != nil { - return "", err - } + errCh <- writer.Close() + }() // Ensure we have a correct URL. rel := &url.URL{Path: "artifact/upload"} u := c.APIURL.ResolveReference(rel) - req, err := http.NewRequest("POST", u.String(), body) + req, err := http.NewRequest("POST", u.String(), pr) if err != nil { return "", err } @@ -465,12 +475,17 @@ func (c *microcksClient) UploadArtifact(specificationFilePath string, mainArtifa } defer resp.Body.Close() + // Check for errors from the multipart writer goroutine. + if pipeErr := <-errCh; pipeErr != nil { + return "", fmt.Errorf("failed to write multipart form: %w", pipeErr) + } + // Dump response if verbose required. config.DumpResponseIfRequired("Microcks for uploading artifact", resp, true) respBody, err := io.ReadAll(resp.Body) if err != nil { - panic(err.Error()) + return "", fmt.Errorf("failed to read upload response: %w", err) } // Raise exception if not created. @@ -478,7 +493,7 @@ func (c *microcksClient) UploadArtifact(specificationFilePath string, mainArtifa return "", errs.New(string(respBody)) } - return string(respBody), err + return string(respBody), nil } func (c *microcksClient) DownloadArtifact(artifactURL string, mainArtifact bool, secret string) (string, error) { diff --git a/pkg/connectors/microcks_client_test.go b/pkg/connectors/microcks_client_test.go index 9e9b6c8..2fe423e 100644 --- a/pkg/connectors/microcks_client_test.go +++ b/pkg/connectors/microcks_client_test.go @@ -1,12 +1,73 @@ package connectors import ( + "io" "net/http" "net/http/httptest" + "os" + "path/filepath" "strings" "testing" ) +func TestUploadArtifactStreamsWithoutBuffering(t *testing.T) { + const fileContent = `{"openapi":"3.0.0","info":{"title":"Test API","version":"1.0.0"}}` + const expectedResponse = "artifact uploaded" + + // Create a temporary file to simulate an API specification. + tmpDir := t.TempDir() + specPath := filepath.Join(tmpDir, "openapi.json") + if err := os.WriteFile(specPath, []byte(fileContent), 0o600); err != nil { + t.Fatalf("failed to create temp spec file: %v", err) + } + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/api/artifact/upload" { + t.Fatalf("unexpected path: %s", r.URL.Path) + } + if r.Method != http.MethodPost { + t.Fatalf("unexpected method: %s", r.Method) + } + + // Verify the multipart form contains the file. + file, header, err := r.FormFile("file") + if err != nil { + t.Fatalf("failed to get form file: %v", err) + } + defer file.Close() + + if header.Filename != "openapi.json" { + t.Fatalf("unexpected filename: %s", header.Filename) + } + + body, err := io.ReadAll(file) + if err != nil { + t.Fatalf("failed to read uploaded file: %v", err) + } + if string(body) != fileContent { + t.Fatalf("file content mismatch: got %q, want %q", string(body), fileContent) + } + + // Verify the mainArtifact field. + if got := r.FormValue("mainArtifact"); got != "true" { + t.Fatalf("unexpected mainArtifact value: %s", got) + } + + w.WriteHeader(http.StatusCreated) + _, _ = w.Write([]byte(expectedResponse)) + })) + defer server.Close() + + client := NewMicrocksClient(server.URL) + msg, err := client.UploadArtifact(specPath, true) + if err != nil { + t.Fatalf("UploadArtifact returned error: %v", err) + } + if strings.TrimSpace(msg) != expectedResponse { + t.Fatalf("expected response %q, got %q", expectedResponse, msg) + } +} + func TestDownloadArtifactReturnsResponseBody(t *testing.T) { const expectedBody = "artifact downloaded"