OSDN Git Service

9bb625f8142d087c3ac13a71933263fe5b64e05b
[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     
14     func customHTTPProtocol(_ proto: CustomHTTPProtocol, didRecieve response: URLResponse)
15     func customHTTPProtocol(_ proto: CustomHTTPProtocol, didRecieve data: Data)
16     func customHTTPProtocolDidFinishLoading(_ proto: CustomHTTPProtocol)
17     func customHTTPProtocol(_ proto: CustomHTTPProtocol, didFailWithError error: Error)
18 }
19
20
21 /// 生成されたスレッド上でoprationを実行する
22 private final class ThreadOperator: NSObject {
23     
24     private let thread: Thread
25     private let modes: [String]
26     private var operation: (() -> Void)?
27     
28     override init() {
29         
30         thread = Thread.current
31         if let mode = RunLoop.current.currentMode {
32             
33             modes = [mode, .defaultRunLoopMode].map { $0.rawValue }
34             
35         } else {
36             
37             modes = [RunLoopMode.defaultRunLoopMode.rawValue]
38         }
39         
40         super.init()
41     }
42     
43     func execute(_ operation: @escaping () -> Void) {
44         
45         self.operation = operation
46         perform(#selector(ThreadOperator.operate),
47                 on: thread,
48                 with: nil,
49                 waitUntilDone: true,
50                 modes: modes)
51         self.operation = nil
52     }
53     
54     @objc private func operate() {
55         
56         operation?()
57     }
58 }
59
60 extension HTTPURLResponse {
61     
62     private static var dateFormatter: DateFormatter = {
63         
64         let formatter = DateFormatter()
65         formatter.dateFormat = "EEE',' dd' 'MMM' 'yyyy HH':'mm':'ss zzz"
66         formatter.locale = Locale(identifier: "en_US")
67         
68         return formatter
69     }()
70     
71     func expires() -> Date? {
72         
73         if let cc = (allHeaderFields["Cache-Control"] as? String)?.lowercased(),
74             let range = cc.range(of: "max-age="),
75             let s = cc[range.upperBound...]
76                 .components(separatedBy: ",")
77                 .first,
78             let age = TimeInterval(s) {
79             
80             return Date(timeIntervalSinceNow: age)
81         }
82         
83         if let ex = (allHeaderFields["Expires"] as? String)?.lowercased(),
84             let exp = HTTPURLResponse.dateFormatter.date(from: ex) {
85             
86             return exp
87         }
88         
89         return nil
90     }
91 }
92
93 extension URLCache {
94     
95     static let kcd = URLCache(memoryCapacity: 32 * 1024 * 1024,
96                               diskCapacity: 1024 * 1024 * 1024,
97                               diskPath: ApplicationDirecrories.support.appendingPathComponent("Caches").path)
98     static let cachedExtensions = ["swf", "flv", "png", "jpg", "jpeg", "mp3"]
99     
100     func storeIfNeeded(for task: URLSessionTask, data: Data) {
101         
102         if let request = task.originalRequest,
103             let response = task.response as? HTTPURLResponse,
104             let ext = request.url?.pathExtension,
105             URLCache.cachedExtensions.contains(ext),
106             let expires = response.expires() {
107             
108             let cache = CachedURLResponse(response: response,
109                                           data: data,
110                                           userInfo: ["Expires": expires],
111                                           storagePolicy: .allowed)
112             storeCachedResponse(cache, for: request)
113         }
114     }
115     
116     func validCach(for request: URLRequest) -> CachedURLResponse? {
117         
118         if let cache = cachedResponse(for: request),
119             let info = cache.userInfo,
120             let expires = info["Expires"] as? Date,
121             Date().compare(expires) == .orderedAscending {
122             
123                 return cache
124         }
125         
126         return nil
127     }
128 }
129
130 final class CustomHTTPProtocol: URLProtocol {
131     
132     private static let requestProperty = "com.masakih.KCD.requestProperty"
133     static var classDelegate: CustomHTTPProtocolDelegate?
134     
135     class func clearCache() {
136         
137         URLCache.kcd.removeAllCachedResponses()
138     }
139     
140     class func start() {
141         
142         URLProtocol.registerClass(CustomHTTPProtocol.self)
143     }
144     
145     override class func canInit(with request: URLRequest) -> Bool {
146         
147         if let _ = property(forKey: requestProperty, in: request) { return false }
148         
149         if let scheme = request.url?.scheme?.lowercased(),
150             (scheme == "http" || scheme == "https") {
151             
152             return true
153         }
154         
155         return false
156     }
157     
158     override class func canonicalRequest(for request: URLRequest) -> URLRequest {
159         
160         return request
161     }
162     
163     private var delegate: CustomHTTPProtocolDelegate? { return CustomHTTPProtocol.classDelegate }
164     
165     private var session: URLSession?
166     private var dataTask: URLSessionDataTask?
167     private var cachePolicy: URLCache.StoragePolicy = .notAllowed
168     private var data: Data = Data()
169     private var didRetry: Bool = false
170     private var didRecieveData: Bool = false
171     
172     private var threadOperator: ThreadOperator?
173     
174     private func use(_ cache: CachedURLResponse) {
175         
176         delegate?.customHTTPProtocol(self, didRecieve: cache.response)
177         client?.urlProtocol(self, didReceive: cache.response, cacheStoragePolicy: .allowed)
178         
179         delegate?.customHTTPProtocol(self, didRecieve: cache.data)
180         client?.urlProtocol(self, didLoad: cache.data)
181         
182         delegate?.customHTTPProtocolDidFinishLoading(self)
183         client?.urlProtocolDidFinishLoading(self)
184     }
185     
186     override func startLoading() {
187         
188         guard let newRequest = (request as NSObject).mutableCopy() as? NSMutableURLRequest else {
189             
190             fatalError("Can not convert to NSMutableURLRequest")
191         }
192         
193         URLProtocol.setProperty(true,
194                                 forKey: CustomHTTPProtocol.requestProperty,
195                                 in: newRequest)
196         
197         if let cache = URLCache.kcd.validCach(for: request) {
198             
199             use(cache)
200             
201             Debug.excute(level: .full) {
202                 
203                 if let name = request.url?.lastPathComponent {
204                     
205                     print("Use cache for", name)
206                     
207                 } else {
208                     
209                     print("Use cache")
210                 }
211             }
212             
213             return
214         }
215         
216         threadOperator = ThreadOperator()
217         
218         let config = URLSessionConfiguration.default
219         session = URLSession(configuration: config, delegate: self, delegateQueue: nil)
220         dataTask = session?.dataTask(with: newRequest as URLRequest)
221         dataTask?.resume()
222     }
223     
224     override func stopLoading() {
225         
226         dataTask?.cancel()
227     }
228 }
229
230 extension CustomHTTPProtocol: URLSessionDataDelegate {
231     
232     func urlSession(_ session: URLSession,
233                     task: URLSessionTask,
234                     willPerformHTTPRedirection response: HTTPURLResponse,
235                     newRequest request: URLRequest,
236                     completionHandler: @escaping (URLRequest?) -> Void) {
237         
238         threadOperator?.execute { [weak self] in
239             
240             guard let `self` = self else {
241                 
242                 return
243             }
244             
245             Debug.print("willPerformHTTPRedirection", level: .full)
246             
247             self.client?.urlProtocol(self, wasRedirectedTo: request, redirectResponse: response)
248             
249             completionHandler(request)
250         }
251     }
252     
253     func urlSession(_ session: URLSession,
254                     dataTask: URLSessionDataTask,
255                     didReceive response: URLResponse,
256                     completionHandler: @escaping (URLSession.ResponseDisposition) -> Void) {
257         
258         threadOperator?.execute { [weak self] in
259             
260             guard let `self` = self else {
261                 
262                 return
263             }
264             
265             Debug.print("didReceive response", level: .full)
266             
267             if let response = response as? HTTPURLResponse,
268                 let request = dataTask.originalRequest {
269                 
270                 self.cachePolicy = cacheStoragePolicy(for: request, response: response)
271             }
272             
273             self.delegate?.customHTTPProtocol(self, didRecieve: response)
274             self.client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: self.cachePolicy)
275             
276             completionHandler(.allow)
277         }
278     }
279     
280     func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
281         
282         threadOperator?.execute { [weak self] in
283             
284             guard let `self` = self else {
285                 
286                 return
287             }
288             
289             Debug.print("didReceive data", level: .full)
290             if self.cachePolicy == .allowed {
291                 
292                 self.data.append(data)
293             }
294             
295             self.delegate?.customHTTPProtocol(self, didRecieve: data)
296             self.client?.urlProtocol(self, didLoad: data)
297             self.didRecieveData = true
298         }
299     }
300     
301     // cfurlErrorNetworkConnectionLost の場合はもう一度試す
302     private func canRetry(error: NSError) -> Bool {
303         
304         guard error.code == Int(CFNetworkErrors.cfurlErrorNetworkConnectionLost.rawValue),
305             !didRetry,
306             !didRecieveData else {
307                 
308                 return false
309         }
310         
311         print("Retry download...")
312         
313         return true
314     }
315     
316     func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
317         
318         threadOperator?.execute { [weak self] in
319             
320             guard let `self` = self else {
321                 
322                 return
323             }
324             
325             if let error = error {
326                 
327                 if self.canRetry(error: error as NSError),
328                     let request = task.originalRequest {
329                     
330                     self.didRetry = true
331                     self.dataTask = session.dataTask(with: request)
332                     self.dataTask?.resume()
333                     
334                     return
335                 }
336                 
337                 Debug.print("didCompleteWithError ERROR", level: .full)
338                 
339                 self.delegate?.customHTTPProtocol(self, didFailWithError: error)
340                 self.client?.urlProtocol(self, didFailWithError: error)
341                 
342                 return
343             }
344             
345             Debug.print("didCompleteWithError SUCCESS", level: .full)
346             
347             self.delegate?.customHTTPProtocolDidFinishLoading(self)
348             self.client?.urlProtocolDidFinishLoading(self)
349             
350             if self.cachePolicy == .allowed {
351                 
352                 URLCache.kcd.storeIfNeeded(for: task, data: self.data)
353             }
354         }
355     }
356 }