deep-linking-sample/Apps/Wikipedia/WMF Framework/ArticleCacheDBWriter+SyncResources.swift

245 lines
13 KiB
Swift
Raw Permalink Normal View History

import Foundation
enum ArticleCacheDBWriterSyncError: Error {
case missingMOC
case cannotFindCacheGroup
case cannotFindCacheItem
case unableToDetermineItemKey
case failureFetchOrCreateCacheItem
case failureToCreateNetworkItem
}
struct CacheDBWritingSyncSuccessResult {
let addURLRequests: [URLRequest]
let removeItemKeyAndVariants: [CacheController.ItemKeyAndVariant]
}
extension ArticleCacheDBWriter {
private struct NetworkItem: Hashable {
let itemKeyAndVariant: CacheController.ItemKeyAndVariant
let url: URL?
let urlRequest: URLRequest?
let cacheItem: CacheItem?
func hash(into hasher: inout Hasher) {
hasher.combine(itemKeyAndVariant)
}
static func ==(lhs: NetworkItem, rhs: NetworkItem) -> Bool {
return lhs.itemKeyAndVariant == rhs.itemKeyAndVariant
}
}
// adds new resources to DB, returns old resources in completion
func syncResources(url: URL, groupKey: CacheController.GroupKey, completion: @escaping (Result<CacheDBWritingSyncSuccessResult, Error>) -> Void) {
let mobileHTMLURL: URL
let mediaListURL: URL
let mobileHTMLRequest: URLRequest
let mediaListRequest: URLRequest
do {
mobileHTMLURL = try articleFetcher.mobileHTMLURL(articleURL: url)
mediaListURL = try articleFetcher.mediaListURL(articleURL: url)
mobileHTMLRequest = try articleFetcher.mobileHTMLRequest(articleURL: url)
mediaListRequest = try articleFetcher.mobileHTMLMediaListRequest(articleURL: url)
} catch let error {
completion(.failure(error))
return
}
let mobileHTMLItemKey = articleFetcher.itemKeyForURL(mobileHTMLURL, type: .article)
let mobileHTMLVariant = articleFetcher.variantForURL(mobileHTMLURL, type: .article)
let mediaListItemKey = articleFetcher.itemKeyForURL(mediaListURL, type: .article)
let mediaListVariant = articleFetcher.variantForURL(mediaListURL, type: .article)
guard let mobileHTMLItemKeyAndVariant = CacheController.ItemKeyAndVariant(itemKey: mobileHTMLItemKey, variant: mobileHTMLVariant),
let mediaListItemKeyAndVariant = CacheController.ItemKeyAndVariant(itemKey: mediaListItemKey, variant: mediaListVariant) else {
completion(.failure(ArticleCacheDBWriterSyncError.failureToCreateNetworkItem))
return
}
let mobileHTMLNetworkItem = NetworkItem(itemKeyAndVariant: mobileHTMLItemKeyAndVariant, url: mobileHTMLURL, urlRequest: mobileHTMLRequest, cacheItem: nil)
let mediaListNetworkItem = NetworkItem(itemKeyAndVariant: mediaListItemKeyAndVariant, url: mediaListURL, urlRequest: mediaListRequest, cacheItem: nil)
fetchImageAndResourceURLsForArticleURL(url, groupKey: groupKey) { [weak self] (result) in
guard let self = self else {
return
}
switch result {
case .success(let urls):
// package offline resource urls into NetworkItems
let offlineResourceItems = urls.offlineResourcesURLs.compactMap { (url) -> NetworkItem? in
guard let itemKey = self.articleFetcher.itemKeyForURL(url, type: .article) else {
return nil
}
let variant = self.articleFetcher.variantForURL(url, type: .article)
guard let itemKeyAndVariant = CacheController.ItemKeyAndVariant(itemKey: itemKey, variant: variant),
let urlRequest = self.articleFetcher.urlRequestFromPersistence(with: url, persistType: .article) else {
return nil
}
return NetworkItem(itemKeyAndVariant: itemKeyAndVariant, url: url, urlRequest: urlRequest, cacheItem: nil)
}
// begin package media list urls into NetworkItems
let mediaListItems = urls.mediaListURLs.compactMap { (url) -> NetworkItem? in
guard let itemKey = self.articleFetcher.itemKeyForURL(url, type: .image) else {
return nil
}
let variant = self.articleFetcher.variantForURL(url, type: .image)
guard let itemKeyAndVariant = CacheController.ItemKeyAndVariant(itemKey: itemKey, variant: variant),
let urlRequest = self.articleFetcher.urlRequestFromPersistence(with: url, persistType: .image) else {
return nil
}
return NetworkItem(itemKeyAndVariant: itemKeyAndVariant, url: url, urlRequest: urlRequest, cacheItem: nil)
}
// group into dictionary of the same itemKey for use in filtering out variants
var similarItemsDictionary: [CacheController.ItemKey: [NetworkItem]] = [:]
for item in mediaListItems {
if var existingItems = similarItemsDictionary[item.itemKeyAndVariant.itemKey] {
existingItems.append(item)
similarItemsDictionary[item.itemKeyAndVariant.itemKey] = existingItems
} else {
similarItemsDictionary[item.itemKeyAndVariant.itemKey] = [item]
}
}
// filter out any variants that image cache controller should not download (i.e. multiple sizes of the same image)
var finalMediaListItems: [NetworkItem] = []
for item in mediaListItems {
guard let similarItems = similarItemsDictionary[item.itemKeyAndVariant.itemKey] else {
continue
}
let allVariantItems = similarItems.map { $0.itemKeyAndVariant }
if self.imageController.shouldDownloadVariantForAllVariantItems(variant: item.itemKeyAndVariant.variant, allVariantItems) {
finalMediaListItems.append(item)
}
}
// end package media list urls into NetworkItems
// begin image info urls into NetworkItems
let imageInfoItems = urls.imageInfoURLs.compactMap { (url) -> NetworkItem? in
guard let itemKey = self.articleFetcher.itemKeyForURL(url, type: .imageInfo) else {
return nil
}
let variant = self.articleFetcher.variantForURL(url, type: .imageInfo)
guard let itemKeyAndVariant = CacheController.ItemKeyAndVariant(itemKey: itemKey, variant: variant),
let urlRequest = self.articleFetcher.urlRequestFromPersistence(with: url, persistType: .imageInfo) else {
return nil
}
return NetworkItem(itemKeyAndVariant: itemKeyAndVariant, url: url, urlRequest: urlRequest, cacheItem: nil)
}
// consolidate list of network items to compare with downloaded cached items
// remove list also contains mobile-html & media-list urls for comparison purposes only, otherwise it will think they should be removed from cache.
let networkItemsForAdd: Set<NetworkItem> = Set(offlineResourceItems + finalMediaListItems + imageInfoItems)
let networkItemsForRemove: Set<NetworkItem> = Set([mobileHTMLNetworkItem] + [mediaListNetworkItem] + offlineResourceItems + finalMediaListItems + imageInfoItems)
self.context.perform { [weak self] in
guard let self = self else {
return
}
guard let group = CacheDBWriterHelper.cacheGroup(with: groupKey, in: self.context) else {
completion(.failure(ArticleCacheDBWriterSyncError.cannotFindCacheGroup))
return
}
guard let cacheItems = group.cacheItems as? Set<CacheItem> else {
completion(.failure(ArticleCacheDBWriterSyncError.cannotFindCacheItem))
return
}
let downloadedCacheItems = cacheItems.compactMap { (cacheItem) -> NetworkItem? in
guard let itemKey = cacheItem.key,
let itemKeyandVariant = CacheController.ItemKeyAndVariant(itemKey: itemKey, variant: cacheItem.variant),
cacheItem.isDownloaded == true else {
return nil
}
return NetworkItem(itemKeyAndVariant: itemKeyandVariant, url: cacheItem.url, urlRequest: nil, cacheItem: cacheItem)
}
let downloadedCacheItemsSet = Set(downloadedCacheItems)
// determine final set of new items that need to be cached
let filteredNewItems = networkItemsForAdd.subtracting(downloadedCacheItemsSet)
let filteredNewURLRequests = filteredNewItems.compactMap { $0.urlRequest }
// determine set of old items that need to be removed
let filteredOldItems = downloadedCacheItemsSet.subtracting(networkItemsForRemove)
// create list of unique item key and variants (those with 1 cache group) to return to CacheController for file deletion. Otherwise delete from group.
var uniqueOldItemKeyAndVariants: [CacheController.ItemKeyAndVariant] = []
for filteredOldItem in filteredOldItems {
if let cacheItem = filteredOldItem.cacheItem {
if cacheItem.cacheGroups?.count == 1 {
uniqueOldItemKeyAndVariants.append(filteredOldItem.itemKeyAndVariant)
} else {
group.removeFromCacheItems(cacheItem)
}
}
}
// cache nonCached urls
self.cacheItems(groupKey: groupKey, items: filteredNewItems) { (result) in
switch result {
case .success:
let result = CacheDBWritingSyncSuccessResult(addURLRequests: filteredNewURLRequests, removeItemKeyAndVariants: uniqueOldItemKeyAndVariants)
completion(.success(result))
case .failure(let error):
completion(.failure(error))
}
}
}
case .failure(let error):
completion(.failure(error))
return
}
}
}
private func cacheItems(groupKey: String, items: Set<NetworkItem>, completion: @escaping ((SaveResult) -> Void)) {
context.perform {
guard let group = CacheDBWriterHelper.fetchOrCreateCacheGroup(with: groupKey, in: self.context) else {
completion(.failure(ArticleCacheDBWriterError.failureFetchOrCreateCacheGroup))
return
}
for item in items {
guard let url = item.url,
item.urlRequest != nil else {
assertionFailure("These need to be populated at this point. They are only optional to be able to compare cleanly with a set of converted CacheItems to NetworkItems")
continue
}
let itemKey = item.itemKeyAndVariant.itemKey
let variant = item.itemKeyAndVariant.variant
guard let item = CacheDBWriterHelper.fetchOrCreateCacheItem(with: url, itemKey: itemKey, variant: variant, in: self.context) else {
completion(.failure(ArticleCacheDBWriterSyncError.failureFetchOrCreateCacheItem))
return
}
group.addToCacheItems(item)
}
CacheDBWriterHelper.save(moc: self.context, completion: completion)
}
}
}