diff --git a/.github/workflows/auto-merge-dependabot.yml b/.github/workflows/auto-merge-dependabot.yml new file mode 100644 index 0000000..7704217 --- /dev/null +++ b/.github/workflows/auto-merge-dependabot.yml @@ -0,0 +1,171 @@ +name: auto-merge dependabot updates + +on: + pull_request_target: + branches: [ main ] + types: + - opened + - synchronize + - reopened + - ready_for_review + +permissions: + pull-requests: write + contents: write + +jobs: + dependabot-merge: + runs-on: ubuntu-latest + if: ${{ github.actor == 'dependabot[bot]' }} + + steps: + - name: Dependabot metadata + id: metadata + uses: dependabot/fetch-metadata@v2.2.0 + with: + github-token: "${{ secrets.DEPENDABOT_PAT }}" # Using PAT for enhanced features, alert-lookup, and compat-lookup + alert-lookup: true # Enable security alert information + compat-lookup: true # Enable compatibility score checking + + - name: Check security and compatibility + id: security_check + run: | + DEPS_JSON='${{ steps.metadata.outputs.updated-dependencies-json }}' + + # Perform checks + if [ "${{ steps.metadata.outputs.alert-state }}" = "OPEN" ]; then + echo "⚠️ Security alert detected (GHSA: ${{ steps.metadata.outputs.ghsa-id }})" + echo "CVSS Score: ${{ steps.metadata.outputs.cvss }}" + echo "is_security_update=true" >> $GITHUB_OUTPUT + else + echo "is_security_update=false" >> $GITHUB_OUTPUT + fi + + if [ "${{ steps.metadata.outputs.compatibility-score }}" -lt 75 ]; then + echo "⚠️ Low compatibility score: ${{ steps.metadata.outputs.compatibility-score }}" + echo "is_compatible=false" >> $GITHUB_OUTPUT + else + echo "is_compatible=true" >> $GITHUB_OUTPUT + fi + + if [ "${{ steps.metadata.outputs.maintainer-changes }}" = "true" ]; then + echo "⚠️ Maintainer changes detected" + echo "has_maintainer_changes=true" >> $GITHUB_OUTPUT + else + echo "has_maintainer_changes=false" >> $GITHUB_OUTPUT + fi + + - name: Checkout repository + uses: actions/checkout@v4 + if: ${{ steps.metadata.outputs.package-ecosystem == 'gomod' }} + + - name: Setup Go + uses: actions/setup-go@v5 + if: ${{ steps.metadata.outputs.package-ecosystem == 'gomod' }} + with: + go-version: 'stable' + + - name: Process Go dependencies + if: ${{ steps.metadata.outputs.package-ecosystem == 'gomod' }} + run: | + log_update_details() { + local pr_number=$1 + echo "::group::Dependency Update Details for PR #$pr_number" + echo "🔄 Dependencies: ${{ steps.metadata.outputs.dependency-names }}" + echo "📦 Type: ${{ steps.metadata.outputs.dependency-type }}" + echo "📈 Version: ${{ steps.metadata.outputs.previous-version }} → ${{ steps.metadata.outputs.new-version }}" + echo "📂 Directory: ${{ steps.metadata.outputs.directory }}" + [ "${{ steps.security_check.outputs.is_security_update }}" = "true" ] && \ + echo "🚨 Security update (CVSS: ${{ steps.metadata.outputs.cvss }})" + echo "::endgroup::" + } + + echo "🔍 Fetching all Go-related Dependabot PRs..." + GO_PRS=$(gh pr list \ + --author "dependabot[bot]" \ + --json number,title,createdAt,headRefName \ + --state open \ + --jq 'sort_by(.createdAt) | .[] | select(.title | contains("go.mod"))') + + CURRENT_PR_PROCESSED=false + + echo "$GO_PRS" | while read -r pr; do + PR_NUMBER=$(echo "$pr" | jq -r .number) + HEAD_BRANCH=$(echo "$pr" | jq -r .headRefName) + + log_update_details $PR_NUMBER + + # Skip indirect dependencies unless they're security updates + if [ "${{ steps.metadata.outputs.dependency-type }}" = "indirect" ] && \ + [ "${{ steps.security_check.outputs.is_security_update }}" != "true" ]; then + echo "⏭️ Skipping indirect dependency update" + continue + fi + + # Special handling for security updates + if [ "${{ steps.security_check.outputs.is_security_update }}" = "true" ]; then + echo "🚨 Processing security update with priority" + PRIORITY_MERGE=true + fi + + git fetch origin $HEAD_BRANCH + git checkout $HEAD_BRANCH + git pull origin $HEAD_BRANCH + + echo "🛠️ Running go mod tidy for PR #$PR_NUMBER" + go mod tidy + + if git diff --quiet; then + echo "✨ No changes required for PR #$PR_NUMBER" + else + echo "💾 Committing changes for PR #$PR_NUMBER" + git config --global user.name "GitHub Actions" + git config --global user.email "actions@github.com" + git commit -am "chore: go mod tidy for PR #$PR_NUMBER" + git push origin $HEAD_BRANCH + fi + + # Auto-merge decision logic + if [ "$PR_NUMBER" = "$CURRENT_PR_NUMBER" ]; then + CURRENT_PR_PROCESSED=true + if { [ "$UPDATE_TYPE" != "version-update:semver-major" ] || \ + [ "${{ steps.security_check.outputs.is_security_update }}" = "true" ]; } && \ + [ "${{ steps.security_check.outputs.is_compatible }}" = "true" ] && \ + [ "${{ steps.security_check.outputs.has_maintainer_changes }}" = "false" ]; then + echo "🤖 Enabling auto-merge for current PR #$PR_NUMBER" + gh pr merge --auto --merge "$PR_URL" + fi + elif [ "$CURRENT_PR_PROCESSED" = false ]; then + echo "🔄 Processing older PR #$PR_NUMBER first" + gh pr merge --auto --merge "$PR_NUMBER" + fi + done + env: + GITHUB_TOKEN: ${{ secrets.DEPENDABOT_PAT }} + PR_URL: ${{ github.event.pull_request.html_url }} + CURRENT_PR_NUMBER: ${{ github.event.pull_request.number }} + UPDATE_TYPE: ${{ steps.metadata.outputs.update-type }} + + # Handle other dependencies with security awareness + - name: Enable auto-merge for pipeline dependencies + if: | + steps.security_check.outputs.is_compatible == 'true' && + steps.security_check.outputs.has_maintainer_changes == 'false' && + (steps.metadata.outputs.update-type != 'version-update:semver-major' || steps.security_check.outputs.is_security_update == 'true') && + contains(steps.metadata.outputs.directory, '.github/workflows') + run: gh pr merge --auto --merge "$PR_URL" + env: + PR_URL: ${{github.event.pull_request.html_url}} + GITHUB_TOKEN: ${{secrets.DEPENDABOT_PAT}} + + - name: Enable auto-merge for other dependencies + if: | + steps.security_check.outputs.is_compatible == 'true' && + steps.security_check.outputs.has_maintainer_changes == 'false' && + (steps.metadata.outputs.update-type != 'version-update:semver-major' || steps.security_check.outputs.is_security_update == 'true') && + steps.metadata.outputs.package-ecosystem != 'gomod' && + !contains(steps.metadata.outputs.directory, '.github/workflows') + run: gh pr merge --auto --merge "$PR_URL" + env: + PR_URL: ${{github.event.pull_request.html_url}} + GITHUB_TOKEN: ${{secrets.DEPENDABOT_PAT}} \ No newline at end of file diff --git a/.github/workflows/linter.yml b/.github/workflows/linter.yml index 8c06765..b6b8272 100644 --- a/.github/workflows/linter.yml +++ b/.github/workflows/linter.yml @@ -11,7 +11,7 @@ jobs: uses: actions/checkout@v4 - name: Lint Code Base - uses: github/super-linter@v6 + uses: github/super-linter@v7 env: VALIDATE_ALL_CODEBASE: false VALIDATE_MARKDOWN: false diff --git a/go.mod b/go.mod index a457052..46b255e 100644 --- a/go.mod +++ b/go.mod @@ -3,16 +3,16 @@ module github.com/deploymenttheory/go-api-http-client go 1.22.4 require ( - github.com/antchfx/xmlquery v1.4.1 + github.com/antchfx/xmlquery v1.4.2 github.com/google/uuid v1.6.0 go.uber.org/zap v1.27.0 - golang.org/x/net v0.26.0 + golang.org/x/net v0.31.0 ) require ( - github.com/antchfx/xpath v1.3.1 // indirect + github.com/antchfx/xpath v1.3.2 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/stretchr/testify v1.9.0 // indirect go.uber.org/multierr v1.11.0 // indirect - golang.org/x/text v0.16.0 // indirect + golang.org/x/text v0.20.0 // indirect ) diff --git a/go.sum b/go.sum index cc2739a..241aecc 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,7 @@ -github.com/antchfx/xmlquery v1.4.1 h1:YgpSwbeWvLp557YFTi8E3z6t6/hYjmFEtiEKbDfEbl0= -github.com/antchfx/xmlquery v1.4.1/go.mod h1:lKezcT8ELGt8kW5L+ckFMTbgdR61/odpPgDv8Gvi1fI= -github.com/antchfx/xpath v1.3.1 h1:PNbFuUqHwWl0xRjvUPjJ95Agbmdj2uzzIwmQKgu4oCk= -github.com/antchfx/xpath v1.3.1/go.mod h1:i54GszH55fYfBmoZXapTHN8T8tkcHfRgLyVwwqzXNcs= +github.com/antchfx/xmlquery v1.4.2 h1:MZKd9+wblwxfQ1zd1AdrTsqVaMjMCwow3IqkCSe00KA= +github.com/antchfx/xmlquery v1.4.2/go.mod h1:QXhvf5ldTuGqhd1SHNvvtlhhdQLks4dD0awIVhXIDTA= +github.com/antchfx/xpath v1.3.2 h1:LNjzlsSjinu3bQpw9hWMY9ocB80oLOWuQqFvO6xt51U= +github.com/antchfx/xpath v1.3.2/go.mod h1:i54GszH55fYfBmoZXapTHN8T8tkcHfRgLyVwwqzXNcs= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= @@ -26,8 +26,8 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ= -golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= +golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo= +golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -43,8 +43,8 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= -golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug= +golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= diff --git a/httpclient/multipartrequest.go b/httpclient/multipartrequest.go index 3d3a7a5..9b5c60c 100644 --- a/httpclient/multipartrequest.go +++ b/httpclient/multipartrequest.go @@ -64,7 +64,11 @@ type UploadState struct { // } // // // Use `result` or `resp` as needed -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) { +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) { + if encodingType != "byte" && encodingType != "base64" { + c.Sugar.Errorw("Invalid encoding type specified", zap.String("encodingType", encodingType)) + return nil, fmt.Errorf("invalid encoding type: %s. Must be 'byte' for rawBytes or 'base64' for base64 encoded content", encodingType) + } if method != http.MethodPost && method != http.MethodPut { c.Sugar.Error("HTTP method not supported for multipart request", zap.String("method", method)) @@ -89,20 +93,21 @@ func (c *Client) DoMultiPartRequest(method, endpoint string, files map[string][] var body io.Reader var contentType string - // Create multipart body in a function to ensure it runs again on retry createBody := func() error { var err error - body, contentType, err = createStreamingMultipartRequestBody(files, formDataFields, fileContentTypes, formDataPartHeaders, c.Sugar) + body, contentType, err = createStreamingMultipartRequestBody(files, formDataFields, fileContentTypes, formDataPartHeaders, encodingType, c.Sugar) if err != nil { c.Sugar.Errorw("Failed to create streaming multipart request body", zap.Error(err)) } else { - c.Sugar.Infow("Successfully created streaming multipart request body", zap.String("content_type", contentType)) + c.Sugar.Infow("Successfully created streaming multipart request body", + zap.String("content_type", contentType), + zap.String("encoding", encodingType)) } return err } if err := createBody(); err != nil { - c.Sugar.Errorw("Failed to create streaming multipart request body", zap.Error(err)) + c.Sugar.Errorw("Failed to create multipart request body", zap.Error(err)) return nil, err } @@ -112,23 +117,33 @@ func (c *Client) DoMultiPartRequest(method, endpoint string, files map[string][] return nil, err } - c.Sugar.Infow("Created HTTP Multipart request", zap.String("method", method), zap.String("url", url), zap.String("content_type", contentType)) + c.Sugar.Infow("Created HTTP Multipart request", + zap.String("method", method), + zap.String("url", url), + zap.String("content_type", contentType), + zap.String("encoding", encodingType)) (*c.Integration).PrepRequestParamsAndAuth(req) - req.Header.Set("Content-Type", contentType) startTime := time.Now() - resp, requestErr := c.http.Do(req) + resp, err := c.http.Do(req) duration := time.Since(startTime) - if requestErr != nil { - c.Sugar.Errorw("Failed to send request", zap.String("method", method), zap.String("endpoint", endpoint), zap.Error(requestErr)) - return nil, requestErr + if err != nil { + c.Sugar.Errorw("Failed to send request", + zap.String("method", method), + zap.String("endpoint", endpoint), + zap.Error(err)) + return nil, err } - c.Sugar.Debugw("Request sent successfully", zap.String("method", method), zap.String("endpoint", endpoint), zap.Int("status_code", resp.StatusCode), zap.Duration("duration", duration)) + c.Sugar.Debugw("Request sent successfully", + zap.String("method", method), + zap.String("endpoint", endpoint), + zap.Int("status_code", resp.StatusCode), + zap.Duration("duration", duration)) if resp.StatusCode >= 200 && resp.StatusCode < 300 { return resp, response.HandleAPISuccessResponse(resp, out, c.Sugar) @@ -158,7 +173,7 @@ func (c *Client) DoMultiPartRequest(method, endpoint string, files map[string][] // - string: The content type of the multipart request body. This includes the boundary string used by the multipart writer. // - error: An error object indicating failure during the construction of the multipart request body. This could be due to issues // such as file reading errors or multipart writer errors. -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) { +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) { pr, pw := io.Pipe() writer := multipart.NewWriter(pw) @@ -174,8 +189,11 @@ func createStreamingMultipartRequestBody(files map[string][]string, formDataFiel for fieldName, filePaths := range files { for _, filePath := range filePaths { - sugar.Debugw("Adding file part", zap.String("field_name", fieldName), zap.String("file_path", filePath)) - if err := addFilePart(writer, fieldName, filePath, fileContentTypes, formDataPartHeaders, sugar); err != nil { + sugar.Debugw("Adding file part", + zap.String("field_name", fieldName), + zap.String("file_path", filePath), + zap.String("encoding", encodingType)) + if err := addFilePartWithEncoding(writer, fieldName, filePath, fileContentTypes, formDataPartHeaders, encodingType, sugar); err != nil { sugar.Errorw("Failed to add file part", zap.Error(err)) pw.CloseWithError(err) return @@ -196,23 +214,19 @@ func createStreamingMultipartRequestBody(files map[string][]string, formDataFiel return pr, writer.FormDataContentType(), nil } -// addFilePart adds a base64 encoded file part to the multipart writer with the provided field name and file path. -// This function opens the specified file, sets the appropriate content type and headers, and adds it to the multipart writer. +// addFilePartWithEncoding adds a file part to the multipart writer with specified encoding. // Parameters: -// - writer: The multipart writer used to construct the multipart request body. -// - fieldName: The field name for the file part. -// - filePath: The path to the file to be included in the request. -// - fileContentTypes: A map specifying the content type for each file part. The key is the field name and the value is the -// content type (e.g., "image/jpeg"). -// - formDataPartHeaders: A map specifying custom headers for each part of the multipart form data. The key is the field name -// and the value is an http.Header containing the headers for that part. -// - sugar: An instance of a logger implementing the logger.Logger interface, used to sugar informational messages, warnings, -// and errors encountered during the addition of the file part. +// - writer: The multipart writer used to construct the request body +// - fieldName: The name of the form field for this file part +// - filePath: Path to the file to be uploaded +// - fileContentTypes: Map of content types for each file field +// - formDataPartHeaders: Map of custom headers for each form field +// - encodingType: The encoding to use ('byte' for raw bytes or 'base64' for base64 encoding) +// - sugar: Logger for progress and debug information // // Returns: -// - error: An error object indicating failure during the addition of the file part. This could be due to issues such as -// file reading errors or multipart writer errors. -func addFilePart(writer *multipart.Writer, fieldName, filePath string, fileContentTypes map[string]string, formDataPartHeaders map[string]http.Header, sugar *zap.SugaredLogger) error { +// - error: Any error encountered during the file part creation or upload process +func addFilePartWithEncoding(writer *multipart.Writer, fieldName, filePath string, fileContentTypes map[string]string, formDataPartHeaders map[string]http.Header, encodingType string, sugar *zap.SugaredLogger) error { file, err := os.Open(filePath) if err != nil { sugar.Errorw("Failed to open file", zap.String("filePath", filePath), zap.Error(err)) @@ -220,13 +234,16 @@ func addFilePart(writer *multipart.Writer, fieldName, filePath string, fileConte } defer file.Close() - // Default fileContentType contentType := "application/octet-stream" if ct, ok := fileContentTypes[fieldName]; ok { contentType = ct } - header := setFormDataPartHeader(fieldName, filepath.Base(filePath), contentType, formDataPartHeaders[fieldName]) + header := createFilePartHeader(fieldName, filePath, contentType, formDataPartHeaders[fieldName], encodingType) + sugar.Debugw("Created file part header", + zap.String("fieldName", fieldName), + zap.String("contentType", contentType), + zap.String("encoding", encodingType)) part, err := writer.CreatePart(header) if err != nil { @@ -234,9 +251,6 @@ func addFilePart(writer *multipart.Writer, fieldName, filePath string, fileConte return err } - encoder := base64.NewEncoder(base64.StdEncoding, part) - defer encoder.Close() - fileSize, err := file.Stat() if err != nil { sugar.Errorw("Failed to get file info", zap.String("filePath", filePath), zap.Error(err)) @@ -245,12 +259,43 @@ func addFilePart(writer *multipart.Writer, fieldName, filePath string, fileConte progressLogger := logUploadProgress(file, fileSize.Size(), sugar) uploadState := &UploadState{} - if err := chunkFileUpload(file, encoder, progressLogger, uploadState, sugar); err != nil { - sugar.Errorw("Failed to copy file content", zap.String("filePath", filePath), zap.Error(err)) - return err + + var writeTarget io.Writer = part + if encodingType == "base64" { + encoder := base64.NewEncoder(base64.StdEncoding, part) + defer encoder.Close() + writeTarget = encoder + sugar.Debugw("Using base64 encoding for file upload", zap.String("fieldName", fieldName)) + } else { + sugar.Debugw("Using raw encoding for file upload", zap.String("fieldName", fieldName)) } - return nil + return chunkFileUpload(file, writeTarget, progressLogger, uploadState, sugar) +} + +// createFilePartHeader creates the MIME header for a file part with the specified encoding type. +// Parameters: +// - fieldname: The name of the form field +// - filename: The name of the file being uploaded +// - contentType: The content type of the file +// - customHeaders: Additional headers to include in the part +// - encodingType: The encoding being used ('byte' or 'base64') +// +// Returns: +// - textproto.MIMEHeader: The constructed MIME header for the file part +func createFilePartHeader(fieldname, filename, contentType string, customHeaders http.Header, encodingType string) textproto.MIMEHeader { + header := textproto.MIMEHeader{} + header.Set("Content-Disposition", fmt.Sprintf(`form-data; name="%s"; filename="%s"`, fieldname, filepath.Base(filename))) + header.Set("Content-Type", contentType) + if encodingType == "base64" { + header.Set("Content-Transfer-Encoding", "base64") + } + for key, values := range customHeaders { + for _, value := range values { + header.Add(key, value) + } + } + return header } // addFormField adds a form field to the multipart writer with the provided key and value. @@ -278,31 +323,6 @@ func addFormField(writer *multipart.Writer, key, val string, sugar *zap.SugaredL return nil } -// setFormDataPartHeader creates a textproto.MIMEHeader for a form data field with the provided field name, file name, content type, and custom headers. -// This function constructs the MIME headers for a multipart form data part, including the content disposition, content type, -// and any custom headers specified. -// Parameters: -// - fieldname: The name of the form field. -// - filename: The name of the file being uploaded (if applicable). -// - contentType: The content type of the form data part (e.g., "image/jpeg"). -// - 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 -// header value. -// -// Returns: -// - textproto.MIMEHeader: The constructed MIME header for the form data part. -func setFormDataPartHeader(fieldname, filename, contentType string, customHeaders http.Header) textproto.MIMEHeader { - header := textproto.MIMEHeader{} - header.Set("Content-Disposition", fmt.Sprintf(`form-data; name="%s"; filename="%s"`, fieldname, filename)) - header.Set("Content-Type", contentType) - header.Set("Content-Transfer-Encoding", "base64") - for key, values := range customHeaders { - for _, value := range values { - header.Add(key, value) - } - } - return header -} - // chunkFileUpload reads the file upload into chunks and writes it to the writer. // This function reads the file in chunks and writes it to the provided writer, allowing for progress logging during the upload. // 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.