Repository navigation
Expand file tree
/
Copy pathmultipart.go
More file actions
163 lines (144 loc) · 4.47 KB
/
Copy pathmultipart.go
File metadata and controls
163 lines (144 loc) · 4.47 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
package httpstream
import (
"context"
"fmt"
"io"
"mime/multipart"
"net/http"
"strconv"
"strings"
"time"
)
// multipartField represents a field in a multipart form
type multipartField struct {
contentType contentType
key, value string
file io.Reader
}
// Multipart provides a streaming multipart/form-data builder for HTTP requests.
type Multipart struct {
client http.Client
request *http.Request
fields []multipartField
cancelFunc context.CancelFunc
}
// NewMultipart creates a new streaming multipart/form-data request builder.
func NewMultipart(ctx context.Context, client http.Client, method, url string) *Multipart {
request, _ := http.NewRequestWithContext(ctx, method, url, nil)
return &Multipart{
client: client,
request: request,
fields: make([]multipartField, 0, 16),
}
}
func (r *Multipart) Use(middleware func(http.RoundTripper) http.RoundTripper) *Multipart {
if r.client.Transport == nil {
r.client.Transport = http.DefaultTransport
}
r.client.Transport = middleware(r.client.Transport)
return r
}
// Timeout sets a timeout for the request.
func (r *Multipart) Timeout(duration time.Duration) *Multipart {
ctx, cancel := context.WithTimeout(r.request.Context(), duration)
r.cancelFunc = cancel
r.request = r.request.WithContext(ctx)
return r
}
// Send executes the HTTP request and returns the response.
func (r *Multipart) Send() (*http.Response, error) {
ctx := r.request.Context()
// Pre-validate files to ensure they are not empty
for i, f := range r.fields {
if f.contentType == applicationOctetStream {
buf := make([]byte, 1)
n, err := f.file.Read(buf)
if err != nil && err != io.EOF {
return nil, fmt.Errorf("failed to read file %s: %w", f.value, err)
}
if n == 0 {
return nil, fmt.Errorf("empty file: %s", f.value)
}
r.fields[i].file = io.MultiReader(strings.NewReader(string(buf[:n])), f.file)
}
}
pr, pw := io.Pipe()
mw := multipart.NewWriter(pw)
r.request.Body = pr
r.request.Header.Set("Content-Type", mw.FormDataContentType())
go func() {
defer pw.Close()
defer mw.Close()
for _, f := range r.fields {
select {
case <-ctx.Done():
pw.CloseWithError(ctx.Err())
return
default:
}
var err error
switch f.contentType {
case multipartFormData:
err = mw.WriteField(f.key, f.value)
case applicationOctetStream:
var part io.Writer
if part, err = mw.CreateFormFile(f.key, f.value); err == nil {
_, err = io.Copy(part, f.file)
}
}
if err != nil {
pw.CloseWithError(err)
return
}
}
}()
return r.sendRequest()
}
func (r *Multipart) sendRequest() (*http.Response, error) {
resp, err := r.client.Do(r.request)
if err != nil {
if r.cancelFunc != nil {
r.cancelFunc()
}
return nil, err
}
if r.cancelFunc != nil {
resp.Body = &cancelCloser{resp.Body, r.cancelFunc}
r.cancelFunc = nil
}
return resp, nil
}
// Header sets an HTTP header on the request.
func (r *Multipart) Header(k, v string) *Multipart { r.request.Header.Set(k, v); return r }
// PathParam replaces a path variable placeholder in the URL.
func (r *Multipart) PathParam(key, val string) *Multipart {
r.request.URL.Path = strings.ReplaceAll(r.request.URL.Path, "{"+key+"}", val)
return r
}
func (r *Multipart) PathInt(k string, v int) *Multipart { return r.PathParam(k, strconv.Itoa(v)) }
func (r *Multipart) PathBool(k string, v bool) *Multipart {
return r.PathParam(k, strconv.FormatBool(v))
}
func (r *Multipart) PathFloat(k string, v float64) *Multipart {
return r.PathParam(k, strconv.FormatFloat(v, 'f', -1, 64))
}
// Param adds a string field to the multipart form.
func (r *Multipart) Param(k, v string) *Multipart {
r.fields = append(r.fields, multipartField{contentType: multipartFormData, key: k, value: v})
return r
}
func (r *Multipart) Int(k string, v int) *Multipart { return r.Param(k, strconv.Itoa(v)) }
func (r *Multipart) Bool(k string, v bool) *Multipart { return r.Param(k, strconv.FormatBool(v)) }
func (r *Multipart) Float(k string, v float64) *Multipart {
return r.Param(k, strconv.FormatFloat(v, 'f', -1, 64))
}
// File adds a file field to the multipart form.
func (r *Multipart) File(key, filename string, content io.Reader) *Multipart {
r.fields = append(r.fields, multipartField{contentType: applicationOctetStream, key: key, value: filename, file: content})
return r
}
// Cookie adds a cookie to the multipart request.
func (r *Multipart) Cookie(name, value string) *Multipart {
r.request.AddCookie(&http.Cookie{Name: name, Value: value})
return r
}