Skip to content

Commit 7014547

Browse files
committed
feat: optimize DoImageMultiPartUpload for handking raw and base64 encoded streams
1 parent 1bb942c commit 7014547

File tree

1 file changed

+136
-53
lines changed

1 file changed

+136
-53
lines changed

httpclient/multipartrequest.go

Lines changed: 136 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,11 @@ type UploadState struct {
6767
// }
6868
//
6969
// // 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) {
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, encodingType string, out interface{}) (*http.Response, error) {
71+
if encodingType != "raw" && encodingType != "base64" {
72+
c.Sugar.Errorw("Invalid encoding type specified", zap.String("encodingType", encodingType))
73+
return nil, fmt.Errorf("invalid encoding type: %s. Must be 'raw' or 'base64'", encodingType)
74+
}
7175

7276
if method != http.MethodPost && method != http.MethodPut {
7377
c.Sugar.Error("HTTP method not supported for multipart request", zap.String("method", method))
@@ -92,20 +96,21 @@ func (c *Client) DoMultiPartRequest(method, endpoint string, files map[string][]
9296
var body io.Reader
9397
var contentType string
9498

95-
// Create multipart body in a function to ensure it runs again on retry
9699
createBody := func() error {
97100
var err error
98-
body, contentType, err = createStreamingMultipartRequestBody(files, formDataFields, fileContentTypes, formDataPartHeaders, c.Sugar)
101+
body, contentType, err = createStreamingMultipartRequestBody(files, formDataFields, fileContentTypes, formDataPartHeaders, encodingType, c.Sugar)
99102
if err != nil {
100103
c.Sugar.Errorw("Failed to create streaming multipart request body", zap.Error(err))
101104
} else {
102-
c.Sugar.Infow("Successfully created streaming multipart request body", zap.String("content_type", contentType))
105+
c.Sugar.Infow("Successfully created streaming multipart request body",
106+
zap.String("content_type", contentType),
107+
zap.String("encoding", encodingType))
103108
}
104109
return err
105110
}
106111

107112
if err := createBody(); err != nil {
108-
c.Sugar.Errorw("Failed to create streaming multipart request body", zap.Error(err))
113+
c.Sugar.Errorw("Failed to create multipart request body", zap.Error(err))
109114
return nil, err
110115
}
111116

@@ -115,23 +120,33 @@ func (c *Client) DoMultiPartRequest(method, endpoint string, files map[string][]
115120
return nil, err
116121
}
117122

118-
c.Sugar.Infow("Created HTTP Multipart request", zap.String("method", method), zap.String("url", url), zap.String("content_type", contentType))
123+
c.Sugar.Infow("Created HTTP Multipart request",
124+
zap.String("method", method),
125+
zap.String("url", url),
126+
zap.String("content_type", contentType),
127+
zap.String("encoding", encodingType))
119128

120129
(*c.Integration).PrepRequestParamsAndAuth(req)
121-
122130
req.Header.Set("Content-Type", contentType)
123131

124132
startTime := time.Now()
125133

126-
resp, requestErr := c.http.Do(req)
134+
resp, err := c.http.Do(req)
127135
duration := time.Since(startTime)
128136

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
137+
if err != nil {
138+
c.Sugar.Errorw("Failed to send request",
139+
zap.String("method", method),
140+
zap.String("endpoint", endpoint),
141+
zap.Error(err))
142+
return nil, err
132143
}
133144

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))
145+
c.Sugar.Debugw("Request sent successfully",
146+
zap.String("method", method),
147+
zap.String("endpoint", endpoint),
148+
zap.Int("status_code", resp.StatusCode),
149+
zap.Duration("duration", duration))
135150

136151
if resp.StatusCode >= 200 && resp.StatusCode < 300 {
137152
return resp, response.HandleAPISuccessResponse(resp, out, c.Sugar)
@@ -161,7 +176,7 @@ func (c *Client) DoMultiPartRequest(method, endpoint string, files map[string][]
161176
// - string: The content type of the multipart request body. This includes the boundary string used by the multipart writer.
162177
// - error: An error object indicating failure during the construction of the multipart request body. This could be due to issues
163178
// 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) {
179+
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) {
165180
pr, pw := io.Pipe()
166181
writer := multipart.NewWriter(pw)
167182

@@ -177,8 +192,11 @@ func createStreamingMultipartRequestBody(files map[string][]string, formDataFiel
177192

178193
for fieldName, filePaths := range files {
179194
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 {
195+
sugar.Debugw("Adding file part",
196+
zap.String("field_name", fieldName),
197+
zap.String("file_path", filePath),
198+
zap.String("encoding", encodingType))
199+
if err := addFilePartWithEncoding(writer, fieldName, filePath, fileContentTypes, formDataPartHeaders, encodingType, sugar); err != nil {
182200
sugar.Errorw("Failed to add file part", zap.Error(err))
183201
pw.CloseWithError(err)
184202
return
@@ -199,47 +217,33 @@ func createStreamingMultipartRequestBody(files map[string][]string, formDataFiel
199217
return pr, writer.FormDataContentType(), nil
200218
}
201219

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 {
220+
// addFilePartWithEncoding adds a file part to the multipart writer with specified encoding.
221+
// Supports both raw file content and base64 encoding based on encodingType parameter.
222+
func addFilePartWithEncoding(writer *multipart.Writer, fieldName, filePath string, fileContentTypes map[string]string, formDataPartHeaders map[string]http.Header, encodingType string, sugar *zap.SugaredLogger) error {
219223
file, err := os.Open(filePath)
220224
if err != nil {
221225
sugar.Errorw("Failed to open file", zap.String("filePath", filePath), zap.Error(err))
222226
return err
223227
}
224228
defer file.Close()
225229

226-
// Default fileContentType
227230
contentType := "application/octet-stream"
228231
if ct, ok := fileContentTypes[fieldName]; ok {
229232
contentType = ct
230233
}
231234

232-
header := setFormDataPartHeader(fieldName, filepath.Base(filePath), contentType, formDataPartHeaders[fieldName])
235+
header := createFilePartHeader(fieldName, filePath, contentType, formDataPartHeaders[fieldName], encodingType)
236+
sugar.Debugw("Created file part header",
237+
zap.String("fieldName", fieldName),
238+
zap.String("contentType", contentType),
239+
zap.String("encoding", encodingType))
233240

234241
part, err := writer.CreatePart(header)
235242
if err != nil {
236243
sugar.Errorw("Failed to create form file part", zap.String("fieldName", fieldName), zap.Error(err))
237244
return err
238245
}
239246

240-
encoder := base64.NewEncoder(base64.StdEncoding, part)
241-
defer encoder.Close()
242-
243247
fileSize, err := file.Stat()
244248
if err != nil {
245249
sugar.Errorw("Failed to get file info", zap.String("filePath", filePath), zap.Error(err))
@@ -248,12 +252,34 @@ func addFilePart(writer *multipart.Writer, fieldName, filePath string, fileConte
248252

249253
progressLogger := logUploadProgress(file, fileSize.Size(), sugar)
250254
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
255+
256+
var writeTarget io.Writer = part
257+
if encodingType == "base64" {
258+
encoder := base64.NewEncoder(base64.StdEncoding, part)
259+
defer encoder.Close()
260+
writeTarget = encoder
261+
sugar.Debugw("Using base64 encoding for file upload", zap.String("fieldName", fieldName))
262+
} else {
263+
sugar.Debugw("Using raw encoding for file upload", zap.String("fieldName", fieldName))
254264
}
255265

256-
return nil
266+
return chunkFileUpload(file, writeTarget, progressLogger, uploadState, sugar)
267+
}
268+
269+
// createFilePartHeader creates the MIME header for a file part with the specified encoding type.
270+
func createFilePartHeader(fieldname, filename, contentType string, customHeaders http.Header, encodingType string) textproto.MIMEHeader {
271+
header := textproto.MIMEHeader{}
272+
header.Set("Content-Disposition", fmt.Sprintf(`form-data; name="%s"; filename="%s"`, fieldname, filepath.Base(filename)))
273+
header.Set("Content-Type", contentType)
274+
if encodingType == "base64" {
275+
header.Set("Content-Transfer-Encoding", "base64")
276+
}
277+
for key, values := range customHeaders {
278+
for _, value := range values {
279+
header.Add(key, value)
280+
}
281+
}
282+
return header
257283
}
258284

259285
// addFormField adds a form field to the multipart writer with the provided key and value.
@@ -281,6 +307,63 @@ func addFormField(writer *multipart.Writer, key, val string, sugar *zap.SugaredL
281307
return nil
282308
}
283309

310+
// addFilePart adds a base64 encoded file part to the multipart writer with the provided field name and file path.
311+
// This function opens the specified file, sets the appropriate content type and headers, and adds it to the multipart writer.
312+
// Parameters:
313+
// - writer: The multipart writer used to construct the multipart request body.
314+
// - fieldName: The field name for the file part.
315+
// - filePath: The path to the file to be included in the request.
316+
// - fileContentTypes: A map specifying the content type for each file part. The key is the field name and the value is the
317+
// content type (e.g., "image/jpeg").
318+
// - formDataPartHeaders: A map specifying custom headers for each part of the multipart form data. The key is the field name
319+
// and the value is an http.Header containing the headers for that part.
320+
// - sugar: An instance of a logger implementing the logger.Logger interface, used to sugar informational messages, warnings,
321+
// and errors encountered during the addition of the file part.
322+
//
323+
// Returns:
324+
// - error: An error object indicating failure during the addition of the file part. This could be due to issues such as
325+
// file reading errors or multipart writer errors.
326+
// func addFilePart(writer *multipart.Writer, fieldName, filePath string, fileContentTypes map[string]string, formDataPartHeaders map[string]http.Header, sugar *zap.SugaredLogger) error {
327+
// file, err := os.Open(filePath)
328+
// if err != nil {
329+
// sugar.Errorw("Failed to open file", zap.String("filePath", filePath), zap.Error(err))
330+
// return err
331+
// }
332+
// defer file.Close()
333+
334+
// // Default fileContentType
335+
// contentType := "application/octet-stream"
336+
// if ct, ok := fileContentTypes[fieldName]; ok {
337+
// contentType = ct
338+
// }
339+
340+
// header := setFormDataPartHeader(fieldName, filepath.Base(filePath), contentType, formDataPartHeaders[fieldName])
341+
342+
// part, err := writer.CreatePart(header)
343+
// if err != nil {
344+
// sugar.Errorw("Failed to create form file part", zap.String("fieldName", fieldName), zap.Error(err))
345+
// return err
346+
// }
347+
348+
// encoder := base64.NewEncoder(base64.StdEncoding, part)
349+
// defer encoder.Close()
350+
351+
// fileSize, err := file.Stat()
352+
// if err != nil {
353+
// sugar.Errorw("Failed to get file info", zap.String("filePath", filePath), zap.Error(err))
354+
// return err
355+
// }
356+
357+
// progressLogger := logUploadProgress(file, fileSize.Size(), sugar)
358+
// uploadState := &UploadState{}
359+
// if err := chunkFileUpload(file, encoder, progressLogger, uploadState, sugar); err != nil {
360+
// sugar.Errorw("Failed to copy file content", zap.String("filePath", filePath), zap.Error(err))
361+
// return err
362+
// }
363+
364+
// return nil
365+
// }
366+
284367
// setFormDataPartHeader creates a textproto.MIMEHeader for a form data field with the provided field name, file name, content type, and custom headers.
285368
// This function constructs the MIME headers for a multipart form data part, including the content disposition, content type,
286369
// and any custom headers specified.
@@ -293,18 +376,18 @@ func addFormField(writer *multipart.Writer, key, val string, sugar *zap.SugaredL
293376
//
294377
// Returns:
295378
// - 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-
}
379+
// func setFormDataPartHeader(fieldname, filename, contentType string, customHeaders http.Header) textproto.MIMEHeader {
380+
// header := textproto.MIMEHeader{}
381+
// header.Set("Content-Disposition", fmt.Sprintf(`form-data; name="%s"; filename="%s"`, fieldname, filename))
382+
// header.Set("Content-Type", contentType)
383+
// header.Set("Content-Transfer-Encoding", "base64")
384+
// for key, values := range customHeaders {
385+
// for _, value := range values {
386+
// header.Add(key, value)
387+
// }
388+
// }
389+
// return header
390+
// }
308391

309392
// chunkFileUpload reads the file upload into chunks and writes it to the writer.
310393
// This function reads the file in chunks and writes it to the provided writer, allowing for progress logging during the upload.

0 commit comments

Comments
 (0)