import Foundation public final class ArticleCacheController: CacheController { init(moc: NSManagedObjectContext, imageCacheController: ImageCacheController, session: Session, configuration: Configuration, preferredLanguageDelegate: WMFPreferredLanguageInfoProvider) { let articleFetcher = ArticleFetcher(session: session, configuration: configuration) let imageInfoFetcher = MWKImageInfoFetcher(session: session, configuration: configuration) imageInfoFetcher.preferredLanguageDelegate = preferredLanguageDelegate let cacheFileWriter = CacheFileWriter(fetcher: articleFetcher) let articleDBWriter = ArticleCacheDBWriter(articleFetcher: articleFetcher, cacheBackgroundContext: moc, imageController: imageCacheController, imageInfoFetcher: imageInfoFetcher) super.init(dbWriter: articleDBWriter, fileWriter: cacheFileWriter) } enum ArticleCacheControllerError: Error { case invalidDBWriterType } // syncs already cached resources with mobile-html-offline-resources and media-list endpoints (caches new urls, removes old urls) public func syncCachedResources(url: URL, groupKey: CacheController.GroupKey, groupCompletion: @escaping GroupCompletionBlock) { guard let articleDBWriter = dbWriter as? ArticleCacheDBWriter else { groupCompletion(.failure(error: ArticleCacheControllerError.invalidDBWriterType)) return } articleDBWriter.syncResources(url: url, groupKey: groupKey) { (result) in switch result { case .success(let syncResult): let group = DispatchGroup() var successfulAddKeys: [CacheController.UniqueKey] = [] var failedAddKeys: [(CacheController.UniqueKey, Error)] = [] var successfulRemoveKeys: [CacheController.UniqueKey] = [] var failedRemoveKeys: [(CacheController.UniqueKey, Error)] = [] // add new urls in file system for urlRequest in syncResult.addURLRequests { guard let uniqueKey = self.fileWriter.uniqueFileNameForURLRequest(urlRequest), urlRequest.url != nil else { continue } group.enter() self.fileWriter.add(groupKey: groupKey, urlRequest: urlRequest) { (fileWriterResult) in switch fileWriterResult { case .success(let response, _): self.dbWriter.markDownloaded(urlRequest: urlRequest, response: response) { (dbWriterResult) in defer { group.leave() } switch dbWriterResult { case .success: successfulAddKeys.append(uniqueKey) case .failure(let error): failedAddKeys.append((uniqueKey, error)) } } case .failure(let error): defer { group.leave() } failedAddKeys.append((uniqueKey, error)) } } } // remove old urls in file system for key in syncResult.removeItemKeyAndVariants { guard let uniqueKey = self.fileWriter.uniqueFileNameForItemKey(key.itemKey, variant: key.variant) else { continue } group.enter() self.fileWriter.remove(itemKey: key.itemKey, variant: key.variant) { (fileWriterResult) in switch fileWriterResult { case .success: self.dbWriter.remove(itemAndVariantKey: key) { (dbWriterResult) in defer { group.leave() } switch dbWriterResult { case .success: successfulRemoveKeys.append(uniqueKey) case .failure(let error): failedRemoveKeys.append((uniqueKey, error)) } } case .failure(let error): defer { group.leave() } failedRemoveKeys.append((uniqueKey, error)) } } } group.notify(queue: DispatchQueue.global(qos: .userInitiated)) { if let error = failedAddKeys.first?.1 ?? failedRemoveKeys.first?.1 { groupCompletion(.failure(error: CacheControllerError.atLeastOneItemFailedInSync(error))) return } let successKeys = successfulAddKeys + successfulRemoveKeys groupCompletion(.success(uniqueKeys: successKeys)) } case .failure(let error): groupCompletion(.failure(error: error)) } } } public func cacheFromMigration(desktopArticleURL: URL, content: String, completionHandler: @escaping ((Error?) -> Void)) { // articleURL should be desktopURL guard let articleDBWriter = dbWriter as? ArticleCacheDBWriter else { completionHandler(ArticleCacheControllerError.invalidDBWriterType) return } cacheBundledResourcesIfNeeded(desktopArticleURL: desktopArticleURL) { (cacheBundledError) in articleDBWriter.addMobileHtmlURLForMigration(desktopArticleURL: desktopArticleURL, success: { urlRequest in self.fileWriter.addMobileHtmlContentForMigration(content: content, urlRequest: urlRequest, success: { articleDBWriter.markDownloaded(urlRequest: urlRequest, response: nil) { (result) in switch result { case .success: if cacheBundledError == nil { completionHandler(nil) } else { completionHandler(cacheBundledError) } case .failure(let error): completionHandler(error) } } }) { (error) in completionHandler(error) } }) { (error) in completionHandler(error) } } } private func bundledResourcesAreCached() -> Bool { guard let articleDBWriter = dbWriter as? ArticleCacheDBWriter else { return false } return articleDBWriter.bundledResourcesAreCached() } private func cacheBundledResourcesIfNeeded(desktopArticleURL: URL, completionHandler: @escaping ((Error?) -> Void)) { // articleURL should be desktopURL guard let articleDBWriter = dbWriter as? ArticleCacheDBWriter else { completionHandler(ArticleCacheControllerError.invalidDBWriterType) return } if !articleDBWriter.bundledResourcesAreCached() { articleDBWriter.addBundledResourcesForMigration(desktopArticleURL: desktopArticleURL) { (result) in switch result { case .success(let requests): self.fileWriter.addBundledResourcesForMigration(urlRequests: requests, success: { (_) in let bulkRequests = requests.map { ArticleCacheDBWriter.BulkMarkDownloadRequest(urlRequest: $0, response: nil) } articleDBWriter.markDownloaded(requests: bulkRequests) { (result) in switch result { case .success: completionHandler(nil) case .failure(let error): completionHandler(error) } } }) { (error) in completionHandler(error) } case .failure(let error): completionHandler(error) } } } else { completionHandler(nil) } } }