@@ -16,6 +16,17 @@ import Vapor
1616import Fluent
1717
1818
19+ enum Ingestion {
20+ enum Error : Swift . Error {
21+ case fetchMetadataFailed( owner: String , name: String , error: Swift . Error )
22+ case findOrCreateRepositoryFailed( url: String , error: Swift . Error )
23+ case invalidURL( String )
24+ case noRepositoryMetadata( owner: String ? , name: String ? )
25+ case repositorySaveFailed( owner: String ? , name: String ? , error: Swift . Error )
26+ }
27+ }
28+
29+
1930struct IngestCommand : AsyncCommand {
2031 typealias Signature = SPICommand . Signature
2132
@@ -152,17 +163,82 @@ func ingestOriginal(client: Client, database: Database, package: Joined<Package,
152163}
153164
154165
155- func fetchMetadata( client: Client , package : Joined < Package , Repository > ) async throws -> ( Github . Metadata , Github . License ? , Github . Readme ? ) {
166+ extension Ingestion {
167+ static func ingestNew( client: Client , database: Database , package : Joined < Package , Repository > ) async {
168+ let result = await Result { ( ) async throws ( Ingestion . Error) - > Joined < Package , Repository > in
169+ Current . logger ( ) . info ( " Ingesting \( package . package . url) " )
170+ let ( metadata, license, readme) = try await fetchMetadata ( client: client, package : package )
171+ let repo = try await Result {
172+ try await Repository . findOrCreate ( on: database, for: package . model)
173+ } . mapError {
174+ Ingestion . Error. findOrCreateRepositoryFailed ( url: package . package . url, error: $0)
175+ } . get ( )
176+
177+ let s3Readme : S3Readme ?
178+ do throws ( S3ReadmeError) {
179+ s3Readme = try await storeS3Readme ( client: client, repository: repo, metadata: metadata, readme: readme)
180+ } catch {
181+ // We don't want to fail ingestion in case storing the readme fails - warn and continue.
182+ Current . logger ( ) . warning ( " storeS3Readme failed: \( error) " )
183+ s3Readme = . error( " \( error) " )
184+ }
185+
186+ try await updateRepository ( on: database, for: repo, metadata: metadata, licenseInfo: license, readmeInfo: readme, s3Readme: s3Readme)
187+ return package
188+ }
189+
190+ switch result {
191+ case . success:
192+ AppMetrics . ingestMetadataSuccessCount? . inc ( )
193+ case . failure:
194+ AppMetrics . ingestMetadataFailureCount? . inc ( )
195+ }
196+
197+ do {
198+ try await updatePackage ( client: client, database: database, result: result, stage: . ingestion)
199+ } catch {
200+ Current . logger ( ) . report ( error: error)
201+ }
202+ }
203+
204+
205+ static func storeS3Readme( client: Client , repository: Repository , metadata: Github . Metadata , readme: Github . Readme ? ) async throws ( S3 ReadmeError) -> S3Readme ? {
206+ if let upstreamEtag = readme? . etag,
207+ repository. s3Readme? . needsUpdate ( upstreamEtag: upstreamEtag) ?? true ,
208+ let owner = metadata. repositoryOwner,
209+ let repository = metadata. repositoryName,
210+ let html = readme? . html {
211+ let objectUrl = try await Current . storeS3Readme ( owner, repository, html)
212+ if let imagesToCache = readme? . imagesToCache, imagesToCache. isEmpty == false {
213+ try await Current . storeS3ReadmeImages ( client, imagesToCache)
214+ }
215+ return . cached( s3ObjectUrl: objectUrl, githubEtag: upstreamEtag)
216+ } else {
217+ return repository. s3Readme
218+ }
219+ }
220+ }
221+
222+ func fetchMetadata( client: Client , package : Joined < Package , Repository > ) async throws ( Ingestion. Error) -> ( Github . Metadata , Github . License ? , Github . Readme ? ) {
156223 // Even though we get through a `Joined<Package, Repository>` as a parameter, it's
157224 // we must not rely on `repository` as it will be nil when a package is first ingested.
158225 // The only way to get `owner` and `repository` here is by parsing them from the URL.
159- let ( owner, repository) = try Github . parseOwnerName ( url: package . model. url)
226+ let ( owner, repository) = try Result {
227+ try Github . parseOwnerName ( url: package . model. url)
228+ } . mapError { _ in
229+ Ingestion . Error. invalidURL ( package . model. url)
230+ } . get ( )
160231
161- async let metadata = try await Current . fetchMetadata ( client, owner, repository)
162232 async let license = await Current . fetchLicense ( client, owner, repository)
163233 async let readme = await Current . fetchReadme ( client, owner, repository)
164234
165- return try await ( metadata, license, readme)
235+ // First one should be an `async let` as well but it doesn't compile right now. Reported as
236+ // https://github.com/swiftlang/swift/issues/76169
237+ return ( try await Result { try await Current . fetchMetadata ( client, owner, repository) }
238+ . mapError { Ingestion . Error. fetchMetadataFailed ( owner: owner, name: repository, error: $0) }
239+ . get ( ) ,
240+ await license,
241+ await readme)
166242}
167243
168244
@@ -177,13 +253,9 @@ func updateRepository(on database: Database,
177253 metadata: Github . Metadata ,
178254 licenseInfo: Github . License ? ,
179255 readmeInfo: Github . Readme ? ,
180- s3Readme: S3Readme ? ) async throws {
256+ s3Readme: S3Readme ? ) async throws ( Ingestion . Error ) {
181257 guard let repoMetadata = metadata. repository else {
182- if repository. $package. value == nil {
183- try await repository. $package. load ( on: database)
184- }
185- throw AppError . genericError ( repository. package . id,
186- " repository metadata is nil for package \( repository. name ?? " unknown " ) " )
258+ throw . noRepositoryMetadata( owner: repository. owner, name: repository. name)
187259 }
188260
189261 repository. defaultBranch = repoMetadata. defaultBranch
@@ -209,7 +281,11 @@ func updateRepository(on database: Database,
209281 repository. stars = repoMetadata. stargazerCount
210282 repository. summary = repoMetadata. description
211283
212- try await repository. save ( on: database)
284+ try await Result {
285+ try await repository. save ( on: database)
286+ } . mapError {
287+ Ingestion . Error. repositorySaveFailed ( owner: repository. owner, name: repository. name, error: $0)
288+ } . get ( )
213289}
214290
215291
0 commit comments