OSDN Git Service

swiftlint 'line_length'の警告を修正
[kcd/KCD.git] / KCD / CustomHTTPProtocol.swift
1 //
2 //  CustomHTTPProtocol.swift
3 //  KCD
4 //
5 //  Created by Hori,Masaki on 2017/02/10.
6 //  Copyright © 2017年 Hori,Masaki. All rights reserved.
7 //
8
9 import Cocoa
10
11
12 protocol CustomHTTPProtocolDelegate: class {
13     func customHTTPProtocol(_ proto: CustomHTTPProtocol, didRecieve response: URLResponse)
14     func customHTTPProtocol(_ proto: CustomHTTPProtocol, didRecieve data: Data)
15     func customHTTPProtocolDidFinishLoading(_ proto: CustomHTTPProtocol)
16     func customHTTPProtocol(_ proto: CustomHTTPProtocol, didFailWithError error: Error)
17 }
18
19 class CustomHTTPProtocol: URLProtocol {
20     fileprivate static let requestProperty = "com.masakih.KCD.requestProperty"
21     static var classDelegate: CustomHTTPProtocolDelegate?
22     fileprivate static let cachedExtensions = ["swf", "flv", "png", "jpg", "jpeg", "mp3"]
23     fileprivate static let cacheFileURL: URL = ApplicationDirecrories.support.appendingPathComponent("Caches")
24     fileprivate static let kcdURLCache = URLCache(memoryCapacity: 32 * 1024 * 1024,
25                                                   diskCapacity: 1024 * 1024 * 1024,
26                                                   diskPath: cacheFileURL.path)
27     
28     class func clearCache() { kcdURLCache.removeAllCachedResponses() }
29     class func start() { URLProtocol.registerClass(CustomHTTPProtocol.self) }
30     
31     override class func canInit(with request: URLRequest) -> Bool {
32         if let _ = property(forKey: requestProperty, in: request) { return false }
33         if let scheme = request.url?.scheme?.lowercased(),
34             (scheme == "http" || scheme == "https")
35         { return true }
36         return false
37     }
38     override class func canonicalRequest(for request: URLRequest) -> URLRequest {
39         return request
40     }
41     
42     fileprivate var delegate: CustomHTTPProtocolDelegate? { return CustomHTTPProtocol.classDelegate }
43     
44     fileprivate var session: URLSession?
45     fileprivate var dataTask: URLSessionDataTask? 
46     fileprivate var cachePolicy: URLCache.StoragePolicy = .notAllowed
47     fileprivate var data: Data = Data()
48     fileprivate var didRetry: Bool = false
49     fileprivate var didRecieveData: Bool = false
50     
51     private func use(_ cache: CachedURLResponse) {
52         delegate?.customHTTPProtocol(self, didRecieve: cache.response)
53         client?.urlProtocol(self, didReceive: cache.response, cacheStoragePolicy: .allowed)
54         
55         delegate?.customHTTPProtocol(self, didRecieve: cache.data)
56         client?.urlProtocol(self, didLoad: cache.data)
57         
58         delegate?.customHTTPProtocolDidFinishLoading(self)
59         client?.urlProtocolDidFinishLoading(self)
60     }
61     override func startLoading() {
62         guard let newRequest = (request as NSObject).mutableCopy() as? NSMutableURLRequest
63             else { fatalError("Can not convert to NSMutableURLRequest") }
64         CustomHTTPProtocol.setProperty(true,
65                                        forKey: CustomHTTPProtocol.requestProperty,
66                                        in: newRequest)
67         
68         if let cache = CustomHTTPProtocol.kcdURLCache.cachedResponse(for: request),
69             let info = cache.userInfo,
70             let expires = info["Expires"] as? Date,
71             Date().compare(expires) == .orderedAscending
72         {
73             use(cache)
74             if let name = request.url?.lastPathComponent {
75                 Debug.print("Use cache for", name, level: .full)
76             } else {
77                 Debug.print("Use cache", level: .full)
78             }
79             
80             return
81         }
82         
83         let config = URLSessionConfiguration.default
84         config.protocolClasses = [type(of: self)]
85         session = URLSession(configuration: config, delegate: self, delegateQueue: nil)
86         dataTask = session?.dataTask(with: newRequest as URLRequest)
87         dataTask?.resume()
88     }
89     override func stopLoading() {
90         dataTask?.cancel()
91     }
92 }
93
94 extension CustomHTTPProtocol: URLSessionDataDelegate {
95     func urlSession(_ session: URLSession,
96                     task: URLSessionTask,
97                     willPerformHTTPRedirection response: HTTPURLResponse,
98                     newRequest request: URLRequest,
99                     completionHandler: @escaping (URLRequest?) -> Void) {
100         Debug.print("willPerformHTTPRedirection", level: .full)
101         client?.urlProtocol(self, wasRedirectedTo: request, redirectResponse: response)
102         completionHandler(request)
103     }
104     func urlSession(_ session: URLSession,
105                     dataTask: URLSessionDataTask,
106                     didReceive response: URLResponse,
107                     completionHandler: @escaping (URLSession.ResponseDisposition) -> Void) {
108         Debug.print("didReceive response", level: .full)
109         
110         if let response = response as? HTTPURLResponse,
111             let request = dataTask.originalRequest {
112             cachePolicy = CacheStoragePolicy(for: request, response: response)
113         }
114         
115         delegate?.customHTTPProtocol(self, didRecieve: response)
116         client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: cachePolicy)
117         completionHandler(.allow)
118     }
119     func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
120         Debug.print("didReceive data", level: .full)
121         if cachePolicy == .allowed {
122             self.data.append(data)
123         }
124         delegate?.customHTTPProtocol(self, didRecieve: data)
125         client?.urlProtocol(self, didLoad: data)
126         didRecieveData = true
127     }
128     
129     // cfurlErrorNetworkConnectionLost の場合はもう一度試す
130     private func canRetry(error: NSError) -> Bool {
131         guard error.code == Int(CFNetworkErrors.cfurlErrorNetworkConnectionLost.rawValue),
132             !didRetry,
133             !didRecieveData
134             else { return false }
135         print("Retry download...")
136         return true
137     }
138     func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
139         if let error = error {
140             if canRetry(error: error as NSError),
141                 let request = task.originalRequest
142             {
143                 didRetry = true
144                 dataTask = session.dataTask(with: request)
145                 dataTask?.resume()
146                 return
147             }
148             Debug.print("didCompleteWithError ERROR", level: .full)
149             delegate?.customHTTPProtocol(self, didFailWithError: error)
150             client?.urlProtocol(self, didFailWithError: error)
151             return
152         }
153         Debug.print("didCompleteWithError SUCCESS", level: .full)
154         delegate?.customHTTPProtocolDidFinishLoading(self)
155         client?.urlProtocolDidFinishLoading(self)
156         
157         if cachePolicy == .allowed,
158             let ext = task.originalRequest?.url?.pathExtension,
159             CustomHTTPProtocol.cachedExtensions.contains(ext),
160             let request = task.originalRequest,
161             let response = task.response as? HTTPURLResponse,
162             let expires = response.expires()
163         {
164             let cache = CachedURLResponse(response: response,
165                                           data: data,
166                                           userInfo: ["Expires": expires],
167                                           storagePolicy: .allowed)
168             CustomHTTPProtocol.kcdURLCache.storeCachedResponse(cache, for: request)
169         }
170     }
171 }
172
173 extension HTTPURLResponse {
174     private var httpDateFormatter: DateFormatter {
175         let formatter = DateFormatter()
176         formatter.dateFormat = "EEE',' dd' 'MMM' 'yyyy HH':'mm':'ss zzz"
177         formatter.locale = Locale(identifier: "en_US")
178         return formatter
179     }
180     func expires() -> Date? {
181         if let cc = (allHeaderFields["Cache-Control"] as? String)?.lowercased(),
182             let range = cc.range(of: "max-age="),
183             let s = cc[range.upperBound..<cc.endIndex]
184                 .components(separatedBy: ",")
185                 .first,
186             let age = TimeInterval(s)
187         {
188             return Date(timeIntervalSinceNow: age)
189         }
190         if let ex = (allHeaderFields["Expires"] as? String)?.lowercased(),
191             let exp = httpDateFormatter.date(from: ex)
192         {
193             return exp
194         }
195         return nil
196     }
197 }