This PR contains all the work related to setting up this project as required to implement the [Assignment](https://repo.rock-n-code.com/rock-n-code/deep-linking-assignment/wiki/Assignment) on top, as intended. To summarise this work: - [x] created a new **Xcode** project; - [x] cloned the `Wikipedia` app and inserted it into the **Xcode** project; - [x] created the `Locations` app and also, its `Libraries` package; - [x] created the `Shared` package to share dependencies between the apps; - [x] added a `Makefile` file and implemented some **environment** and **help** commands. Co-authored-by: Javier Cicchelli <javier@rock-n-code.com> Reviewed-on: rock-n-code/deep-linking-assignment#1
209 lines
9.3 KiB
Swift
209 lines
9.3 KiB
Swift
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)
|
|
}
|
|
}
|
|
}
|