@@ -26,10 +26,16 @@ extension Workspace {
26
26
public struct CustomBinaryArtifactsManager {
27
27
let httpClient : LegacyHTTPClient ?
28
28
let archiver : Archiver ?
29
+ let useCache : Bool ?
29
30
30
- public init ( httpClient: LegacyHTTPClient ? = . none, archiver: Archiver ? = . none) {
31
+ public init (
32
+ httpClient: LegacyHTTPClient ? = . none,
33
+ archiver: Archiver ? = . none,
34
+ useCache: Bool ? = . none
35
+ ) {
31
36
self . httpClient = httpClient
32
37
self . archiver = archiver
38
+ self . useCache = useCache
33
39
}
34
40
}
35
41
@@ -43,13 +49,15 @@ extension Workspace {
43
49
private let httpClient : LegacyHTTPClient
44
50
private let archiver : Archiver
45
51
private let checksumAlgorithm : HashAlgorithm
52
+ private let cachePath : AbsolutePath ?
46
53
private let delegate : Delegate ?
47
54
48
55
public init (
49
56
fileSystem: FileSystem ,
50
57
authorizationProvider: AuthorizationProvider ? ,
51
58
hostToolchain: UserToolchain ,
52
59
checksumAlgorithm: HashAlgorithm ,
60
+ cachePath: AbsolutePath ? ,
53
61
customHTTPClient: LegacyHTTPClient ? ,
54
62
customArchiver: Archiver ? ,
55
63
delegate: Delegate ?
@@ -60,6 +68,7 @@ extension Workspace {
60
68
self . checksumAlgorithm = checksumAlgorithm
61
69
self . httpClient = customHTTPClient ?? LegacyHTTPClient ( )
62
70
self . archiver = customArchiver ?? ZipArchiver ( fileSystem: fileSystem)
71
+ self . cachePath = cachePath
63
72
self . delegate = delegate
64
73
}
65
74
@@ -126,7 +135,7 @@ extension Workspace {
126
135
return ( local: localArtifacts, remote: remoteArtifacts)
127
136
}
128
137
129
- func download (
138
+ func fetch (
130
139
_ artifacts: [ RemoteArtifact ] ,
131
140
artifactsDirectory: AbsolutePath ,
132
141
observabilityScope: ObservabilityScope
@@ -229,37 +238,24 @@ extension Workspace {
229
238
}
230
239
231
240
group. enter ( )
232
- var headers = HTTPClientHeaders ( )
233
- headers. add ( name: " Accept " , value: " application/octet-stream " )
234
- var request = LegacyHTTPClient . Request. download (
235
- url: artifact. url,
236
- headers: headers,
237
- fileSystem: self . fileSystem,
238
- destination: archivePath
239
- )
240
- request. options. authorizationProvider = self . authorizationProvider? . httpAuthorizationHeader ( for: )
241
- request. options. retryStrategy = . exponentialBackoff( maxAttempts: 3 , baseDelay: . milliseconds( 50 ) )
242
- request. options. validResponseCodes = [ 200 ]
243
-
244
- let downloadStart : DispatchTime = . now( )
245
- self . delegate? . willDownloadBinaryArtifact ( from: artifact. url. absoluteString)
246
- observabilityScope. emit ( debug: " downloading \( artifact. url) to \( archivePath) " )
247
- self . httpClient. execute (
248
- request,
241
+ let fetchStart : DispatchTime = . now( )
242
+ self . fetch (
243
+ artifact: artifact,
244
+ destination: archivePath,
245
+ observabilityScope: observabilityScope,
249
246
progress: { bytesDownloaded, totalBytesToDownload in
250
247
self . delegate? . downloadingBinaryArtifact (
251
248
from: artifact. url. absoluteString,
252
249
bytesDownloaded: bytesDownloaded,
253
250
totalBytesToDownload: totalBytesToDownload
254
251
)
255
252
} ,
256
- completion: { downloadResult in
253
+ completion: { fetchResult in
257
254
defer { group. leave ( ) }
258
255
259
- // TODO: Use the same extraction logic for both remote and local archived artifacts.
260
- switch downloadResult {
261
- case . success:
262
-
256
+ switch fetchResult {
257
+ case . success( let cached) :
258
+ // TODO: Use the same extraction logic for both remote and local archived artifacts.
263
259
group. enter ( )
264
260
observabilityScope. emit ( debug: " validating \( archivePath) " )
265
261
self . archiver. validate ( path: archivePath, completion: { validationResult in
@@ -381,8 +377,8 @@ extension Workspace {
381
377
)
382
378
self . delegate? . didDownloadBinaryArtifact (
383
379
from: artifact. url. absoluteString,
384
- result: . success( artifactPath) ,
385
- duration: downloadStart . distance ( to: . now( ) )
380
+ result: . success( ( path : artifactPath, fromCache : cached ) ) ,
381
+ duration: fetchStart . distance ( to: . now( ) )
386
382
)
387
383
case . failure( let error) :
388
384
observabilityScope. emit ( . remoteArtifactFailedExtraction(
@@ -393,7 +389,7 @@ extension Workspace {
393
389
self . delegate? . didDownloadBinaryArtifact (
394
390
from: artifact. url. absoluteString,
395
391
result: . failure( error) ,
396
- duration: downloadStart . distance ( to: . now( ) )
392
+ duration: fetchStart . distance ( to: . now( ) )
397
393
)
398
394
}
399
395
@@ -409,7 +405,7 @@ extension Workspace {
409
405
self . delegate? . didDownloadBinaryArtifact (
410
406
from: artifact. url. absoluteString,
411
407
result: . failure( error) ,
412
- duration: downloadStart . distance ( to: . now( ) )
408
+ duration: fetchStart . distance ( to: . now( ) )
413
409
)
414
410
}
415
411
} )
@@ -423,7 +419,7 @@ extension Workspace {
423
419
self . delegate? . didDownloadBinaryArtifact (
424
420
from: artifact. url. absoluteString,
425
421
result: . failure( error) ,
426
- duration: downloadStart . distance ( to: . now( ) )
422
+ duration: fetchStart . distance ( to: . now( ) )
427
423
)
428
424
}
429
425
}
@@ -563,17 +559,116 @@ extension Workspace {
563
559
try cancellableArchiver. cancel ( deadline: deadline)
564
560
}
565
561
}
562
+
563
+ private func fetch(
564
+ artifact: RemoteArtifact ,
565
+ destination: AbsolutePath ,
566
+ observabilityScope: ObservabilityScope ,
567
+ progress: @escaping ( Int64 , Optional < Int64 > ) -> Void ,
568
+ completion: @escaping ( Result < Bool , Error > ) -> Void
569
+ ) {
570
+ // not using cache, download directly
571
+ guard let cachePath = self . cachePath else {
572
+ self . delegate? . willDownloadBinaryArtifact ( from: artifact. url. absoluteString, fromCache: false )
573
+ return self . download (
574
+ artifact: artifact,
575
+ destination: destination,
576
+ observabilityScope: observabilityScope,
577
+ progress: progress,
578
+ completion: { result in
579
+ // not fetched from cache
580
+ completion ( result. map { _ in false } )
581
+ }
582
+ )
583
+ }
584
+
585
+ // initialize cache if necessary
586
+ do {
587
+ if !self . fileSystem. exists ( cachePath) {
588
+ try self . fileSystem. createDirectory ( cachePath, recursive: true )
589
+ }
590
+ } catch {
591
+ return completion ( . failure( error) )
592
+ }
593
+
594
+
595
+ // try to fetch from cache, or download and cache
596
+ // / FIXME: use better escaping of URL
597
+ let cacheKey = artifact. url. absoluteString. spm_mangledToC99ExtendedIdentifier ( )
598
+ let cachedArtifactPath = cachePath. appending ( cacheKey)
599
+
600
+ if self . fileSystem. exists ( cachedArtifactPath) {
601
+ observabilityScope. emit ( debug: " copying cached binary artifact for \( artifact. url) from \( cachedArtifactPath) " )
602
+ self . delegate? . willDownloadBinaryArtifact ( from: artifact. url. absoluteString, fromCache: true )
603
+ return completion (
604
+ Result . init ( catching: {
605
+ // copy from cache to destination
606
+ try self . fileSystem. copy ( from: cachedArtifactPath, to: destination)
607
+ return true // fetched from cache
608
+ } )
609
+ )
610
+ }
611
+
612
+ // download to the cache
613
+ observabilityScope. emit ( debug: " downloading binary artifact for \( artifact. url) to cached at \( cachedArtifactPath) " )
614
+ self . download (
615
+ artifact: artifact,
616
+ destination: cachedArtifactPath,
617
+ observabilityScope: observabilityScope,
618
+ progress: progress,
619
+ completion: { result in
620
+ self . delegate? . willDownloadBinaryArtifact ( from: artifact. url. absoluteString, fromCache: false )
621
+ completion ( result. flatMap {
622
+ Result . init ( catching: {
623
+ // copy from cache to destination
624
+ try self . fileSystem. copy ( from: cachedArtifactPath, to: destination)
625
+ return false // not fetched from cache
626
+ } )
627
+ } )
628
+ }
629
+ )
630
+ }
631
+
632
+ private func download(
633
+ artifact: RemoteArtifact ,
634
+ destination: AbsolutePath ,
635
+ observabilityScope: ObservabilityScope ,
636
+ progress: @escaping ( Int64 , Optional < Int64 > ) -> Void ,
637
+ completion: @escaping ( Result < Void , Error > ) -> Void
638
+ ) {
639
+ observabilityScope. emit ( debug: " downloading \( artifact. url) to \( destination) " )
640
+
641
+ var headers = HTTPClientHeaders ( )
642
+ headers. add ( name: " Accept " , value: " application/octet-stream " )
643
+ var request = LegacyHTTPClient . Request. download (
644
+ url: artifact. url,
645
+ headers: headers,
646
+ fileSystem: self . fileSystem,
647
+ destination: destination
648
+ )
649
+ request. options. authorizationProvider = self . authorizationProvider? . httpAuthorizationHeader ( for: )
650
+ request. options. retryStrategy = . exponentialBackoff( maxAttempts: 3 , baseDelay: . milliseconds( 50 ) )
651
+ request. options. validResponseCodes = [ 200 ]
652
+
653
+ self . httpClient. execute (
654
+ request,
655
+ progress: progress,
656
+ completion: { result in
657
+ completion ( result. map { _ in Void ( ) } )
658
+ }
659
+ )
660
+ }
566
661
}
567
662
}
568
663
569
664
/// Delegate to notify clients about actions being performed by BinaryArtifactsDownloadsManage.
570
665
public protocol BinaryArtifactsManagerDelegate {
571
666
/// The workspace has started downloading a binary artifact.
572
- func willDownloadBinaryArtifact( from url: String )
667
+ func willDownloadBinaryArtifact( from url: String , fromCache : Bool )
573
668
/// The workspace has finished downloading a binary artifact.
574
669
func didDownloadBinaryArtifact(
575
670
from url: String ,
576
- result: Result < AbsolutePath , Error > ,
671
+ result: Result < ( path : AbsolutePath , fromCache : Bool ) , Error > ,
577
672
duration: DispatchTimeInterval
578
673
)
579
674
/// The workspace is downloading a binary artifact.
@@ -833,7 +928,7 @@ extension Workspace {
833
928
}
834
929
835
930
// Download the artifacts
836
- let downloadedArtifacts = try self . binaryArtifactsManager. download (
931
+ let downloadedArtifacts = try self . binaryArtifactsManager. fetch (
837
932
artifactsToDownload,
838
933
artifactsDirectory: self . location. artifactsDirectory,
839
934
observabilityScope: observabilityScope
0 commit comments