11package httpclient
22
33import (
4- "bytes"
54 "context"
65 "encoding/base64"
76 "fmt"
87 "io"
98 "mime/multipart"
109 "net/http"
1110 "net/textproto"
12- "net/url"
1311 "os"
1412 "path/filepath"
15- "strings"
1613 "sync"
1714 "time"
1815
@@ -67,7 +64,11 @@ type UploadState struct {
6764// }
6865//
6966// // Use `result` or `resp` as needed
70- func (c * Client ) DoMultiPartRequest (method , endpoint string , files map [string ][]string , formDataFields map [string ]string , fileContentTypes map [string ]string , formDataPartHeaders map [string ]http.Header , out interface {}) (* http.Response , error ) {
67+ func (c * Client ) DoMultiPartRequest (method , endpoint string , files map [string ][]string , formDataFields map [string ]string , fileContentTypes map [string ]string , formDataPartHeaders map [string ]http.Header , encodingType string , out interface {}) (* http.Response , error ) {
68+ if encodingType != "raw" && encodingType != "base64" {
69+ c .Sugar .Errorw ("Invalid encoding type specified" , zap .String ("encodingType" , encodingType ))
70+ return nil , fmt .Errorf ("invalid encoding type: %s. Must be 'raw' or 'base64'" , encodingType )
71+ }
7172
7273 if method != http .MethodPost && method != http .MethodPut {
7374 c .Sugar .Error ("HTTP method not supported for multipart request" , zap .String ("method" , method ))
@@ -92,20 +93,21 @@ func (c *Client) DoMultiPartRequest(method, endpoint string, files map[string][]
9293 var body io.Reader
9394 var contentType string
9495
95- // Create multipart body in a function to ensure it runs again on retry
9696 createBody := func () error {
9797 var err error
98- body , contentType , err = createStreamingMultipartRequestBody (files , formDataFields , fileContentTypes , formDataPartHeaders , c .Sugar )
98+ body , contentType , err = createStreamingMultipartRequestBody (files , formDataFields , fileContentTypes , formDataPartHeaders , encodingType , c .Sugar )
9999 if err != nil {
100100 c .Sugar .Errorw ("Failed to create streaming multipart request body" , zap .Error (err ))
101101 } else {
102- c .Sugar .Infow ("Successfully created streaming multipart request body" , zap .String ("content_type" , contentType ))
102+ c .Sugar .Infow ("Successfully created streaming multipart request body" ,
103+ zap .String ("content_type" , contentType ),
104+ zap .String ("encoding" , encodingType ))
103105 }
104106 return err
105107 }
106108
107109 if err := createBody (); err != nil {
108- c .Sugar .Errorw ("Failed to create streaming multipart request body" , zap .Error (err ))
110+ c .Sugar .Errorw ("Failed to create multipart request body" , zap .Error (err ))
109111 return nil , err
110112 }
111113
@@ -115,23 +117,33 @@ func (c *Client) DoMultiPartRequest(method, endpoint string, files map[string][]
115117 return nil , err
116118 }
117119
118- c .Sugar .Infow ("Created HTTP Multipart request" , zap .String ("method" , method ), zap .String ("url" , url ), zap .String ("content_type" , contentType ))
120+ c .Sugar .Infow ("Created HTTP Multipart request" ,
121+ zap .String ("method" , method ),
122+ zap .String ("url" , url ),
123+ zap .String ("content_type" , contentType ),
124+ zap .String ("encoding" , encodingType ))
119125
120126 (* c .Integration ).PrepRequestParamsAndAuth (req )
121-
122127 req .Header .Set ("Content-Type" , contentType )
123128
124129 startTime := time .Now ()
125130
126- resp , requestErr := c .http .Do (req )
131+ resp , err := c .http .Do (req )
127132 duration := time .Since (startTime )
128133
129- if requestErr != nil {
130- c .Sugar .Errorw ("Failed to send request" , zap .String ("method" , method ), zap .String ("endpoint" , endpoint ), zap .Error (requestErr ))
131- return nil , requestErr
134+ if err != nil {
135+ c .Sugar .Errorw ("Failed to send request" ,
136+ zap .String ("method" , method ),
137+ zap .String ("endpoint" , endpoint ),
138+ zap .Error (err ))
139+ return nil , err
132140 }
133141
134- c .Sugar .Debugw ("Request sent successfully" , zap .String ("method" , method ), zap .String ("endpoint" , endpoint ), zap .Int ("status_code" , resp .StatusCode ), zap .Duration ("duration" , duration ))
142+ c .Sugar .Debugw ("Request sent successfully" ,
143+ zap .String ("method" , method ),
144+ zap .String ("endpoint" , endpoint ),
145+ zap .Int ("status_code" , resp .StatusCode ),
146+ zap .Duration ("duration" , duration ))
135147
136148 if resp .StatusCode >= 200 && resp .StatusCode < 300 {
137149 return resp , response .HandleAPISuccessResponse (resp , out , c .Sugar )
@@ -161,7 +173,7 @@ func (c *Client) DoMultiPartRequest(method, endpoint string, files map[string][]
161173// - string: The content type of the multipart request body. This includes the boundary string used by the multipart writer.
162174// - error: An error object indicating failure during the construction of the multipart request body. This could be due to issues
163175// such as file reading errors or multipart writer errors.
164- func createStreamingMultipartRequestBody (files map [string ][]string , formDataFields map [string ]string , fileContentTypes map [string ]string , formDataPartHeaders map [string ]http.Header , sugar * zap.SugaredLogger ) (io.Reader , string , error ) {
176+ func createStreamingMultipartRequestBody (files map [string ][]string , formDataFields map [string ]string , fileContentTypes map [string ]string , formDataPartHeaders map [string ]http.Header , encodingType string , sugar * zap.SugaredLogger ) (io.Reader , string , error ) {
165177 pr , pw := io .Pipe ()
166178 writer := multipart .NewWriter (pw )
167179
@@ -177,8 +189,11 @@ func createStreamingMultipartRequestBody(files map[string][]string, formDataFiel
177189
178190 for fieldName , filePaths := range files {
179191 for _ , filePath := range filePaths {
180- sugar .Debugw ("Adding file part" , zap .String ("field_name" , fieldName ), zap .String ("file_path" , filePath ))
181- if err := addFilePart (writer , fieldName , filePath , fileContentTypes , formDataPartHeaders , sugar ); err != nil {
192+ sugar .Debugw ("Adding file part" ,
193+ zap .String ("field_name" , fieldName ),
194+ zap .String ("file_path" , filePath ),
195+ zap .String ("encoding" , encodingType ))
196+ if err := addFilePartWithEncoding (writer , fieldName , filePath , fileContentTypes , formDataPartHeaders , encodingType , sugar ); err != nil {
182197 sugar .Errorw ("Failed to add file part" , zap .Error (err ))
183198 pw .CloseWithError (err )
184199 return
@@ -199,47 +214,33 @@ func createStreamingMultipartRequestBody(files map[string][]string, formDataFiel
199214 return pr , writer .FormDataContentType (), nil
200215}
201216
202- // addFilePart adds a base64 encoded file part to the multipart writer with the provided field name and file path.
203- // This function opens the specified file, sets the appropriate content type and headers, and adds it to the multipart writer.
204- // Parameters:
205- // - writer: The multipart writer used to construct the multipart request body.
206- // - fieldName: The field name for the file part.
207- // - filePath: The path to the file to be included in the request.
208- // - fileContentTypes: A map specifying the content type for each file part. The key is the field name and the value is the
209- // content type (e.g., "image/jpeg").
210- // - formDataPartHeaders: A map specifying custom headers for each part of the multipart form data. The key is the field name
211- // and the value is an http.Header containing the headers for that part.
212- // - sugar: An instance of a logger implementing the logger.Logger interface, used to sugar informational messages, warnings,
213- // and errors encountered during the addition of the file part.
214- //
215- // Returns:
216- // - error: An error object indicating failure during the addition of the file part. This could be due to issues such as
217- // file reading errors or multipart writer errors.
218- func addFilePart (writer * multipart.Writer , fieldName , filePath string , fileContentTypes map [string ]string , formDataPartHeaders map [string ]http.Header , sugar * zap.SugaredLogger ) error {
217+ // addFilePartWithEncoding adds a file part to the multipart writer with specified encoding.
218+ // Supports both raw file content and base64 encoding based on encodingType parameter.
219+ func addFilePartWithEncoding (writer * multipart.Writer , fieldName , filePath string , fileContentTypes map [string ]string , formDataPartHeaders map [string ]http.Header , encodingType string , sugar * zap.SugaredLogger ) error {
219220 file , err := os .Open (filePath )
220221 if err != nil {
221222 sugar .Errorw ("Failed to open file" , zap .String ("filePath" , filePath ), zap .Error (err ))
222223 return err
223224 }
224225 defer file .Close ()
225226
226- // Default fileContentType
227227 contentType := "application/octet-stream"
228228 if ct , ok := fileContentTypes [fieldName ]; ok {
229229 contentType = ct
230230 }
231231
232- header := setFormDataPartHeader (fieldName , filepath .Base (filePath ), contentType , formDataPartHeaders [fieldName ])
232+ header := createFilePartHeader (fieldName , filePath , contentType , formDataPartHeaders [fieldName ], encodingType )
233+ sugar .Debugw ("Created file part header" ,
234+ zap .String ("fieldName" , fieldName ),
235+ zap .String ("contentType" , contentType ),
236+ zap .String ("encoding" , encodingType ))
233237
234238 part , err := writer .CreatePart (header )
235239 if err != nil {
236240 sugar .Errorw ("Failed to create form file part" , zap .String ("fieldName" , fieldName ), zap .Error (err ))
237241 return err
238242 }
239243
240- encoder := base64 .NewEncoder (base64 .StdEncoding , part )
241- defer encoder .Close ()
242-
243244 fileSize , err := file .Stat ()
244245 if err != nil {
245246 sugar .Errorw ("Failed to get file info" , zap .String ("filePath" , filePath ), zap .Error (err ))
@@ -248,12 +249,34 @@ func addFilePart(writer *multipart.Writer, fieldName, filePath string, fileConte
248249
249250 progressLogger := logUploadProgress (file , fileSize .Size (), sugar )
250251 uploadState := & UploadState {}
251- if err := chunkFileUpload (file , encoder , progressLogger , uploadState , sugar ); err != nil {
252- sugar .Errorw ("Failed to copy file content" , zap .String ("filePath" , filePath ), zap .Error (err ))
253- return err
252+
253+ var writeTarget io.Writer = part
254+ if encodingType == "base64" {
255+ encoder := base64 .NewEncoder (base64 .StdEncoding , part )
256+ defer encoder .Close ()
257+ writeTarget = encoder
258+ sugar .Debugw ("Using base64 encoding for file upload" , zap .String ("fieldName" , fieldName ))
259+ } else {
260+ sugar .Debugw ("Using raw encoding for file upload" , zap .String ("fieldName" , fieldName ))
254261 }
255262
256- return nil
263+ return chunkFileUpload (file , writeTarget , progressLogger , uploadState , sugar )
264+ }
265+
266+ // createFilePartHeader creates the MIME header for a file part with the specified encoding type.
267+ func createFilePartHeader (fieldname , filename , contentType string , customHeaders http.Header , encodingType string ) textproto.MIMEHeader {
268+ header := textproto.MIMEHeader {}
269+ header .Set ("Content-Disposition" , fmt .Sprintf (`form-data; name="%s"; filename="%s"` , fieldname , filepath .Base (filename )))
270+ header .Set ("Content-Type" , contentType )
271+ if encodingType == "base64" {
272+ header .Set ("Content-Transfer-Encoding" , "base64" )
273+ }
274+ for key , values := range customHeaders {
275+ for _ , value := range values {
276+ header .Add (key , value )
277+ }
278+ }
279+ return header
257280}
258281
259282// addFormField adds a form field to the multipart writer with the provided key and value.
@@ -281,31 +304,6 @@ func addFormField(writer *multipart.Writer, key, val string, sugar *zap.SugaredL
281304 return nil
282305}
283306
284- // setFormDataPartHeader creates a textproto.MIMEHeader for a form data field with the provided field name, file name, content type, and custom headers.
285- // This function constructs the MIME headers for a multipart form data part, including the content disposition, content type,
286- // and any custom headers specified.
287- // Parameters:
288- // - fieldname: The name of the form field.
289- // - filename: The name of the file being uploaded (if applicable).
290- // - contentType: The content type of the form data part (e.g., "image/jpeg").
291- // - customHeaders: A map of custom headers to be added to the form data part. The key is the header name and the value is the
292- // header value.
293- //
294- // Returns:
295- // - textproto.MIMEHeader: The constructed MIME header for the form data part.
296- func setFormDataPartHeader (fieldname , filename , contentType string , customHeaders http.Header ) textproto.MIMEHeader {
297- header := textproto.MIMEHeader {}
298- header .Set ("Content-Disposition" , fmt .Sprintf (`form-data; name="%s"; filename="%s"` , fieldname , filename ))
299- header .Set ("Content-Type" , contentType )
300- header .Set ("Content-Transfer-Encoding" , "base64" )
301- for key , values := range customHeaders {
302- for _ , value := range values {
303- header .Add (key , value )
304- }
305- }
306- return header
307- }
308-
309307// chunkFileUpload reads the file upload into chunks and writes it to the writer.
310308// This function reads the file in chunks and writes it to the provided writer, allowing for progress logging during the upload.
311309// The chunk size is set to 8192 KB (8 MB) by default. This is a common chunk size used for file uploads to cloud storage services.
@@ -425,111 +423,3 @@ func logUploadProgress(file *os.File, fileSize int64, sugar *zap.SugaredLogger)
425423 }
426424 }
427425}
428-
429- // DoImageMultiPartUpload performs a multipart request with a specifically formatted payload.
430- // This is designed for APIs that expect a very specific multipart format, where the payload
431- // needs to be constructed manually rather than using the standard multipart writer.
432- func (c * Client ) DoImageMultiPartUpload (method , endpoint string , fileName string , base64Data string , customBoundary string , out interface {}) (* http.Response , error ) {
433- c .Sugar .Infow ("Starting DoImageMultiPartUpload" ,
434- zap .String ("method" , method ),
435- zap .String ("endpoint" , endpoint ),
436- zap .String ("fileName" , fileName ),
437- zap .String ("boundary" , customBoundary ))
438-
439- // URL encode the filename for both the Content-Disposition and data prefix
440- encodedFileName := url .QueryEscape (fileName )
441- c .Sugar .Debugw ("URL encoded filename" , zap .String ("encodedFileName" , encodedFileName ))
442-
443- // Construct payload exactly like the example
444- payload := fmt .Sprintf ("%s\r \n " +
445- "Content-Disposition: form-data; name=\" file\" ; filename=\" %s\" \r \n " +
446- "Content-Type: image/png\r \n \r \n " +
447- "data:image/png;name=%s;base64,%s\r \n " +
448- "%s-" ,
449- customBoundary ,
450- encodedFileName ,
451- encodedFileName ,
452- base64Data ,
453- customBoundary )
454-
455- // Create truncated version of payload for logging
456- truncatedPayload := payload
457- if len (base64Data ) > 100 {
458- // Find the position of base64 data in the payload
459- base64Start := strings .Index (payload , ";base64," ) + 8
460- if base64Start > 0 {
461- truncatedPayload = payload [:base64Start ] + "[BASE64_DATA_LENGTH: " +
462- fmt .Sprintf ("%d" , len (base64Data )) + "]\r \n " +
463- customBoundary + "-"
464- }
465- }
466-
467- c .Sugar .Debugw ("Constructed request payload" ,
468- zap .String ("payload" , truncatedPayload ))
469-
470- url := (* c .Integration ).GetFQDN () + endpoint
471- c .Sugar .Debugw ("Full request URL" , zap .String ("url" , url ))
472-
473- // Create request with string payload
474- req , err := http .NewRequest (method , url , strings .NewReader (payload ))
475- if err != nil {
476- c .Sugar .Errorw ("Failed to create request" , zap .Error (err ))
477- return nil , fmt .Errorf ("failed to create request: %v" , err )
478- }
479-
480- // Set headers exactly as in example
481- req .Header .Set ("accept" , "application/json" )
482- req .Header .Set ("content-type" , fmt .Sprintf ("multipart/form-data; boundary=%s" , strings .TrimPrefix (customBoundary , "---" )))
483-
484- c .Sugar .Debugw ("Initial headers" ,
485- zap .Any ("headers" , req .Header ),
486- zap .String ("accept" , req .Header .Get ("accept" )),
487- zap .String ("content-type" , req .Header .Get ("content-type" )))
488-
489- // Store initial headers
490- contentType := req .Header .Get ("content-type" )
491- accept := req .Header .Get ("accept" )
492-
493- // Apply auth
494- (* c .Integration ).PrepRequestParamsAndAuth (req )
495-
496- // Restore and log final headers
497- req .Header .Set ("accept" , accept )
498- req .Header .Set ("content-type" , contentType )
499-
500- c .Sugar .Infow ("Final request headers" ,
501- zap .Any ("headers" , req .Header ),
502- zap .String ("accept" , req .Header .Get ("accept" )),
503- zap .String ("content-type" , req .Header .Get ("content-type" )))
504-
505- // Send the request
506- resp , err := c .http .Do (req )
507- if err != nil {
508- return nil , fmt .Errorf ("failed to send request: %v" , err )
509- }
510-
511- c .Sugar .Debugw ("Response received" ,
512- zap .Int ("statusCode" , resp .StatusCode ),
513- zap .Any ("responseHeaders" , resp .Header ))
514-
515- // Handle response
516- if resp .StatusCode >= 200 && resp .StatusCode < 300 {
517- return resp , response .HandleAPISuccessResponse (resp , out , c .Sugar )
518- }
519-
520- // For error responses, try to log the response body
521- if resp .Body != nil {
522- bodyBytes , err := io .ReadAll (resp .Body )
523- if err != nil {
524- c .Sugar .Warnw ("Failed to read error response body" , zap .Error (err ))
525- } else {
526- c .Sugar .Errorw ("Request failed" ,
527- zap .Int ("statusCode" , resp .StatusCode ),
528- zap .String ("responseBody" , string (bodyBytes )))
529- // Create new reader with same data for error handler
530- resp .Body = io .NopCloser (bytes .NewReader (bodyBytes ))
531- }
532- }
533-
534- return resp , response .HandleAPIErrorResponse (resp , c .Sugar )
535- }
0 commit comments