Javier Cicchelli 9bcdaa697b [Setup] Basic project structure (#1)
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
2023-04-08 18:37:13 +00:00

247 lines
9.0 KiB
Swift

import Foundation
import CocoaLumberjackSwift
enum CacheFileWriterError: Error {
case missingTemporaryFileURL
case missingHeaderItemKey
case missingHTTPResponse
case unableToDetermineSiteURLFromMigration
case unexpectedFetcherTypeForBundledMigration
case unableToDetermineBundledOfflineURLS
case failureToSaveBundledFiles
case unableToPullCachedDataFromNotModified
case missingURLInRequest
case unableToGenerateHTTPURLResponse
case unableToDetermineFileNames
}
enum CacheFileWriterAddResult {
case success(response: HTTPURLResponse, data: Data)
case failure(Error)
}
enum CacheFileWriterRemoveResult {
case success
case failure(Error)
}
final class CacheFileWriter: CacheTaskTracking {
private let fetcher: CacheFetching
lazy private var baseCSSFileURL: URL = {
URL(fileURLWithPath: WikipediaAppUtils.assetsPath())
.appendingPathComponent("pcs-html-converter", isDirectory: true)
.appendingPathComponent("base.css", isDirectory: false)
}()
lazy private var pcsCSSFileURL: URL = {
URL(fileURLWithPath: WikipediaAppUtils.assetsPath())
.appendingPathComponent("pcs-html-converter", isDirectory: true)
.appendingPathComponent("pcs.css", isDirectory: false)
}()
lazy private var pcsJSFileURL: URL = {
URL(fileURLWithPath: WikipediaAppUtils.assetsPath())
.appendingPathComponent("pcs-html-converter", isDirectory: true)
.appendingPathComponent("pcs.js", isDirectory: false)
}()
var groupedTasks: [String : [IdentifiedTask]] = [:]
init(fetcher: CacheFetching) {
self.fetcher = fetcher
do {
try FileManager.default.createDirectory(at: CacheController.cacheURL, withIntermediateDirectories: true, attributes: nil)
} catch {
DDLogError("Error creating permanent cache: \(error)")
}
}
func add(groupKey: String, urlRequest: URLRequest, completion: @escaping (CacheFileWriterAddResult) -> Void) {
let untrackKey = UUID().uuidString
let task = fetcher.dataForURLRequest(urlRequest) { [weak self] (response) in
guard let self = self else {
return
}
defer {
self.untrackTask(untrackKey: untrackKey, from: groupKey)
}
switch response {
case .success(let result):
guard let httpUrlResponse = result.response as? HTTPURLResponse else {
completion(.failure(CacheFileWriterError.missingHTTPResponse))
return
}
self.fetcher.cacheResponse(httpUrlResponse: httpUrlResponse, content: .data(result.data), urlRequest: urlRequest, success: {
completion(.success(response: httpUrlResponse, data: result.data))
}) { (error) in
completion(.failure(error))
}
case .failure(let error):
DDLogError("Error downloading data for offline: \(error)\n\(String(describing: response))")
completion(.failure(error))
return
}
}
if let task = task {
trackTask(untrackKey: untrackKey, task: task, to: groupKey)
}
}
func remove(itemKey: String, variant: String?, completion: @escaping (CacheFileWriterRemoveResult) -> Void) {
guard let fileName = self.fetcher.uniqueFileNameForItemKey(itemKey, variant: variant),
let headerFileName = self.fetcher.uniqueHeaderFileNameForItemKey(itemKey, variant: variant) else {
completion(.failure(CacheFileWriterError.unableToDetermineFileNames))
return
}
var responseHeaderRemoveError: Error? = nil
var responseRemoveError: Error? = nil
// remove response from file system
let responseCachedFileURL = CacheFileWriterHelper.fileURL(for: fileName)
do {
try FileManager.default.removeItem(at: responseCachedFileURL)
} catch let error as NSError {
if !(error.code == NSURLErrorFileDoesNotExist || error.code == NSFileNoSuchFileError) {
responseRemoveError = error
}
}
// remove response header from file system
let responseHeaderCachedFileURL = CacheFileWriterHelper.fileURL(for: headerFileName)
do {
try FileManager.default.removeItem(at: responseHeaderCachedFileURL)
} catch let error as NSError {
if !(error.code == NSURLErrorFileDoesNotExist || error.code == NSFileNoSuchFileError) {
responseHeaderRemoveError = error
}
}
if let responseHeaderRemoveError = responseHeaderRemoveError {
completion(.failure(responseHeaderRemoveError))
return
}
if let responseRemoveError = responseRemoveError {
completion(.failure(responseRemoveError))
return
}
completion(.success)
}
func uniqueFileNameForItemKey(_ itemKey: CacheController.ItemKey, variant: String?) -> String? {
return fetcher.uniqueFileNameForItemKey(itemKey, variant: variant)
}
func uniqueFileNameForURLRequest(_ urlRequest: URLRequest) -> String? {
return fetcher.uniqueFileNameForURLRequest(urlRequest)
}
}
// MARK: Migration
extension CacheFileWriter {
// assumes urlRequest is already populated with the right type header
func addMobileHtmlContentForMigration(content: String, urlRequest: URLRequest, success: @escaping () -> Void, failure: @escaping (Error) -> Void) {
guard let url = urlRequest.url else {
failure(CacheFileWriterError.missingURLInRequest)
return
}
// artificially create HTTPURLResponse
guard let response = HTTPURLResponse(url: url, statusCode: 200, httpVersion: nil, headerFields: ["Content-Type": "text/html"]) else {
failure(CacheFileWriterError.unableToGenerateHTTPURLResponse)
return
}
fetcher.cacheResponse(httpUrlResponse: response, content: .string(content), urlRequest: urlRequest, success: {
success()
}) { (error) in
failure(error)
}
}
func addBundledResourcesForMigration(urlRequests:[URLRequest], success: @escaping ([URLRequest]) -> Void, failure: @escaping (Error) -> Void) {
guard let articleFetcher = fetcher as? ArticleFetcher else {
failure(CacheFileWriterError.unexpectedFetcherTypeForBundledMigration)
return
}
guard let bundledOfflineResources = articleFetcher.bundledOfflineResourceURLs() else {
failure(CacheFileWriterError.unableToDetermineBundledOfflineURLS)
return
}
var failedURLRequests: [URLRequest] = []
var succeededURLRequests: [URLRequest] = []
for urlRequest in urlRequests {
guard let urlString = urlRequest.url?.absoluteString else {
continue
}
switch urlString {
case bundledOfflineResources.baseCSS.absoluteString:
fetcher.writeBundledFiles(mimeType: "text/css", bundledFileURL: baseCSSFileURL, urlRequest: urlRequest) { (result) in
switch result {
case .success:
succeededURLRequests.append(urlRequest)
case .failure:
failedURLRequests.append(urlRequest)
}
}
case bundledOfflineResources.pcsCSS.absoluteString:
fetcher.writeBundledFiles(mimeType: "text/css", bundledFileURL: pcsCSSFileURL, urlRequest: urlRequest) { (result) in
switch result {
case .success:
succeededURLRequests.append(urlRequest)
case .failure:
failedURLRequests.append(urlRequest)
}
}
case bundledOfflineResources.pcsJS.absoluteString:
fetcher.writeBundledFiles(mimeType: "application/javascript", bundledFileURL: pcsJSFileURL, urlRequest: urlRequest) { (result) in
switch result {
case .success:
succeededURLRequests.append(urlRequest)
case .failure:
failedURLRequests.append(urlRequest)
}
}
default:
failedURLRequests.append(urlRequest)
}
}
if succeededURLRequests.count == 0 {
failure(CacheFileWriterError.failureToSaveBundledFiles)
return
}
success(succeededURLRequests)
}
}