Skip to content

Commit ca9d675

Browse files
committed
URLCache store and fetch methods implemented using NSKeyedArchiver and NSKeyedUnarchiver
1 parent 98b2c73 commit ca9d675

File tree

3 files changed

+162
-183
lines changed

3 files changed

+162
-183
lines changed

Foundation/URLCache.swift

Lines changed: 127 additions & 128 deletions
Original file line numberDiff line numberDiff line change
@@ -125,24 +125,10 @@ open class CachedURLResponse : NSObject, NSSecureCoding, NSCopying {
125125
open class URLCache : NSObject {
126126

127127
private static let sharedSyncQ = DispatchQueue(label: "org.swift.URLCache.sharedSyncQ")
128-
129-
private static var sharedCache: URLCache? {
130-
willSet {
131-
URLCache.sharedCache?.syncQ.sync {
132-
URLCache.sharedCache?._databaseClient?.close()
133-
URLCache.sharedCache?.flushDatabase()
134-
}
135-
}
136-
didSet {
137-
URLCache.sharedCache?.syncQ.sync {
138-
URLCache.sharedCache?.setupCacheDatabaseIfNotExist()
139-
}
140-
}
141-
}
128+
private static var sharedCache: URLCache?
142129

143130
private let syncQ = DispatchQueue(label: "org.swift.URLCache.syncQ")
144-
private let _baseDiskPath: String?
145-
private var _databaseClient: _CacheSQLiteClient?
131+
private var persistence: CachePersistence?
146132

147133
/*!
148134
@method sharedURLCache
@@ -169,19 +155,31 @@ open class URLCache : NSObject {
169155
} else {
170156
let fourMegaByte = 4 * 1024 * 1024
171157
let twentyMegaByte = 20 * 1024 * 1024
172-
let cacheDirectoryPath = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first?.path ?? "\(NSHomeDirectory())/Library/Caches/"
173-
let path = "\(cacheDirectoryPath)\(Bundle.main.bundleIdentifier ?? UUID().uuidString)"
174-
let cache = URLCache(memoryCapacity: fourMegaByte, diskCapacity: twentyMegaByte, diskPath: path)
158+
let bundleIdentifier = Bundle.main.bundleIdentifier ?? UUID().uuidString
159+
let cacheDirectoryPath = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first?.path ?? "\(NSHomeDirectory())/Library/Caches"
160+
let diskPath = cacheDirectoryPath + "/" + bundleIdentifier
161+
let cache = URLCache(memoryCapacity: fourMegaByte, diskCapacity: twentyMegaByte, diskPath: diskPath)
175162
sharedCache = cache
163+
cache.persistence?.setupCacheDirectory()
176164
return cache
177165
}
178166
}
179167
}
180168
set {
181-
sharedSyncQ.sync { sharedCache = newValue }
169+
guard newValue !== sharedCache else { return }
170+
171+
sharedSyncQ.sync {
172+
// Remove previous data before resetting new URLCache instance
173+
URLCache.sharedCache?.persistence?.deleteAllCaches()
174+
sharedCache = newValue
175+
newValue.persistence?.setupCacheDirectory()
176+
}
182177
}
183178
}
184179

180+
private var allCaches: [String: CachedURLResponse] = [:]
181+
private var cacheSizeInMemory: Int = 0
182+
185183
/*!
186184
@method initWithMemoryCapacity:diskCapacity:diskPath:
187185
@abstract Initializes an URLCache with the given capacity and
@@ -198,8 +196,11 @@ open class URLCache : NSObject {
198196
public init(memoryCapacity: Int, diskCapacity: Int, diskPath path: String?) {
199197
self.memoryCapacity = memoryCapacity
200198
self.diskCapacity = diskCapacity
201-
self._baseDiskPath = path
202-
199+
200+
if let _path = path {
201+
self.persistence = CachePersistence(path: _path)
202+
}
203+
203204
super.init()
204205
}
205206

@@ -214,7 +215,27 @@ open class URLCache : NSObject {
214215
request, or nil if there is no NSCachedURLResponse stored with the
215216
given request.
216217
*/
217-
open func cachedResponse(for request: URLRequest) -> CachedURLResponse? { NSUnimplemented() }
218+
open func cachedResponse(for request: URLRequest) -> CachedURLResponse? {
219+
var cache: CachedURLResponse?
220+
self.syncQ.sync {
221+
let identifier = request.cacheFileIdentifier
222+
if let inMemoryCache = self.allCaches[identifier] {
223+
cache = inMemoryCache
224+
} else if let _persistence = self.persistence {
225+
guard let inDiskCache = _persistence.cachedResponse(for: request) else {
226+
cache = nil
227+
return
228+
}
229+
230+
self.allCaches[identifier] = inDiskCache
231+
self.cacheSizeInMemory += inDiskCache.data.count
232+
cache = inDiskCache
233+
} else {
234+
cache = nil
235+
}
236+
}
237+
return cache
238+
}
218239

219240
/*!
220241
@method storeCachedResponse:forRequest:
@@ -223,7 +244,19 @@ open class URLCache : NSObject {
223244
@param cachedResponse The cached response to store.
224245
@param request the NSURLRequest to use as a key for the storage.
225246
*/
226-
open func storeCachedResponse(_ cachedResponse: CachedURLResponse, for request: URLRequest) { NSUnimplemented() }
247+
open func storeCachedResponse(_ cachedResponse: CachedURLResponse, for request: URLRequest) {
248+
self.syncQ.sync {
249+
purgeOldMemoryForNewCacheIfNeeded(cachedResponse)
250+
251+
let identifier = request.cacheFileIdentifier
252+
self.allCaches[identifier] = cachedResponse
253+
self.cacheSizeInMemory += cachedResponse.data.count
254+
255+
if self.persistence != nil {
256+
self.persistence?.saveCachedResponse(cachedResponse, for: request)
257+
}
258+
}
259+
}
227260

228261
/*!
229262
@method removeCachedResponseForRequest:
@@ -272,7 +305,9 @@ open class URLCache : NSObject {
272305
usage of the in-memory cache.
273306
@result the current usage of the in-memory cache of the receiver.
274307
*/
275-
open var currentMemoryUsage: Int { NSUnimplemented() }
308+
open var currentMemoryUsage: Int {
309+
return self.syncQ.sync { self.cacheSizeInMemory }
310+
}
276311

277312
/*!
278313
@method currentDiskUsage
@@ -282,17 +317,14 @@ open class URLCache : NSObject {
282317
usage of the on-disk cache.
283318
@result the current usage of the on-disk cache of the receiver.
284319
*/
285-
open var currentDiskUsage: Int { NSUnimplemented() }
320+
open var currentDiskUsage: Int {
321+
return self.syncQ.sync { self.persistence?.cacheSizeInDesk ?? 0 }
322+
}
286323

287-
private func flushDatabase() {
288-
guard let path = _baseDiskPath else { return }
289-
290-
do {
291-
let dbPath = path.appending("/Cache.db")
292-
try FileManager.default.removeItem(atPath: dbPath)
293-
} catch {
294-
fatalError("Unable to flush database for URLCache: \(error.localizedDescription)")
295-
}
324+
private func purgeOldMemoryForNewCacheIfNeeded(_ newCache: CachedURLResponse) {
325+
// 1. Check the in-memory cache size and purge old if needed
326+
// 2. Check the disk-memory cache size and purge old if needed
327+
NSUnimplemented()
296328
}
297329

298330
}
@@ -303,112 +335,79 @@ extension URLCache {
303335
public func removeCachedResponse(for dataTask: URLSessionDataTask) { NSUnimplemented() }
304336
}
305337

306-
extension URLCache {
307-
308-
private func setupCacheDatabaseIfNotExist() {
309-
guard let path = _baseDiskPath else { return }
310-
311-
if !FileManager.default.fileExists(atPath: path) {
312-
do {
313-
try FileManager.default.createDirectory(atPath: path, withIntermediateDirectories: true)
314-
} catch {
315-
fatalError("Unable to create directories for URLCache: \(error.localizedDescription)")
338+
fileprivate struct CachePersistence {
339+
340+
let path: String
341+
342+
var cacheSizeInDesk: Int {
343+
do {
344+
let subFiles = try FileManager.default.subpathsOfDirectory(atPath: path)
345+
var total: Int = 0
346+
for fileName in subFiles {
347+
let attributes = try FileManager.default.attributesOfItem(atPath: path.appending(fileName))
348+
total += (attributes[.size] as? Int) ?? 0
316349
}
317-
}
318-
319-
// Close the currently opened database connection(if any), before creating/replacing the db file
320-
_databaseClient?.close()
321-
322-
let dbPath = path.appending("/Cache.db")
323-
if !FileManager.default.createFile(atPath: dbPath, contents: nil, attributes: nil) {
324-
fatalError("Unable to setup database for URLCache")
325-
}
326-
327-
_databaseClient = _CacheSQLiteClient(databasePath: dbPath)
328-
if _databaseClient == nil {
329-
_databaseClient?.close()
330-
flushDatabase()
331-
fatalError("Unable to setup database for URLCache")
332-
}
333-
334-
if !createTables() {
335-
_databaseClient?.close()
336-
flushDatabase()
337-
fatalError("Unable to setup database for URLCache: Tables not created")
338-
}
339-
340-
if !createIndicesForTables() {
341-
_databaseClient?.close()
342-
flushDatabase()
343-
fatalError("Unable to setup database for URLCache: Indices not created for tables")
350+
return total
351+
} catch {
352+
return 0
344353
}
345354
}
346355

347-
private func createTables() -> Bool {
348-
guard _databaseClient != nil else {
349-
fatalError("Cannot create table before database setup")
350-
}
351-
352-
let tableSQLs = [
353-
"CREATE TABLE cfurl_cache_response(entry_ID INTEGER PRIMARY KEY, version INTEGER, hash_value VARCHAR, storage_policy INTEGER, request_key VARCHAR, time_stamp DATETIME, partition VARCHAR)",
354-
"CREATE TABLE cfurl_cache_receiver_data(entry_ID INTEGER PRIMARY KEY, isDataOnFS INTEGER, receiver_data BLOB)",
355-
"CREATE TABLE cfurl_cache_blob_data(entry_ID INTEGER PRIMARY KEY, response_object BLOB, request_object BLOB, proto_props BLOB, user_info BLOB)",
356-
"CREATE TABLE cfurl_cache_schema_version(schema_version INTEGER)"
357-
]
358-
359-
for sql in tableSQLs {
360-
if let isSuccess = _databaseClient?.execute(sql: sql), !isSuccess {
361-
return false
356+
func setupCacheDirectory() {
357+
do {
358+
if FileManager.default.fileExists(atPath: path) {
359+
try FileManager.default.removeItem(atPath: path)
362360
}
361+
362+
try FileManager.default.createDirectory(atPath: path, withIntermediateDirectories: true)
363+
} catch {
364+
fatalError("Unable to create directories for URLCache: \(error.localizedDescription)")
363365
}
364-
365-
return true
366366
}
367-
368-
private func createIndicesForTables() -> Bool {
369-
guard _databaseClient != nil else {
370-
fatalError("Cannot create table before database setup")
371-
}
372-
373-
let indicesSQLs = [
374-
"CREATE INDEX proto_props_index ON cfurl_cache_blob_data(entry_ID)",
375-
"CREATE INDEX receiver_data_index ON cfurl_cache_receiver_data(entry_ID)",
376-
"CREATE INDEX request_key_index ON cfurl_cache_response(request_key)",
377-
"CREATE INDEX time_stamp_index ON cfurl_cache_response(time_stamp)"
378-
]
379-
380-
for sql in indicesSQLs {
381-
if let isSuccess = _databaseClient?.execute(sql: sql), !isSuccess {
382-
return false
383-
}
367+
368+
func saveCachedResponse(_ response: CachedURLResponse, for request: URLRequest) {
369+
let archivedData = NSKeyedArchiver.archivedData(withRootObject: response)
370+
do {
371+
let cacheFileURL = urlForFileIdentifier(request.cacheFileIdentifier)
372+
try archivedData.write(to: cacheFileURL)
373+
} catch {
374+
fatalError("Unable to save cache data: \(error.localizedDescription)")
384375
}
385-
386-
return true
387376
}
388-
389-
}
390377

391-
fileprivate struct _CacheSQLiteClient {
392-
393-
private var database: OpaquePointer?
394-
395-
init?(databasePath: String) {
396-
if sqlite3_open_v2(databasePath, &database, SQLITE_OPEN_CREATE | SQLITE_OPEN_READWRITE, nil) != SQLITE_OK {
378+
func cachedResponse(for request: URLRequest) -> CachedURLResponse? {
379+
let url = urlForFileIdentifier(request.cacheFileIdentifier)
380+
guard let data = try? Data(contentsOf: url),
381+
let response = NSKeyedUnarchiver.unarchiveObject(with: data) as? CachedURLResponse else {
397382
return nil
398383
}
384+
385+
return response
399386
}
400-
401-
func execute(sql: String) -> Bool {
402-
guard let db = database else { return false }
403-
404-
return sqlite3_exec(db, sql, nil, nil, nil) == SQLITE_OK
387+
388+
func deleteAllCaches() {
389+
do {
390+
try FileManager.default.removeItem(atPath: path)
391+
} catch {
392+
fatalError("Unable to flush database for URLCache: \(error.localizedDescription)")
393+
}
405394
}
406-
407-
mutating func close() {
408-
guard let db = database else { return }
409-
410-
sqlite3_close_v2(db)
411-
database = nil
395+
396+
private func urlForFileIdentifier(_ identifier: String) -> URL {
397+
return URL(fileURLWithPath: path + "/" + identifier)
412398
}
413-
399+
400+
}
401+
402+
fileprivate extension URLRequest {
403+
404+
var cacheFileIdentifier: String {
405+
guard let urlString = url?.absoluteString else {
406+
fatalError("Unable to create cache identifier for request: \(self)")
407+
}
408+
409+
let method = httpMethod ?? "GET"
410+
return urlString + method
411+
}
412+
414413
}

0 commit comments

Comments
 (0)