mirror of
https://github.com/SAP/jenkins-library.git
synced 2025-01-18 05:18:24 +02:00
Merge pull request #1096 from SAP/http_uploader
Add upload of files to HTTP capabilities
This commit is contained in:
commit
997ae27ccd
126
pkg/http/http.go
126
pkg/http/http.go
@ -1,13 +1,18 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/SAP/jenkins-library/pkg/log"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// Client defines an http client object
|
||||
@ -16,6 +21,7 @@ type Client struct {
|
||||
username string
|
||||
password string
|
||||
token string
|
||||
logger *logrus.Entry
|
||||
}
|
||||
|
||||
// ClientOptions defines the options to be set on the client
|
||||
@ -26,33 +32,112 @@ type ClientOptions struct {
|
||||
Token string
|
||||
}
|
||||
|
||||
// Sender provides an interface to the piper http client for uid/pwd authenticated requests
|
||||
// Sender provides an interface to the piper http client for uid/pwd and token authenticated requests
|
||||
type Sender interface {
|
||||
SendRequest(method, url string, body io.Reader, header http.Header, cookies []*http.Cookie) (*http.Response, error)
|
||||
SetOptions(options ClientOptions)
|
||||
}
|
||||
|
||||
// Uploader provides an interface to the piper http client for uid/pwd and token authenticated requests with upload capabilities
|
||||
type Uploader interface {
|
||||
SendRequest(method, url string, body io.Reader, header http.Header, cookies []*http.Cookie) (*http.Response, error)
|
||||
UploadFile(url, file, fieldName string, header http.Header, cookies []*http.Cookie) (*http.Response, error)
|
||||
SetOptions(options ClientOptions)
|
||||
}
|
||||
|
||||
// UploadFile uploads a file's content as multipart-form POST request to the specified URL
|
||||
func (c *Client) UploadFile(url, file, fieldName string, header http.Header, cookies []*http.Cookie) (*http.Response, error) {
|
||||
httpClient := c.initialize()
|
||||
|
||||
bodyBuffer := &bytes.Buffer{}
|
||||
bodyWriter := multipart.NewWriter(bodyBuffer)
|
||||
|
||||
fileWriter, err := bodyWriter.CreateFormFile(fieldName, file)
|
||||
if err != nil {
|
||||
return &http.Response{}, errors.Wrapf(err, "error creating form file %v for field %v", file, fieldName)
|
||||
}
|
||||
|
||||
fileHandle, err := os.Open(file)
|
||||
if err != nil {
|
||||
return &http.Response{}, errors.Wrapf(err, "unable to locate file %v", file)
|
||||
}
|
||||
defer fileHandle.Close()
|
||||
|
||||
_, err = io.Copy(fileWriter, fileHandle)
|
||||
if err != nil {
|
||||
return &http.Response{}, errors.Wrapf(err, "unable to copy file content of %v into request body", file)
|
||||
}
|
||||
err = bodyWriter.Close()
|
||||
|
||||
method := http.MethodPost
|
||||
request, err := c.createRequest(method, url, bodyBuffer, &header, cookies)
|
||||
if err != nil {
|
||||
c.logger.Debugf("New %v request to %v", method, url)
|
||||
return &http.Response{}, errors.Wrapf(err, "error creating %v request to %v", method, url)
|
||||
}
|
||||
|
||||
startBoundary := strings.Index(bodyWriter.FormDataContentType(), "=") + 1
|
||||
boundary := bodyWriter.FormDataContentType()[startBoundary:]
|
||||
|
||||
request.Header.Add("Content-Type", "multipart/form-data; boundary=\""+boundary+"\"")
|
||||
request.Header.Add("Connection", "Keep-Alive")
|
||||
|
||||
response, err := httpClient.Do(request)
|
||||
if err != nil {
|
||||
return response, errors.Wrapf(err, "HTTP %v request to %v failed with error", method, url)
|
||||
}
|
||||
|
||||
return c.handleResponse(response)
|
||||
}
|
||||
|
||||
// SendRequest sends an http request with a defined method
|
||||
func (c *Client) SendRequest(method, url string, body io.Reader, header http.Header, cookies []*http.Cookie) (*http.Response, error) {
|
||||
httpClient := c.initialize()
|
||||
|
||||
logger := log.Entry().WithField("package", "SAP/jenkins-library/pkg/http")
|
||||
request, err := c.createRequest(method, url, body, &header, cookies)
|
||||
if err != nil {
|
||||
c.logger.Debugf("New %v request to %v", method, url)
|
||||
return &http.Response{}, errors.Wrapf(err, "error creating %v request to %v", method, url)
|
||||
}
|
||||
|
||||
response, err := httpClient.Do(request)
|
||||
if err != nil {
|
||||
return response, errors.Wrapf(err, "error opening %v", url)
|
||||
}
|
||||
|
||||
return c.handleResponse(response)
|
||||
}
|
||||
|
||||
// SetOptions sets options used for the http client
|
||||
func (c *Client) SetOptions(options ClientOptions) {
|
||||
c.timeout = options.Timeout
|
||||
c.username = options.Username
|
||||
c.password = options.Password
|
||||
c.token = options.Token
|
||||
c.logger = log.Entry().WithField("package", "SAP/jenkins-library/pkg/http")
|
||||
}
|
||||
|
||||
func (c *Client) initialize() *http.Client {
|
||||
c.applyDefaults()
|
||||
|
||||
var httpClient = &http.Client{
|
||||
Timeout: c.timeout,
|
||||
}
|
||||
|
||||
logger.Debugf("Timeout set to %v", c.timeout)
|
||||
c.logger.Debugf("Timeout set to %v", c.timeout)
|
||||
|
||||
return httpClient
|
||||
}
|
||||
|
||||
func (c *Client) createRequest(method, url string, body io.Reader, header *http.Header, cookies []*http.Cookie) (*http.Request, error) {
|
||||
c.logger.Debugf("New %v request to %v", method, url)
|
||||
request, err := http.NewRequest(method, url, body)
|
||||
logger.Debugf("New %v request to %v", method, url)
|
||||
if err != nil {
|
||||
return &http.Response{}, errors.Wrapf(err, "error creating %v request to %v", method, url)
|
||||
return &http.Request{}, err
|
||||
}
|
||||
|
||||
if header != nil {
|
||||
for name, headers := range header {
|
||||
for name, headers := range *header {
|
||||
for _, h := range headers {
|
||||
request.Header.Add(name, h)
|
||||
}
|
||||
@ -67,14 +152,17 @@ func (c *Client) SendRequest(method, url string, body io.Reader, header http.Hea
|
||||
|
||||
if len(c.username) > 0 && len(c.password) > 0 {
|
||||
request.SetBasicAuth(c.username, c.password)
|
||||
logger.Debug("Using Basic Authentication ****/****")
|
||||
c.logger.Debug("Using Basic Authentication ****/****")
|
||||
}
|
||||
|
||||
response, err := httpClient.Do(request)
|
||||
if err != nil {
|
||||
return response, errors.Wrapf(err, "error opening %v", url)
|
||||
if len(c.token) > 0 {
|
||||
request.Header.Add("Authorization", c.token)
|
||||
}
|
||||
|
||||
return request, nil
|
||||
}
|
||||
|
||||
func (c *Client) handleResponse(response *http.Response) (*http.Response, error) {
|
||||
// 2xx codes do not create an error
|
||||
if response.StatusCode >= 200 && response.StatusCode < 300 {
|
||||
return response, nil
|
||||
@ -82,24 +170,16 @@ func (c *Client) SendRequest(method, url string, body io.Reader, header http.Hea
|
||||
|
||||
switch response.StatusCode {
|
||||
case http.StatusUnauthorized:
|
||||
logger.WithField("HTTP Error", "401 (Unauthorized)").Error("Credentials invalid, please check your user credentials!")
|
||||
c.logger.WithField("HTTP Error", "401 (Unauthorized)").Error("Credentials invalid, please check your user credentials!")
|
||||
case http.StatusForbidden:
|
||||
logger.WithField("HTTP Error", "403 (Forbidden)").Error("Permission issue, please check your user permissions!")
|
||||
c.logger.WithField("HTTP Error", "403 (Forbidden)").Error("Permission issue, please check your user permissions!")
|
||||
case http.StatusNotFound:
|
||||
logger.WithField("HTTP Error", "404 (Not Found)").Error("Requested resource could not be found")
|
||||
c.logger.WithField("HTTP Error", "404 (Not Found)").Error("Requested resource could not be found")
|
||||
case http.StatusInternalServerError:
|
||||
logger.WithField("HTTP Error", "500 (Internal Server Error)").Error("Unknown error occured.")
|
||||
c.logger.WithField("HTTP Error", "500 (Internal Server Error)").Error("Unknown error occured.")
|
||||
}
|
||||
|
||||
return response, fmt.Errorf("Request to %v returned with HTTP Code %v", url, response.StatusCode)
|
||||
}
|
||||
|
||||
// SetOptions sets options used for the http client
|
||||
func (c *Client) SetOptions(options ClientOptions) {
|
||||
c.timeout = options.Timeout
|
||||
c.username = options.Username
|
||||
c.password = options.Password
|
||||
c.token = options.Token
|
||||
return response, fmt.Errorf("Request to %v returned with HTTP Code %v", response.Request.URL, response.StatusCode)
|
||||
}
|
||||
|
||||
func (c *Client) applyDefaults() {
|
||||
|
@ -4,11 +4,14 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/SAP/jenkins-library/pkg/log"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
@ -41,10 +44,10 @@ func TestSendRequest(t *testing.T) {
|
||||
cookies []*http.Cookie
|
||||
expected string
|
||||
}{
|
||||
{client: Client{}, method: "GET", expected: "OK"},
|
||||
{client: Client{}, method: "GET", header: map[string][]string{"Testheader": []string{"Test1", "Test2"}}, expected: "OK"},
|
||||
{client: Client{}, cookies: []*http.Cookie{{Name: "TestCookie1", Value: "TestValue1"}, {Name: "TestCookie2", Value: "TestValue2"}}, method: "GET", expected: "OK"},
|
||||
{client: Client{username: "TestUser", password: "TestPwd"}, method: "GET", expected: "OK"},
|
||||
{client: Client{logger: log.Entry().WithField("package", "SAP/jenkins-library/pkg/http")}, method: "GET", expected: "OK"},
|
||||
{client: Client{logger: log.Entry().WithField("package", "SAP/jenkins-library/pkg/http")}, method: "GET", header: map[string][]string{"Testheader": []string{"Test1", "Test2"}}, expected: "OK"},
|
||||
{client: Client{logger: log.Entry().WithField("package", "SAP/jenkins-library/pkg/http")}, cookies: []*http.Cookie{{Name: "TestCookie1", Value: "TestValue1"}, {Name: "TestCookie2", Value: "TestValue2"}}, method: "GET", expected: "OK"},
|
||||
{client: Client{logger: log.Entry().WithField("package", "SAP/jenkins-library/pkg/http"), username: "TestUser", password: "TestPwd"}, method: "GET", expected: "OK"},
|
||||
}
|
||||
|
||||
for key, test := range tt {
|
||||
@ -100,3 +103,99 @@ func TestApplyDefaults(t *testing.T) {
|
||||
assert.Equal(t, v.expected, v.client, fmt.Sprintf("Run %v failed", k))
|
||||
}
|
||||
}
|
||||
|
||||
func TestUploadFile(t *testing.T) {
|
||||
var passedHeaders = map[string][]string{}
|
||||
passedCookies := []*http.Cookie{}
|
||||
var passedUsername string
|
||||
var passedPassword string
|
||||
var multipartFile multipart.File
|
||||
var multipartHeader *multipart.FileHeader
|
||||
var passedFileContents []byte
|
||||
// Start a local HTTP server
|
||||
server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
|
||||
passedHeaders = map[string][]string{}
|
||||
if req.Header != nil {
|
||||
for name, headers := range req.Header {
|
||||
passedHeaders[name] = headers
|
||||
}
|
||||
}
|
||||
passedCookies = req.Cookies()
|
||||
passedUsername, passedPassword, _ = req.BasicAuth()
|
||||
err := req.ParseMultipartForm(4096)
|
||||
if err != nil {
|
||||
t.FailNow()
|
||||
}
|
||||
multipartFile, multipartHeader, err = req.FormFile("Field1")
|
||||
if err != nil {
|
||||
t.FailNow()
|
||||
}
|
||||
defer req.Body.Close()
|
||||
passedFileContents, err = ioutil.ReadAll(multipartFile)
|
||||
if err != nil {
|
||||
t.FailNow()
|
||||
}
|
||||
|
||||
rw.Write([]byte("OK"))
|
||||
}))
|
||||
// Close the server when test finishes
|
||||
defer server.Close()
|
||||
|
||||
testFile, err := ioutil.TempFile("", "testFileUpload")
|
||||
if err != nil {
|
||||
t.FailNow()
|
||||
}
|
||||
defer os.RemoveAll(testFile.Name()) // clean up
|
||||
|
||||
fileContents, err := ioutil.ReadFile(testFile.Name())
|
||||
if err != nil {
|
||||
t.FailNow()
|
||||
}
|
||||
|
||||
tt := []struct {
|
||||
clientOptions ClientOptions
|
||||
method string
|
||||
body io.Reader
|
||||
header http.Header
|
||||
cookies []*http.Cookie
|
||||
expected string
|
||||
}{
|
||||
{clientOptions: ClientOptions{}, method: "POST", expected: "OK"},
|
||||
{clientOptions: ClientOptions{}, method: "POST", header: map[string][]string{"Testheader": []string{"Test1", "Test2"}}, expected: "OK"},
|
||||
{clientOptions: ClientOptions{}, cookies: []*http.Cookie{{Name: "TestCookie1", Value: "TestValue1"}, {Name: "TestCookie2", Value: "TestValue2"}}, method: "POST", expected: "OK"},
|
||||
{clientOptions: ClientOptions{Username: "TestUser", Password: "TestPwd"}, method: "POST", expected: "OK"},
|
||||
}
|
||||
|
||||
client := Client{logger: log.Entry().WithField("package", "SAP/jenkins-library/pkg/http")}
|
||||
for key, test := range tt {
|
||||
t.Run(fmt.Sprintf("Row %v", key+1), func(t *testing.T) {
|
||||
client.SetOptions(test.clientOptions)
|
||||
response, err := client.UploadFile(server.URL, testFile.Name(), "Field1", test.header, test.cookies)
|
||||
assert.NoError(t, err, "Error occurred but none expected")
|
||||
content, err := ioutil.ReadAll(response.Body)
|
||||
assert.NoError(t, err, "Error occurred but none expected")
|
||||
assert.Equal(t, test.expected, string(content), "Returned content incorrect")
|
||||
response.Body.Close()
|
||||
|
||||
assert.Equal(t, testFile.Name(), multipartHeader.Filename, "Uploaded file incorrect")
|
||||
assert.Equal(t, fileContents, passedFileContents, "Uploaded file incorrect")
|
||||
|
||||
for k, h := range test.header {
|
||||
assert.Containsf(t, passedHeaders, k, "Header %v not contained", k)
|
||||
assert.Equalf(t, h, passedHeaders[k], "Header %v contains different value")
|
||||
}
|
||||
|
||||
if len(test.cookies) > 0 {
|
||||
assert.Equal(t, test.cookies, passedCookies, "Passed cookies not correct")
|
||||
}
|
||||
|
||||
if len(client.username) > 0 {
|
||||
assert.Equal(t, client.username, passedUsername)
|
||||
}
|
||||
|
||||
if len(client.password) > 0 {
|
||||
assert.Equal(t, client.password, passedPassword)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user