@@ -44,17 +44,20 @@ const FileSizeLimit = 50_000_000 // arbitrary number, chosen to support max size
44
44
// FileCountLimit specifies the maximum number of files allowed in a single upload.
45
45
const FileCountLimit = 100 // arbitrary number, will need to be re-evaluated
46
46
47
- // ContentType is the HTTP header name for content type.
48
- const ContentType = "Content-Type"
49
-
50
47
// NewClient creates a new file upload client with the given configuration and options.
51
48
func NewClient (cfg Config , opts ... Opt ) * HTTPClient {
52
- c := HTTPClient {cfg , http .DefaultClient }
49
+ httpClient := & http.Client {
50
+ Transport : http .DefaultTransport ,
51
+ }
52
+ c := HTTPClient {cfg , httpClient }
53
53
54
54
for _ , opt := range opts {
55
55
opt (& c )
56
56
}
57
57
58
+ crt := NewCompressionRoundTripper (c .httpClient .Transport )
59
+ c .httpClient .Transport = crt
60
+
58
61
return & c
59
62
}
60
63
@@ -80,7 +83,7 @@ func (c *HTTPClient) CreateRevision(ctx context.Context, orgID OrgID) (*UploadRe
80
83
url := fmt .Sprintf ("%s/hidden/orgs/%s/upload_revisions?version=%s" , c .cfg .BaseURL , orgID , APIVersion )
81
84
req , err := http .NewRequestWithContext (ctx , http .MethodPost , url , buff )
82
85
if err != nil {
83
- return nil , fmt .Errorf ("failed to create request body : %w" , err )
86
+ return nil , fmt .Errorf ("failed to create revision request : %w" , err )
84
87
}
85
88
req .Header .Set (ContentType , "application/vnd.api+json" )
86
89
@@ -91,7 +94,7 @@ func (c *HTTPClient) CreateRevision(ctx context.Context, orgID OrgID) (*UploadRe
91
94
defer res .Body .Close ()
92
95
93
96
if res .StatusCode != http .StatusCreated {
94
- return nil , c . handleUnexpectedStatusCodes (res .Body , res .StatusCode , res .Status , "create upload revision" )
97
+ return nil , handleUnexpectedStatusCodes (res .Body , res .StatusCode , res .Status , "create upload revision" )
95
98
}
96
99
97
100
var respBody UploadRevisionResponseBody
@@ -112,44 +115,23 @@ func (c *HTTPClient) UploadFiles(ctx context.Context, orgID OrgID, revisionID Re
112
115
return ErrEmptyRevisionID
113
116
}
114
117
115
- if len (files ) > FileCountLimit {
116
- return NewFileCountLimitError ( len ( files ), FileCountLimit )
118
+ if err := validateFiles (files ); err != nil {
119
+ return err
117
120
}
118
121
119
- if len (files ) == 0 {
120
- return ErrNoFilesProvided
121
- }
122
-
123
- for _ , file := range files {
124
- fileInfo , err := file .File .Stat ()
125
- if err != nil {
126
- return NewFileAccessError (file .Path , err )
127
- }
128
-
129
- if fileInfo .IsDir () {
130
- return NewDirectoryError (file .Path )
131
- }
122
+ // Create pipe for multipart data
123
+ pipeReader , pipeWriter := io .Pipe ()
124
+ defer pipeReader .Close ()
132
125
133
- if fileInfo .Size () > FileSizeLimit {
134
- return NewFileSizeLimitError (file .Path , fileInfo .Size (), FileSizeLimit )
135
- }
136
- }
137
-
138
- // Create pipe for streaming multipart data
139
- pReader , pWriter := io .Pipe ()
140
- mpartWriter := multipart .NewWriter (pWriter )
126
+ mpartWriter := multipart .NewWriter (pipeWriter )
141
127
142
- // Start goroutine to write multipart data
143
- go c .streamFilesToPipe (pWriter , mpartWriter , files )
128
+ go streamFilesToPipe (pipeWriter , mpartWriter , files )
144
129
145
- // Create HTTP request with streaming body
146
130
url := fmt .Sprintf ("%s/hidden/orgs/%s/upload_revisions/%s/files?version=%s" , c .cfg .BaseURL , orgID , revisionID , APIVersion )
147
- req , err := http .NewRequestWithContext (ctx , http .MethodPost , url , pReader )
131
+ req , err := http .NewRequestWithContext (ctx , http .MethodPost , url , pipeReader )
148
132
if err != nil {
149
- pReader .Close ()
150
133
return fmt .Errorf ("failed to create upload files request: %w" , err )
151
134
}
152
-
153
135
req .Header .Set (ContentType , mpartWriter .FormDataContentType ())
154
136
155
137
res , err := c .httpClient .Do (req )
@@ -159,18 +141,21 @@ func (c *HTTPClient) UploadFiles(ctx context.Context, orgID OrgID, revisionID Re
159
141
defer res .Body .Close ()
160
142
161
143
if res .StatusCode != http .StatusNoContent {
162
- return c . handleUnexpectedStatusCodes (res .Body , res .StatusCode , res .Status , "upload files" )
144
+ return handleUnexpectedStatusCodes (res .Body , res .StatusCode , res .Status , "upload files" )
163
145
}
164
146
165
147
return nil
166
148
}
167
149
168
- func (c * HTTPClient ) streamFilesToPipe (pWriter * io.PipeWriter , mpartWriter * multipart.Writer , files []UploadFile ) {
150
+ // streamFilesToPipe writes files to the multipart form.
151
+ func streamFilesToPipe (pipeWriter * io.PipeWriter , mpartWriter * multipart.Writer , files []UploadFile ) {
169
152
var streamError error
170
153
defer func () {
171
- pWriter .CloseWithError (streamError )
154
+ if closeErr := mpartWriter .Close (); closeErr != nil && streamError == nil {
155
+ streamError = closeErr
156
+ }
157
+ pipeWriter .CloseWithError (streamError )
172
158
}()
173
- defer mpartWriter .Close ()
174
159
175
160
for _ , file := range files {
176
161
// Create form file part
@@ -180,14 +165,41 @@ func (c *HTTPClient) streamFilesToPipe(pWriter *io.PipeWriter, mpartWriter *mult
180
165
return
181
166
}
182
167
183
- _ , err = io .Copy (part , file .File )
184
- if err != nil {
168
+ if _ , err := io .Copy (part , file .File ); err != nil {
185
169
streamError = fmt .Errorf ("failed to copy file content for %s: %w" , file .Path , err )
186
170
return
187
171
}
188
172
}
189
173
}
190
174
175
+ // validateFiles validates the files before upload.
176
+ func validateFiles (files []UploadFile ) error {
177
+ if len (files ) > FileCountLimit {
178
+ return NewFileCountLimitError (len (files ), FileCountLimit )
179
+ }
180
+
181
+ if len (files ) == 0 {
182
+ return ErrNoFilesProvided
183
+ }
184
+
185
+ for _ , file := range files {
186
+ fileInfo , err := file .File .Stat ()
187
+ if err != nil {
188
+ return NewFileAccessError (file .Path , err )
189
+ }
190
+
191
+ if fileInfo .IsDir () {
192
+ return NewDirectoryError (file .Path )
193
+ }
194
+
195
+ if fileInfo .Size () > FileSizeLimit {
196
+ return NewFileSizeLimitError (file .Path , fileInfo .Size (), FileSizeLimit )
197
+ }
198
+ }
199
+
200
+ return nil
201
+ }
202
+
191
203
// SealRevision seals the specified upload revision, marking it as complete.
192
204
func (c * HTTPClient ) SealRevision (ctx context.Context , orgID OrgID , revisionID RevisionID ) (* SealUploadRevisionResponseBody , error ) {
193
205
if orgID == uuid .Nil {
@@ -215,7 +227,7 @@ func (c *HTTPClient) SealRevision(ctx context.Context, orgID OrgID, revisionID R
215
227
url := fmt .Sprintf ("%s/hidden/orgs/%s/upload_revisions/%s?version=%s" , c .cfg .BaseURL , orgID , revisionID , APIVersion )
216
228
req , err := http .NewRequestWithContext (ctx , http .MethodPatch , url , buff )
217
229
if err != nil {
218
- return nil , fmt .Errorf ("failed to create request body : %w" , err )
230
+ return nil , fmt .Errorf ("failed to create seal request : %w" , err )
219
231
}
220
232
req .Header .Set (ContentType , "application/vnd.api+json" )
221
233
@@ -226,7 +238,7 @@ func (c *HTTPClient) SealRevision(ctx context.Context, orgID OrgID, revisionID R
226
238
defer res .Body .Close ()
227
239
228
240
if res .StatusCode != http .StatusOK {
229
- return nil , c . handleUnexpectedStatusCodes (res .Body , res .StatusCode , res .Status , "seal upload revision" )
241
+ return nil , handleUnexpectedStatusCodes (res .Body , res .StatusCode , res .Status , "seal upload revision" )
230
242
}
231
243
232
244
var respBody SealUploadRevisionResponseBody
@@ -237,7 +249,7 @@ func (c *HTTPClient) SealRevision(ctx context.Context, orgID OrgID, revisionID R
237
249
return & respBody , nil
238
250
}
239
251
240
- func ( c * HTTPClient ) handleUnexpectedStatusCodes (body io.ReadCloser , statusCode int , status , operation string ) error {
252
+ func handleUnexpectedStatusCodes (body io.ReadCloser , statusCode int , status , operation string ) error {
241
253
bts , err := io .ReadAll (body )
242
254
if err != nil {
243
255
return fmt .Errorf ("failed to read response body: %w" , err )
0 commit comments