@@ -2,13 +2,15 @@ package compose
2
2
3
3
import (
4
4
"archive/tar"
5
+ "archive/zip"
5
6
"bytes"
6
7
"compress/gzip"
7
8
"context"
8
9
"crypto/sha256"
9
10
"encoding/base64"
10
11
"fmt"
11
12
"io"
13
+ "io/fs"
12
14
"os"
13
15
"path/filepath"
14
16
"strings"
@@ -72,6 +74,94 @@ defang.exe
72
74
.defang`
73
75
)
74
76
77
+ type ArchiveType string
78
+
79
+ const ArchiveTypeZip ArchiveType = "application/zip"
80
+ const ArchiveTypeGzip ArchiveType = "application/gzip"
81
+
82
+ type WriterFactory interface {
83
+ CreateHeader (info fs.FileInfo , slashPath string ) (io.Writer , error )
84
+ Close () error
85
+ }
86
+
87
+ type tarFactory struct {
88
+ * tar.Writer
89
+ gzipWriter io.WriteCloser
90
+ }
91
+
92
+ func (tw * tarFactory ) CreateHeader (info fs.FileInfo , slashPath string ) (io.Writer , error ) {
93
+ // Convert zip header to tar header
94
+ header , err := tar .FileInfoHeader (info , info .Name ())
95
+ if err != nil {
96
+ return nil , err
97
+ }
98
+
99
+ if ! info .Mode ().IsRegular () {
100
+ return nil , nil
101
+ }
102
+
103
+ // Make reproducible; WalkDir walks files in lexical order.
104
+ header .ModTime = time .Unix (sourceDateEpoch , 0 )
105
+ header .Gid = 0
106
+ header .Uid = 0
107
+ header .Name = slashPath
108
+ err = tw .WriteHeader (header )
109
+ return tw .Writer , err
110
+ }
111
+
112
+ func (tw * tarFactory ) Close () error {
113
+ // Close the tar and gzip writers before returning the buffer
114
+ err := tw .Writer .Close ()
115
+ if err != nil {
116
+ return err
117
+ }
118
+
119
+ err = tw .gzipWriter .Close ()
120
+ if err != nil {
121
+ return err
122
+ }
123
+
124
+ return nil
125
+ }
126
+
127
+ type zipFactory struct {
128
+ * zip.Writer
129
+ }
130
+
131
+ func (zw * zipFactory ) CreateHeader (info fs.FileInfo , slashPath string ) (io.Writer , error ) {
132
+ // Create a new zip file header
133
+ header := & zip.FileHeader {
134
+ Name : slashPath ,
135
+ Method : zip .Deflate ,
136
+ }
137
+
138
+ // Make reproducible
139
+ header .Modified = time .Unix (sourceDateEpoch , 0 )
140
+
141
+ if ! info .Mode ().IsRegular () {
142
+ if info .IsDir () {
143
+ header .Name = slashPath + "/" // Ensure directory paths end with slash
144
+ header .Method = zip .Store // Directories are stored without compression
145
+ _ , err := zw .Writer .CreateHeader (header )
146
+ return nil , err
147
+ }
148
+ return nil , nil
149
+ }
150
+
151
+ // Create file entry in zip
152
+ writer , err := zw .Writer .CreateHeader (header )
153
+ return writer , err
154
+ }
155
+
156
+ func (zw * zipFactory ) Close () error {
157
+ // Close the zip writer before returning the buffer
158
+ err := zw .Writer .Close ()
159
+ if err != nil {
160
+ return err
161
+ }
162
+ return nil
163
+ }
164
+
75
165
func parseContextLimit (limit string , def int64 ) int64 {
76
166
if size , err := units .RAMInBytes (limit ); err == nil {
77
167
return size
@@ -89,17 +179,26 @@ func getRemoteBuildContext(ctx context.Context, provider client.Provider, projec
89
179
return "" , fmt .Errorf ("invalid build context: %w" , err ) // already checked in ValidateProject
90
180
}
91
181
182
+ var archiveType ArchiveType
183
+ // If we have a Railpack build, we use a zip archive
184
+ if build .Dockerfile == RAILPACK {
185
+ archiveType = ArchiveTypeZip
186
+ // We use tar for all other builds
187
+ } else {
188
+ archiveType = ArchiveTypeGzip
189
+ }
190
+
92
191
switch upload {
93
192
case UploadModeIgnore :
94
- // `compose config`, ie. dry-run: don't upload the tarball , just return the path as-is
193
+ // `compose config`, ie. dry-run: don't upload the archive , just return the path as-is
95
194
return root , nil
96
195
case UploadModeEstimate :
97
196
// For estimation, we don't bother packaging the files, we just return a placeholder URL
98
197
return fmt .Sprintf ("s3://cd-preview/%v" , time .Now ().Unix ()), nil
99
198
}
100
199
101
200
term .Info ("Packaging the project files for" , name , "at" , root )
102
- buffer , err := createTarball (ctx , build .Context , build .Dockerfile )
201
+ buffer , err := createArchive (ctx , build .Context , build .Dockerfile , archiveType )
103
202
if err != nil {
104
203
return "" , err
105
204
}
@@ -121,19 +220,19 @@ func getRemoteBuildContext(ctx context.Context, provider client.Provider, projec
121
220
}
122
221
123
222
term .Info ("Uploading the project files for" , name )
124
- return uploadTarball (ctx , provider , project , buffer , digest )
223
+ return uploadArchive (ctx , provider , project , buffer , archiveType , digest )
125
224
}
126
225
127
- func uploadTarball (ctx context.Context , provider client.Provider , project string , body io.Reader , digest string ) (string , error ) {
128
- // Upload the tarball to the fabric controller storage;; TODO: use a streaming API
226
+ func uploadArchive (ctx context.Context , provider client.Provider , project string , body io.Reader , contentType ArchiveType , digest string ) (string , error ) {
227
+ // Upload the archive to the fabric controller storage;; TODO: use a streaming API
129
228
ureq := & defangv1.UploadURLRequest {Digest : digest , Project : project }
130
229
res , err := provider .CreateUploadURL (ctx , ureq )
131
230
if err != nil {
132
231
return "" , err
133
232
}
134
233
135
234
// Do an HTTP PUT to the generated URL
136
- resp , err := http .Put (ctx , res .Url , "application/gzip" , body )
235
+ resp , err := http .Put (ctx , res .Url , string ( contentType ) , body )
137
236
if err != nil {
138
237
return "" , err
139
238
}
@@ -150,17 +249,17 @@ func uploadTarball(ctx context.Context, provider client.Provider, project string
150
249
return url , nil
151
250
}
152
251
153
- type contextAwareWriter struct {
252
+ type contextAwareReader struct {
154
253
ctx context.Context
155
- io.WriteCloser
254
+ io.ReadCloser
156
255
}
157
256
158
- func (cw contextAwareWriter ) Write (p []byte ) (n int , err error ) {
257
+ func (cr contextAwareReader ) Read (p []byte ) (n int , err error ) {
159
258
select {
160
- case <- cw .ctx .Done (): // Detect context cancelation
161
- return 0 , cw .ctx .Err ()
259
+ case <- cr .ctx .Done (): // Detect context cancelation
260
+ return 0 , cr .ctx .Err ()
162
261
default :
163
- return cw . WriteCloser . Write (p )
262
+ return cr . ReadCloser . Read (p )
164
263
}
165
264
}
166
265
@@ -229,7 +328,6 @@ func getDockerIgnorePatterns(root, dockerfile string) ([]string, string, error)
229
328
}
230
329
231
330
func WalkContextFolder (root , dockerfile string , fn func (path string , de os.DirEntry , slashPath string ) error ) error {
232
- foundDockerfile := false
233
331
if dockerfile == "" {
234
332
dockerfile = "Dockerfile"
235
333
} else {
@@ -267,7 +365,6 @@ func WalkContextFolder(root, dockerfile string, fn func(path string, de os.DirEn
267
365
268
366
// we need the Dockerfile, even if it's in the .dockerignore file
269
367
if relPath == dockerfile {
270
- foundDockerfile = true
271
368
} else if relPath == dockerignore {
272
369
// we need the .dockerignore file too: it might ignore itself and/or the Dockerfile, but is needed by the builder
273
370
} else {
@@ -291,19 +388,23 @@ func WalkContextFolder(root, dockerfile string, fn func(path string, de os.DirEn
291
388
return err
292
389
}
293
390
294
- if ! foundDockerfile {
295
- return fmt .Errorf ("the specified dockerfile could not be read: %q" , dockerfile )
296
- }
297
-
298
391
return nil
299
392
}
300
393
301
- func createTarball (ctx context.Context , root , dockerfile string ) (* bytes.Buffer , error ) {
394
+ func createArchive (ctx context.Context , root string , dockerfile string , contentType ArchiveType ) (* bytes.Buffer , error ) {
302
395
fileCount := 0
303
396
// TODO: use io.Pipe and do proper streaming (instead of buffering everything in memory)
397
+
304
398
buf := & bytes.Buffer {}
305
- gzipWriter := & contextAwareWriter {ctx , gzip .NewWriter (buf )}
306
- tarWriter := tar .NewWriter (gzipWriter )
399
+ var factory WriterFactory
400
+ if contentType == ArchiveTypeZip {
401
+ zipWriter := zip .NewWriter (buf )
402
+ factory = & zipFactory {zipWriter }
403
+ } else {
404
+ gzipWriter := gzip .NewWriter (buf )
405
+ tarWriter := tar .NewWriter (gzipWriter )
406
+ factory = & tarFactory {tarWriter , gzipWriter }
407
+ }
307
408
308
409
doProgress := term .StdoutCanColor () && term .IsTerminal ()
309
410
err := WalkContextFolder (root , dockerfile , func (path string , de os.DirEntry , slashPath string ) error {
@@ -319,38 +420,27 @@ func createTarball(ctx context.Context, root, dockerfile string) (*bytes.Buffer,
319
420
return err
320
421
}
321
422
322
- header , err := tar .FileInfoHeader (info , info .Name ())
323
- if err != nil {
324
- return err
325
- }
326
-
327
- // Make reproducible; WalkDir walks files in lexical order.
328
- header .ModTime = time .Unix (sourceDateEpoch , 0 )
329
- header .Gid = 0
330
- header .Uid = 0
331
- header .Name = slashPath
332
- err = tarWriter .WriteHeader (header )
333
- if err != nil {
423
+ writer , err := factory .CreateHeader (info , slashPath )
424
+ if err != nil || writer == nil {
334
425
return err
335
426
}
336
427
337
- if ! info .Mode ().IsRegular () {
338
- return nil
339
- }
340
-
341
428
file , err := os .Open (path )
342
429
if err != nil {
343
430
return err
344
431
}
345
432
defer file .Close ()
346
433
434
+ // Wrap the file reader with context-aware reader
435
+ contextReader := & contextAwareReader {ctx , file }
436
+
347
437
fileCount ++
348
438
if fileCount == ContextFileLimit + 1 {
349
439
term .Warnf ("the build context contains more than %d files; use --debug or create .dockerignore to exclude caches and build artifacts" , ContextFileLimit )
350
440
}
351
441
352
442
bufLen := buf .Len ()
353
- _ , err = io .Copy (tarWriter , file )
443
+ _ , err = io .Copy (writer , contextReader )
354
444
if int64 (buf .Len ()) > ContextSizeHardLimit {
355
445
return fmt .Errorf ("the build context is limited to %s; consider downloading large files in the Dockerfile or set the DEFANG_BUILD_CONTEXT_LIMIT environment variable" , units .BytesSize (float64 (ContextSizeHardLimit )))
356
446
}
@@ -364,12 +454,8 @@ func createTarball(ctx context.Context, root, dockerfile string) (*bytes.Buffer,
364
454
return nil , err
365
455
}
366
456
367
- // Close the tar and gzip writers before returning the buffer
368
- if err = tarWriter .Close (); err != nil {
369
- return nil , err
370
- }
371
-
372
- if err = gzipWriter .Close (); err != nil {
457
+ err = factory .Close () // Close the tar or zip writer
458
+ if err != nil {
373
459
return nil , err
374
460
}
375
461
0 commit comments