OSDN Git Service

機能ごとに分割した
[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 fileprivate class ThreadOperator: NSObject {
20     private let thread: Thread
21     private let modes: [String]
22     private var operation: (() -> Void)?
23     
24     override init() {
25         thread = Thread.current
26         let mode = RunLoop.current.currentMode ?? .defaultRunLoopMode
27         if mode == .defaultRunLoopMode {
28             modes = [mode.rawValue]
29         } else {
30             modes = [mode, .defaultRunLoopMode].map { $0.rawValue }
31         }
32         super.init()
33     }
34     func execute(_ operation: @escaping () -> Void) {
35         self.operation = operation
36         perform(#selector(ThreadOperator.operate),
37                 on: thread,
38                 with: nil,
39                 waitUntilDone: true,
40                 modes: modes)
41         self.operation = nil
42     }
43     func operate() {
44         operation?()
45     }
46 }
47
48 extension HTTPURLResponse {
49     private var httpDateFormatter: DateFormatter {
50         let formatter = DateFormatter()
51         formatter.dateFormat = "EEE',' dd' 'MMM' 'yyyy HH':'mm':'ss zzz"
52         formatter.locale = Locale(identifier: "en_US")
53         return formatter
54     }
55     func expires() -> Date? {
56         if let cc = (allHeaderFields["Cache-Control"] as? String)?.lowercased(),
57             let range = cc.range(of: "max-age="),
58             let s = cc[range.upperBound..<cc.endIndex]
59                 .components(separatedBy: ",")
60                 .first,
61             let age = TimeInterval(s) {
62             return Date(timeIntervalSinceNow: age)
63         }
64         if let ex = (allHeaderFields["Expires"] as? String)?.lowercased(),
65             let exp = httpDateFormatter.date(from: ex) {
66             return exp
67         }
68         return nil
69     }
70 }
71
72 extension URLCache {
73     static let kcd = URLCache(memoryCapacity: 32 * 1024 * 1024,
74                                       diskCapacity: 1024 * 1024 * 1024,
75                                       diskPath: ApplicationDirecrories.support.appendingPathComponent("Caches").path)
76     static let cachedExtensions = ["swf", "flv", "png", "jpg", "jpeg", "mp3"]
77     
78     func storeIfNeeded(for task: URLSessionTask, data: Data) {
79         if let request = task.originalRequest,
80             let response = task.response as? HTTPURLResponse,
81             let ext = request.url?.pathExtension,
82             URLCache.cachedExtensions.contains(ext),
83             let expires = response.expires() {
84             let cache = CachedURLResponse(response: response,
85                                           data: data,
86                                           userInfo: ["Expires": expires],
87                                           storagePolicy: .allowed)
88             storeCachedResponse(cache, for: request)
89         }
90     }
91     func validCach(for request: URLRequest) -> CachedURLResponse? {
92         if let cache = cachedResponse(for: request),
93             let info = cache.userInfo,
94             let expires = info["Expires"] as? Date,
95             Date().compare(expires) == .orderedAscending {
96                 return cache
97         }
98         return nil
99     }
100 }
101
102 class CustomHTTPProtocol: URLProtocol {
103     fileprivate static let requestProperty = "com.masakih.KCD.requestProperty"
104     static var classDelegate: CustomHTTPProtocolDelegate?
105     
106     class func clearCache() { URLCache.kcd.removeAllCachedResponses() }
107     class func start() { URLProtocol.registerClass(CustomHTTPProtocol.self) }
108     
109     override class func canInit(with request: URLRequest) -> Bool {
110         if let _ = property(forKey: requestProperty, in: request) { return false }
111         if let scheme = request.url?.scheme?.lowercased(),
112             (scheme == "http" || scheme == "https") {
113             return true
114         }
115         return false
116     }
117     override class func canonicalRequest(for request: URLRequest) -> URLRequest {
118         return request
119     }
120     
121     fileprivate var delegate: CustomHTTPProtocolDelegate? { return CustomHTTPProtocol.classDelegate }
122     
123     fileprivate var session: URLSession?
124     fileprivate var dataTask: URLSessionDataTask? 
125     fileprivate var cachePolicy: URLCache.StoragePolicy = .notAllowed
126     fileprivate var data: Data = Data()
127     fileprivate var didRetry: Bool = false
128     fileprivate var didRecieveData: Bool = false
129     
130     fileprivate var threadOperator: ThreadOperator?
131     
132     private func use(_ cache: CachedURLResponse) {
133         delegate?.customHTTPProtocol(self, didRecieve: cache.response)
134         client?.urlProtocol(self, didReceive: cache.response, cacheStoragePolicy: .allowed)
135         
136         delegate?.customHTTPProtocol(self, didRecieve: cache.data)
137         client?.urlProtocol(self, didLoad: cache.data)
138         
139         delegate?.customHTTPProtocolDidFinishLoading(self)
140         client?.urlProtocolDidFinishLoading(self)
141     }
142     override func startLoading() {
143         guard let newRequest = (request as NSObject).mutableCopy() as? NSMutableURLRequest
144             else { fatalError("Can not convert to NSMutableURLRequest") }
145         URLProtocol.setProperty(true,
146                                 forKey: CustomHTTPProtocol.requestProperty,
147                                 in: newRequest)
148         
149         if let cache = URLCache.kcd.validCach(for: request) {
150             use(cache)
151             if let name = request.url?.lastPathComponent {
152                 Debug.print("Use cache for", name, level: .full)
153             } else {
154                 Debug.print("Use cache", level: .full)
155             }
156             
157             return
158         }
159         
160         threadOperator = ThreadOperator()
161         
162         let config = URLSessionConfiguration.default
163         session = URLSession(configuration: config, delegate: self, delegateQueue: nil)
164         dataTask = session?.dataTask(with: newRequest as URLRequest)
165         dataTask?.resume()
166     }
167     override func stopLoading() {
168         dataTask?.cancel()
169     }
170 }
171
172 extension CustomHTTPProtocol: URLSessionDataDelegate {
173     func urlSession(_ session: URLSession,
174                     task: URLSessionTask,
175                     willPerformHTTPRedirection response: HTTPURLResponse,
176                     newRequest request: URLRequest,
177                     completionHandler: @escaping (URLRequest?) -> Void) {
178         threadOperator?.execute { [weak self] in
179             guard let `self` = self else { return }
180             Debug.print("willPerformHTTPRedirection", level: .full)
181             self.client?.urlProtocol(self, wasRedirectedTo: request, redirectResponse: response)
182             completionHandler(request)
183         }
184     }
185     func urlSession(_ session: URLSession,
186                     dataTask: URLSessionDataTask,
187                     didReceive response: URLResponse,
188                     completionHandler: @escaping (URLSession.ResponseDisposition) -> Void) {
189         threadOperator?.execute { [weak self] in
190             guard let `self` = self else { return }
191             Debug.print("didReceive response", level: .full)
192             
193             if let response = response as? HTTPURLResponse,
194                 let request = dataTask.originalRequest {
195                 self.cachePolicy = CacheStoragePolicy(for: request, response: response)
196             }
197             
198             self.delegate?.customHTTPProtocol(self, didRecieve: response)
199             self.client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: self.cachePolicy)
200             completionHandler(.allow)
201         }
202     }
203     func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
204         threadOperator?.execute { [weak self] in
205             guard let `self` = self else { return }
206             Debug.print("didReceive data", level: .full)
207             if self.cachePolicy == .allowed {
208                 self.data.append(data)
209             }
210             self.delegate?.customHTTPProtocol(self, didRecieve: data)
211             self.client?.urlProtocol(self, didLoad: data)
212             self.didRecieveData = true
213         }
214     }
215     
216     // cfurlErrorNetworkConnectionLost の場合はもう一度試す
217     private func canRetry(error: NSError) -> Bool {
218         guard error.code == Int(CFNetworkErrors.cfurlErrorNetworkConnectionLost.rawValue),
219             !didRetry,
220             !didRecieveData
221             else { return false }
222         print("Retry download...")
223         return true
224     }
225     func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
226         threadOperator?.execute { [weak self] in
227             guard let `self` = self else { return }
228             if let error = error {
229                 if self.canRetry(error: error as NSError),
230                     let request = task.originalRequest {
231                     self.didRetry = true
232                     self.dataTask = session.dataTask(with: request)
233                     self.dataTask?.resume()
234                     return
235                 }
236                 Debug.print("didCompleteWithError ERROR", level: .full)
237                 self.delegate?.customHTTPProtocol(self, didFailWithError: error)
238                 self.client?.urlProtocol(self, didFailWithError: error)
239                 return
240             }
241             Debug.print("didCompleteWithError SUCCESS", level: .full)
242             self.delegate?.customHTTPProtocolDidFinishLoading(self)
243             self.client?.urlProtocolDidFinishLoading(self)
244             
245             if self.cachePolicy == .allowed {
246                 URLCache.kcd.storeIfNeeded(for: task, data: self.data)
247             }
248         }
249     }
250 }