@@ -32,6 +32,7 @@ import (
32
32
"path/filepath"
33
33
"regexp"
34
34
"strings"
35
+ "sync"
35
36
"time"
36
37
37
38
"github.com/fatih/color"
@@ -68,6 +69,20 @@ func getCacheKeyForURL(archiveUrl string) string {
68
69
return hex .EncodeToString (urlHash [:16 ])
69
70
}
70
71
72
+ // getArchiveMutex returns a mutex for the given cache key, creating one if necessary
73
+ func getArchiveMutex (cacheKey string ) * sync.Mutex {
74
+ archiveCacheMutexesLock .Lock ()
75
+ defer archiveCacheMutexesLock .Unlock ()
76
+
77
+ if mutex , exists := archiveCacheMutexes [cacheKey ]; exists {
78
+ return mutex
79
+ }
80
+
81
+ mutex := & sync.Mutex {}
82
+ archiveCacheMutexes [cacheKey ] = mutex
83
+ return mutex
84
+ }
85
+
71
86
type GitPackage struct {
72
87
Source * deps.Git
73
88
}
84
99
GlobalCacheEnabled = true
85
100
// DefaultGlobalCacheDir is the default location for the global cache
86
101
DefaultGlobalCacheDir = ""
102
+
103
+ // globalCacheMutex protects concurrent access to the global cache
104
+ globalCacheMutex sync.Mutex
105
+ // archiveCacheMutexes protects concurrent downloads of the same archive
106
+ archiveCacheMutexes = make (map [string ]* sync.Mutex )
107
+ archiveCacheMutexesLock sync.Mutex
87
108
)
88
109
110
+ // validateGzipFile checks if a file is a valid gzip file by reading through the entire archive
111
+ func validateGzipFile (filepath string ) error {
112
+ file , err := os .Open (filepath )
113
+ if err != nil {
114
+ return fmt .Errorf ("failed to open file: %w" , err )
115
+ }
116
+ defer file .Close ()
117
+
118
+ // Get file info for size
119
+ info , err := file .Stat ()
120
+ if err != nil {
121
+ return fmt .Errorf ("failed to stat file: %w" , err )
122
+ }
123
+
124
+ // Try to read the gzip header
125
+ gr , err := gzip .NewReader (file )
126
+ if err != nil {
127
+ return fmt .Errorf ("invalid gzip header: %w" , err )
128
+ }
129
+ defer gr .Close ()
130
+
131
+ // Read through the entire tar archive to ensure it's not corrupted
132
+ tr := tar .NewReader (gr )
133
+ entriesProcessed := 0
134
+ totalSize := int64 (0 )
135
+
136
+ for {
137
+ header , err := tr .Next ()
138
+ if err == io .EOF {
139
+ // Reached end of archive
140
+ break
141
+ }
142
+ if err != nil {
143
+ return fmt .Errorf ("corrupted tar entry at position %d: %w" , entriesProcessed , err )
144
+ }
145
+
146
+ if header == nil {
147
+ continue
148
+ }
149
+
150
+ entriesProcessed ++
151
+
152
+ // For regular files, read through the content to ensure it's not corrupted
153
+ if header .Typeflag == tar .TypeReg {
154
+ // Use io.CopyN to read the exact file size
155
+ n , err := io .CopyN (io .Discard , tr , header .Size )
156
+ if err != nil {
157
+ return fmt .Errorf ("corrupted file content for %s (read %d of %d bytes): %w" ,
158
+ header .Name , n , header .Size , err )
159
+ }
160
+ totalSize += n
161
+ }
162
+ }
163
+
164
+ if entriesProcessed == 0 {
165
+ return fmt .Errorf ("tar archive is empty" )
166
+ }
167
+
168
+ // Archive is valid - we successfully read through all entries
169
+ if ! GitQuiet {
170
+ color .Green ("Archive validation passed (size: %d bytes, entries: %d, content size: %d bytes)" ,
171
+ info .Size (), entriesProcessed , totalSize )
172
+ }
173
+
174
+ return nil
175
+ }
176
+
89
177
func downloadGitHubArchive (filepath string , urlStr string ) error {
90
178
// Check if this is an S3 URL
91
179
if s3 .IsS3URL (urlStr ) {
@@ -151,6 +239,12 @@ func downloadGitHubArchive(filepath string, urlStr string) error {
151
239
// Success - proceed with download
152
240
defer resp .Body .Close ()
153
241
242
+ // Get ETag for integrity verification if available
243
+ etag := resp .Header .Get ("ETag" )
244
+ if etag != "" && ! GitQuiet {
245
+ color .Cyan ("Download ETag: %s" , etag )
246
+ }
247
+
154
248
// Create the file
155
249
out , err := os .Create (filepath )
156
250
if err != nil {
@@ -159,9 +253,13 @@ func downloadGitHubArchive(filepath string, urlStr string) error {
159
253
}
160
254
defer out .Close ()
161
255
162
- // Write the body to file
163
- _ , err = io .Copy (out , resp .Body )
256
+ // Write the body to file with a hash calculator
257
+ hasher := sha256 .New ()
258
+ writer := io .MultiWriter (out , hasher )
259
+ written , err := io .Copy (writer , resp .Body )
164
260
resp .Body .Close ()
261
+ out .Close () // Close the file explicitly before validation
262
+
165
263
if err != nil {
166
264
os .Remove (filepath ) // Clean up partial file
167
265
lastErr = err
@@ -171,6 +269,37 @@ func downloadGitHubArchive(filepath string, urlStr string) error {
171
269
continue
172
270
}
173
271
272
+ // Calculate file hash
273
+ fileHash := fmt .Sprintf ("%x" , hasher .Sum (nil ))
274
+ if ! GitQuiet {
275
+ color .Cyan ("Downloaded file SHA256: %s" , fileHash )
276
+ }
277
+
278
+ // Verify file size if Content-Length was provided
279
+ if contentLength := resp .ContentLength ; contentLength > 0 && written != contentLength {
280
+ os .Remove (filepath )
281
+ lastErr = fmt .Errorf ("incomplete download: expected %d bytes, got %d" , contentLength , written )
282
+ if ! GitQuiet {
283
+ color .Yellow ("Download attempt %d/%d incomplete: %v" , attempt , maxRetries , lastErr )
284
+ }
285
+ continue
286
+ }
287
+
288
+ // Validate it's a valid gzip file by reading through the entire archive
289
+ if err := validateGzipFile (filepath ); err != nil {
290
+ os .Remove (filepath )
291
+ // Check if this is an EOF error which might indicate truncation
292
+ if strings .Contains (err .Error (), "unexpected EOF" ) || strings .Contains (err .Error (), "EOF" ) {
293
+ lastErr = fmt .Errorf ("archive appears to be truncated or corrupted: %w" , err )
294
+ } else {
295
+ lastErr = fmt .Errorf ("invalid archive: %w" , err )
296
+ }
297
+ if ! GitQuiet {
298
+ color .Yellow ("Download attempt %d/%d produced invalid archive: %v" , attempt , maxRetries , err )
299
+ }
300
+ continue
301
+ }
302
+
174
303
// Success!
175
304
return nil
176
305
} else if resp .StatusCode >= 500 || resp .StatusCode == 429 {
@@ -239,7 +368,15 @@ func getGlobalCacheDir() (string, error) {
239
368
// 2. Remote caches (if configured)
240
369
// 3. Upstream source (if all else fails)
241
370
func ensureArchiveCache (archiveFilepath , archiveUrl string ) error {
242
- // Check if file already exists at the destination
371
+ // Create a unique key based on the URL
372
+ cacheKey := getCacheKeyForURL (archiveUrl )
373
+
374
+ // Get a mutex for this specific archive to prevent concurrent downloads
375
+ archiveMutex := getArchiveMutex (cacheKey )
376
+ archiveMutex .Lock ()
377
+ defer archiveMutex .Unlock ()
378
+
379
+ // Check if file already exists at the destination (double-check after acquiring lock)
243
380
if _ , err := os .Stat (archiveFilepath ); err == nil {
244
381
if ! GitQuiet {
245
382
color .Green ("FILE ALREADY EXISTS %s" , archiveFilepath )
@@ -249,9 +386,6 @@ func ensureArchiveCache(archiveFilepath, archiveUrl string) error {
249
386
return err
250
387
}
251
388
252
- // Create a unique key based on the URL
253
- cacheKey := getCacheKeyForURL (archiveUrl )
254
-
255
389
// Step 1: If global cache is enabled, check the global cache
256
390
if GlobalCacheEnabled {
257
391
globalCacheDir , err := getGlobalCacheDir ()
@@ -549,6 +683,10 @@ DownloadToDestination:
549
683
550
684
// registerInGlobalCacheIndex adds an entry to the global cache index
551
685
func registerInGlobalCacheIndex (filePath , url string ) {
686
+ // Lock for global cache index operations
687
+ globalCacheMutex .Lock ()
688
+ defer globalCacheMutex .Unlock ()
689
+
552
690
// Create a unique key from the URL
553
691
cacheKey := getCacheKeyForURL (url )
554
692
@@ -783,26 +921,44 @@ func registerInGlobalCacheIndex(filePath, url string) {
783
921
func gzipUntar (dst string , r io.Reader , subDir string ) error {
784
922
gzr , err := gzip .NewReader (r )
785
923
if err != nil {
786
- return err
924
+ return fmt . Errorf ( "failed to create gzip reader: %w" , err )
787
925
}
788
926
defer gzr .Close ()
789
927
790
928
subDirWithoutSlash := strings .TrimPrefix (subDir , "/" )
791
929
792
930
tr := tar .NewReader (gzr )
793
931
932
+ entriesProcessed := 0
933
+ bytesProcessed := int64 (0 )
934
+ var lastEntryName string
935
+
794
936
for {
795
937
header , err := tr .Next ()
796
938
switch {
797
939
case err == io .EOF :
940
+ if entriesProcessed == 0 {
941
+ return fmt .Errorf ("tar archive appears to be empty" )
942
+ }
943
+ if ! GitQuiet {
944
+ color .Green ("Successfully extracted %d entries (content size: %d bytes)" , entriesProcessed , bytesProcessed )
945
+ }
798
946
return nil
799
947
800
948
case err != nil :
801
- return err
949
+ // Provide detailed error context
950
+ errMsg := fmt .Sprintf ("failed to read tar entry #%d" , entriesProcessed + 1 )
951
+ if lastEntryName != "" {
952
+ errMsg += fmt .Sprintf (" (after '%s')" , lastEntryName )
953
+ }
954
+ errMsg += fmt .Sprintf (", extracted %d bytes of content so far" , bytesProcessed )
955
+ return fmt .Errorf ("%s: %w" , errMsg , err )
802
956
803
957
case header == nil :
804
958
continue
805
959
}
960
+ entriesProcessed ++
961
+ lastEntryName = header .Name
806
962
807
963
// strip the two first components of the path
808
964
parts := strings .SplitAfterN (header .Name , "/" , 2 )
@@ -847,9 +1003,16 @@ func gzipUntar(dst string, r io.Reader, subDir string) error {
847
1003
}
848
1004
defer f .Close ()
849
1005
850
- // copy over contents
851
- if _ , err := io .Copy (f , tr ); err != nil {
852
- return err
1006
+ // copy over contents and track bytes
1007
+ written , err := io .Copy (f , tr )
1008
+ if err != nil {
1009
+ return fmt .Errorf ("failed to extract %s: %w" , header .Name , err )
1010
+ }
1011
+ bytesProcessed += written
1012
+
1013
+ // Verify we read the expected amount
1014
+ if written != header .Size {
1015
+ return fmt .Errorf ("file %s: size mismatch (expected %d bytes, got %d)" , header .Name , header .Size , written )
853
1016
}
854
1017
return nil
855
1018
}()
@@ -928,8 +1091,10 @@ func (p *GitPackage) Install(ctx context.Context, name, dir, version string) (st
928
1091
return commitSha , nil
929
1092
}
930
1093
// Fall back to git clone on error
931
- color .Yellow ("archive install failed: %s" , err )
932
- color .Yellow ("falling back to git clone..." )
1094
+ if ! GitQuiet {
1095
+ color .Yellow ("archive install failed: %v" , err )
1096
+ color .Yellow ("falling back to git clone..." )
1097
+ }
933
1098
}
934
1099
935
1100
// Try to use global cache or fall back to git clone
@@ -982,10 +1147,20 @@ func (p *GitPackage) resolveVersionToCommitSHA(ctx context.Context, version stri
982
1147
983
1148
// extractArchiveToDestination extracts a downloaded archive to the destination path
984
1149
func (p * GitPackage ) extractArchiveToDestination (archiveFilepath , destPath , commitSha string ) (string , error ) {
1150
+ // Get file info for debugging before opening
1151
+ info , err := os .Stat (archiveFilepath )
1152
+ if err != nil {
1153
+ return "" , fmt .Errorf ("failed to stat archive file before extraction: %w" , err )
1154
+ }
1155
+
1156
+ if ! GitQuiet {
1157
+ color .Cyan ("Extracting archive: %s (size: %d bytes)" , archiveFilepath , info .Size ())
1158
+ }
1159
+
985
1160
// Open the archive file
986
1161
ar , err := os .Open (archiveFilepath )
987
1162
if err != nil {
988
- return "" , err
1163
+ return "" , fmt . Errorf ( "failed to open archive file: %w" , err )
989
1164
}
990
1165
defer ar .Close ()
991
1166
@@ -997,7 +1172,16 @@ func (p *GitPackage) extractArchiveToDestination(archiveFilepath, destPath, comm
997
1172
// Extract the sub-directory (if any) from the archive to the final destination
998
1173
err = gzipUntar (destPath , ar , p .Source .Subdir )
999
1174
if err != nil {
1000
- return "" , err
1175
+ // Provide more context about the error
1176
+ if strings .Contains (err .Error (), "unexpected EOF" ) {
1177
+ // Re-validate the archive to get more details
1178
+ ar .Close ()
1179
+ if validateErr := validateGzipFile (archiveFilepath ); validateErr != nil {
1180
+ return "" , fmt .Errorf ("archive validation failed after extraction error - archive may be corrupted or truncated (size: %d bytes): %w" , info .Size (), validateErr )
1181
+ }
1182
+ return "" , fmt .Errorf ("extraction failed with unexpected EOF - archive may be truncated (size: %d bytes): %w" , info .Size (), err )
1183
+ }
1184
+ return "" , fmt .Errorf ("failed to extract archive %s (size: %d bytes): %w" , archiveFilepath , info .Size (), err )
1001
1185
}
1002
1186
1003
1187
return commitSha , nil
0 commit comments