@@ -26,10 +26,16 @@ extension Workspace {
2626 public struct CustomBinaryArtifactsManager {
2727 let httpClient : LegacyHTTPClient ?
2828 let archiver : Archiver ?
29+ let useCache : Bool ?
2930
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+ ) {
3136 self . httpClient = httpClient
3237 self . archiver = archiver
38+ self . useCache = useCache
3339 }
3440 }
3541
@@ -43,13 +49,15 @@ extension Workspace {
4349 private let httpClient : LegacyHTTPClient
4450 private let archiver : Archiver
4551 private let checksumAlgorithm : HashAlgorithm
52+ private let cachePath : AbsolutePath ?
4653 private let delegate : Delegate ?
4754
4855 public init (
4956 fileSystem: FileSystem ,
5057 authorizationProvider: AuthorizationProvider ? ,
5158 hostToolchain: UserToolchain ,
5259 checksumAlgorithm: HashAlgorithm ,
60+ cachePath: AbsolutePath ? ,
5361 customHTTPClient: LegacyHTTPClient ? ,
5462 customArchiver: Archiver ? ,
5563 delegate: Delegate ?
@@ -60,6 +68,7 @@ extension Workspace {
6068 self . checksumAlgorithm = checksumAlgorithm
6169 self . httpClient = customHTTPClient ?? LegacyHTTPClient ( )
6270 self . archiver = customArchiver ?? ZipArchiver ( fileSystem: fileSystem)
71+ self . cachePath = cachePath
6372 self . delegate = delegate
6473 }
6574
@@ -126,7 +135,7 @@ extension Workspace {
126135 return ( local: localArtifacts, remote: remoteArtifacts)
127136 }
128137
129- func download (
138+ func fetch (
130139 _ artifacts: [ RemoteArtifact ] ,
131140 artifactsDirectory: AbsolutePath ,
132141 observabilityScope: ObservabilityScope
@@ -229,37 +238,24 @@ extension Workspace {
229238 }
230239
231240 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,
249246 progress: { bytesDownloaded, totalBytesToDownload in
250247 self . delegate? . downloadingBinaryArtifact (
251248 from: artifact. url. absoluteString,
252249 bytesDownloaded: bytesDownloaded,
253250 totalBytesToDownload: totalBytesToDownload
254251 )
255252 } ,
256- completion: { downloadResult in
253+ completion: { fetchResult in
257254 defer { group. leave ( ) }
258255
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.
263259 group. enter ( )
264260 observabilityScope. emit ( debug: " validating \( archivePath) " )
265261 self . archiver. validate ( path: archivePath, completion: { validationResult in
@@ -381,8 +377,8 @@ extension Workspace {
381377 )
382378 self . delegate? . didDownloadBinaryArtifact (
383379 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( ) )
386382 )
387383 case . failure( let error) :
388384 observabilityScope. emit ( . remoteArtifactFailedExtraction(
@@ -393,7 +389,7 @@ extension Workspace {
393389 self . delegate? . didDownloadBinaryArtifact (
394390 from: artifact. url. absoluteString,
395391 result: . failure( error) ,
396- duration: downloadStart . distance ( to: . now( ) )
392+ duration: fetchStart . distance ( to: . now( ) )
397393 )
398394 }
399395
@@ -409,7 +405,7 @@ extension Workspace {
409405 self . delegate? . didDownloadBinaryArtifact (
410406 from: artifact. url. absoluteString,
411407 result: . failure( error) ,
412- duration: downloadStart . distance ( to: . now( ) )
408+ duration: fetchStart . distance ( to: . now( ) )
413409 )
414410 }
415411 } )
@@ -423,7 +419,7 @@ extension Workspace {
423419 self . delegate? . didDownloadBinaryArtifact (
424420 from: artifact. url. absoluteString,
425421 result: . failure( error) ,
426- duration: downloadStart . distance ( to: . now( ) )
422+ duration: fetchStart . distance ( to: . now( ) )
427423 )
428424 }
429425 }
@@ -563,17 +559,116 @@ extension Workspace {
563559 try cancellableArchiver. cancel ( deadline: deadline)
564560 }
565561 }
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+ }
566661 }
567662}
568663
569664/// Delegate to notify clients about actions being performed by BinaryArtifactsDownloadsManage.
570665public protocol BinaryArtifactsManagerDelegate {
571666 /// The workspace has started downloading a binary artifact.
572- func willDownloadBinaryArtifact( from url: String )
667+ func willDownloadBinaryArtifact( from url: String , fromCache : Bool )
573668 /// The workspace has finished downloading a binary artifact.
574669 func didDownloadBinaryArtifact(
575670 from url: String ,
576- result: Result < AbsolutePath , Error > ,
671+ result: Result < ( path : AbsolutePath , fromCache : Bool ) , Error > ,
577672 duration: DispatchTimeInterval
578673 )
579674 /// The workspace is downloading a binary artifact.
@@ -833,7 +928,7 @@ extension Workspace {
833928 }
834929
835930 // Download the artifacts
836- let downloadedArtifacts = try self . binaryArtifactsManager. download (
931+ let downloadedArtifacts = try self . binaryArtifactsManager. fetch (
837932 artifactsToDownload,
838933 artifactsDirectory: self . location. artifactsDirectory,
839934 observabilityScope: observabilityScope
0 commit comments