mirror of
https://github.com/Ranchero-Software/NetNewsWire
synced 2025-08-12 06:26:36 +00:00
Rename OneShotDownload to Downloader. Use built-in caching support.
This commit is contained in:
@@ -14,8 +14,8 @@ import RSCore
|
||||
class FeedFinder {
|
||||
|
||||
static func find(url: URL, completion: @escaping (Result<Set<FeedSpecifier>, Error>) -> Void) {
|
||||
downloadAddingToCache(url) { (data, response, error) in
|
||||
|
||||
Downloader.shared.download(url) { (data, response, error) in
|
||||
|
||||
if response?.forcedStatusCode == 404 {
|
||||
if var urlComponents = URLComponents(url: url, resolvingAgainstBaseURL: false), urlComponents.host == "micro.blog" {
|
||||
urlComponents.path = "\(urlComponents.path).json"
|
||||
@@ -147,7 +147,7 @@ private extension FeedFinder {
|
||||
}
|
||||
|
||||
group.enter()
|
||||
downloadUsingCache(url) { (data, response, error) in
|
||||
Downloader.shared.download(url) { (data, response, error) in
|
||||
if let data = data, let response = response, response.statusIsOK, error == nil {
|
||||
if self.isFeed(data, downloadFeedSpecifier.urlString) {
|
||||
addFeedSpecifier(downloadFeedSpecifier, feedSpecifiers: &resultFeedSpecifiers)
|
||||
|
||||
@@ -14,7 +14,7 @@ struct InitialFeedDownloader {
|
||||
|
||||
static func download(_ url: URL,_ completion: @escaping (_ parsedFeed: ParsedFeed?) -> Void) {
|
||||
|
||||
downloadUsingCache(url) { (data, response, error) in
|
||||
Downloader.shared.download(url) { (data, response, error) in
|
||||
guard let data = data else {
|
||||
completion(nil)
|
||||
return
|
||||
|
||||
@@ -54,9 +54,7 @@ struct CrashReporter {
|
||||
let formData = formString.data(using: .utf8, allowLossyConversion: true)
|
||||
request.httpBody = formData
|
||||
|
||||
download(request) { (_, _, _) in
|
||||
// Don’t care about the result.
|
||||
}
|
||||
Downloader.shared.download(request) // Don’t care about the result.
|
||||
}
|
||||
|
||||
static func runCrashReporterWindow(_ crashLogText: String) {
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
//
|
||||
// DownloadObject.swift
|
||||
// RSWeb
|
||||
//
|
||||
// Created by Brent Simmons on 8/3/16.
|
||||
// Copyright © 2016 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public final class DownloadObject: Hashable {
|
||||
|
||||
public let url: URL
|
||||
public var data = Data()
|
||||
|
||||
public init(url: URL) {
|
||||
self.url = url
|
||||
}
|
||||
|
||||
// MARK: - Hashable
|
||||
|
||||
public func hash(into hasher: inout Hasher) {
|
||||
hasher.combine(url)
|
||||
}
|
||||
|
||||
// MARK: - Equatable
|
||||
|
||||
public static func ==(lhs: DownloadObject, rhs: DownloadObject) -> Bool {
|
||||
return lhs.url == rhs.url && lhs.data == rhs.data
|
||||
}
|
||||
}
|
||||
|
||||
57
RSWeb/Sources/RSWeb/Downloader.swift
Executable file
57
RSWeb/Sources/RSWeb/Downloader.swift
Executable file
@@ -0,0 +1,57 @@
|
||||
//
|
||||
// Downloader.swift
|
||||
// RSWeb
|
||||
//
|
||||
// Created by Brent Simmons on 8/27/16.
|
||||
// Copyright © 2016 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public typealias DownloadCallback = (Data?, URLResponse?, Error?) -> Swift.Void
|
||||
|
||||
/// Simple downloader, for a one-shot download like an image
|
||||
/// or a web page. For a download-feeds session, see DownloadSession.
|
||||
public final class Downloader {
|
||||
|
||||
public static let shared = Downloader()
|
||||
private let urlSession: URLSession
|
||||
|
||||
private init() {
|
||||
|
||||
let sessionConfiguration = URLSessionConfiguration.default
|
||||
sessionConfiguration.requestCachePolicy = .useProtocolCachePolicy
|
||||
sessionConfiguration.httpShouldSetCookies = false
|
||||
sessionConfiguration.httpCookieAcceptPolicy = .never
|
||||
sessionConfiguration.httpMaximumConnectionsPerHost = 1
|
||||
sessionConfiguration.httpCookieStorage = nil
|
||||
|
||||
if let userAgentHeaders = UserAgent.headers() {
|
||||
sessionConfiguration.httpAdditionalHeaders = userAgentHeaders
|
||||
}
|
||||
|
||||
urlSession = URLSession(configuration: sessionConfiguration)
|
||||
}
|
||||
|
||||
deinit {
|
||||
urlSession.invalidateAndCancel()
|
||||
}
|
||||
|
||||
public func download(_ url: URL, _ completion: DownloadCallback? = nil) {
|
||||
let task = urlSession.dataTask(with: url) { (data, response, error) in
|
||||
DispatchQueue.main.async() {
|
||||
completion?(data, response, error)
|
||||
}
|
||||
}
|
||||
task.resume()
|
||||
}
|
||||
|
||||
public func download(_ urlRequest: URLRequest, _ completion: DownloadCallback? = nil) {
|
||||
let task = urlSession.dataTask(with: urlRequest) { (data, response, error) in
|
||||
DispatchQueue.main.async() {
|
||||
completion?(data, response, error)
|
||||
}
|
||||
}
|
||||
task.resume()
|
||||
}
|
||||
}
|
||||
@@ -1,192 +0,0 @@
|
||||
//
|
||||
// OneShotDownload.swift
|
||||
// RSWeb
|
||||
//
|
||||
// Created by Brent Simmons on 8/27/16.
|
||||
// Copyright © 2016 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
// Main thread only.
|
||||
|
||||
public typealias OneShotDownloadCallback = (Data?, URLResponse?, Error?) -> Swift.Void
|
||||
|
||||
private final class OneShotDownloadManager {
|
||||
|
||||
private let urlSession: URLSession
|
||||
fileprivate static let shared = OneShotDownloadManager()
|
||||
|
||||
public init() {
|
||||
|
||||
let sessionConfiguration = URLSessionConfiguration.ephemeral
|
||||
sessionConfiguration.requestCachePolicy = .reloadIgnoringLocalCacheData
|
||||
sessionConfiguration.httpShouldSetCookies = false
|
||||
sessionConfiguration.httpCookieAcceptPolicy = .never
|
||||
sessionConfiguration.httpMaximumConnectionsPerHost = 2
|
||||
sessionConfiguration.httpCookieStorage = nil
|
||||
sessionConfiguration.urlCache = nil
|
||||
sessionConfiguration.timeoutIntervalForRequest = 30
|
||||
|
||||
if let userAgentHeaders = UserAgent.headers() {
|
||||
sessionConfiguration.httpAdditionalHeaders = userAgentHeaders
|
||||
}
|
||||
|
||||
urlSession = URLSession(configuration: sessionConfiguration)
|
||||
}
|
||||
|
||||
deinit {
|
||||
urlSession.invalidateAndCancel()
|
||||
}
|
||||
|
||||
public func download(_ url: URL, _ completion: @escaping OneShotDownloadCallback) {
|
||||
let task = urlSession.dataTask(with: url) { (data, response, error) in
|
||||
DispatchQueue.main.async() {
|
||||
completion(data, response, error)
|
||||
}
|
||||
}
|
||||
task.resume()
|
||||
}
|
||||
|
||||
public func download(_ urlRequest: URLRequest, _ completion: @escaping OneShotDownloadCallback) {
|
||||
let task = urlSession.dataTask(with: urlRequest) { (data, response, error) in
|
||||
DispatchQueue.main.async() {
|
||||
completion(data, response, error)
|
||||
}
|
||||
}
|
||||
task.resume()
|
||||
}
|
||||
}
|
||||
|
||||
// Call one of these. It’s easier than referring to OneShotDownloadManager.
|
||||
// callback is called on the main queue.
|
||||
|
||||
public func download(_ url: URL, _ completion: @escaping OneShotDownloadCallback) {
|
||||
precondition(Thread.isMainThread)
|
||||
OneShotDownloadManager.shared.download(url, completion)
|
||||
}
|
||||
|
||||
public func download(_ urlRequest: URLRequest, _ completion: @escaping OneShotDownloadCallback) {
|
||||
precondition(Thread.isMainThread)
|
||||
OneShotDownloadManager.shared.download(urlRequest, completion)
|
||||
}
|
||||
|
||||
// MARK: - Downloading using a cache
|
||||
|
||||
private struct WebCacheRecord {
|
||||
|
||||
let url: URL
|
||||
let dateDownloaded: Date
|
||||
let data: Data
|
||||
let response: URLResponse
|
||||
}
|
||||
|
||||
private final class WebCache {
|
||||
|
||||
private var cache = [URL: WebCacheRecord]()
|
||||
|
||||
func cleanup(_ cleanupInterval: TimeInterval) {
|
||||
|
||||
let cutoffDate = Date(timeInterval: -cleanupInterval, since: Date())
|
||||
cache.keys.forEach { (key) in
|
||||
let cacheRecord = self[key]!
|
||||
if shouldDelete(cacheRecord, cutoffDate) {
|
||||
cache[key] = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func shouldDelete(_ cacheRecord: WebCacheRecord, _ cutoffDate: Date) -> Bool {
|
||||
|
||||
return cacheRecord.dateDownloaded < cutoffDate
|
||||
}
|
||||
|
||||
subscript(_ url: URL) -> WebCacheRecord? {
|
||||
get {
|
||||
return cache[url]
|
||||
}
|
||||
set {
|
||||
if let cacheRecord = newValue {
|
||||
cache[url] = cacheRecord
|
||||
}
|
||||
else {
|
||||
cache[url] = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// URLSessionConfiguration has a cache policy.
|
||||
// But we don’t know how it works, and the unimplemented parts spook us a bit.
|
||||
// So we use a cache that works exactly as we want it to work.
|
||||
// It also makes sure we don’t have multiple requests for the same URL at the same time.
|
||||
|
||||
private struct CallbackRecord {
|
||||
let url: URL
|
||||
let completion: OneShotDownloadCallback
|
||||
}
|
||||
|
||||
private final class DownloadWithCacheManager {
|
||||
|
||||
static let shared = DownloadWithCacheManager()
|
||||
private var cache = WebCache()
|
||||
private static let timeToLive: TimeInterval = 10 * 60 // 10 minutes
|
||||
private static let cleanupInterval: TimeInterval = 5 * 60 // clean up the cache at most every 5 minutes
|
||||
private var lastCleanupDate = Date()
|
||||
private var pendingCallbacks = [CallbackRecord]()
|
||||
private var urlsInProgress = Set<URL>()
|
||||
|
||||
func download(_ url: URL, _ completion: @escaping OneShotDownloadCallback, forceRedownload: Bool = false) {
|
||||
|
||||
if lastCleanupDate.timeIntervalSinceNow < -DownloadWithCacheManager.cleanupInterval {
|
||||
lastCleanupDate = Date()
|
||||
cache.cleanup(DownloadWithCacheManager.timeToLive)
|
||||
}
|
||||
|
||||
if !forceRedownload {
|
||||
let cacheRecord: WebCacheRecord? = cache[url]
|
||||
if let cacheRecord = cacheRecord {
|
||||
completion(cacheRecord.data, cacheRecord.response, nil)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
let callbackRecord = CallbackRecord(url: url, completion: completion)
|
||||
pendingCallbacks.append(callbackRecord)
|
||||
if urlsInProgress.contains(url) {
|
||||
return // The completion handler will get called later.
|
||||
}
|
||||
urlsInProgress.insert(url)
|
||||
|
||||
OneShotDownloadManager.shared.download(url) { (data, response, error) in
|
||||
|
||||
self.urlsInProgress.remove(url)
|
||||
|
||||
if let data = data, let response = response, response.statusIsOK, error == nil {
|
||||
let cacheRecord = WebCacheRecord(url: url, dateDownloaded: Date(), data: data, response: response)
|
||||
self.cache[url] = cacheRecord
|
||||
}
|
||||
|
||||
var callbackCount = 0
|
||||
self.pendingCallbacks.forEach{ (callbackRecord) in
|
||||
if url == callbackRecord.url {
|
||||
callbackRecord.completion(data, response, error)
|
||||
callbackCount += 1
|
||||
}
|
||||
}
|
||||
self.pendingCallbacks.removeAll(where: { (callbackRecord) -> Bool in
|
||||
return callbackRecord.url == url
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func downloadUsingCache(_ url: URL, _ completion: @escaping OneShotDownloadCallback) {
|
||||
precondition(Thread.isMainThread)
|
||||
DownloadWithCacheManager.shared.download(url, completion)
|
||||
}
|
||||
|
||||
public func downloadAddingToCache(_ url: URL, _ completion: @escaping OneShotDownloadCallback) {
|
||||
precondition(Thread.isMainThread)
|
||||
DownloadWithCacheManager.shared.download(url, completion, forceRedownload: true)
|
||||
}
|
||||
@@ -139,7 +139,7 @@ private extension SingleFaviconDownloader {
|
||||
return
|
||||
}
|
||||
|
||||
downloadUsingCache(url) { (data, response, error) in
|
||||
Downloader.shared.download(url) { (data, response, error) in
|
||||
|
||||
if let data = data, !data.isEmpty, let response = response, response.statusIsOK, error == nil {
|
||||
self.saveToDisk(data)
|
||||
|
||||
@@ -20,7 +20,7 @@ struct HTMLMetadataDownloader {
|
||||
return
|
||||
}
|
||||
|
||||
downloadUsingCache(actualURL) { (data, response, error) in
|
||||
Downloader.shared.download(actualURL) { (data, response, error) in
|
||||
if let data = data, !data.isEmpty, let response = response, response.statusIsOK, error == nil {
|
||||
let urlToUse = response.url ?? actualURL
|
||||
let parserData = ParserData(url: urlToUse.absoluteString, data: data)
|
||||
|
||||
@@ -103,7 +103,7 @@ private extension ImageDownloader {
|
||||
return
|
||||
}
|
||||
|
||||
downloadUsingCache(imageURL) { (data, response, error) in
|
||||
Downloader.shared.download(imageURL) { (data, response, error) in
|
||||
|
||||
if let data = data, !data.isEmpty, let response = response, response.statusIsOK, error == nil {
|
||||
self.saveToDisk(url, data)
|
||||
|
||||
Reference in New Issue
Block a user