Skip to content

Commit 8875d8f

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

File tree

2 files changed

+108
-198
lines changed

2 files changed

+108
-198
lines changed

Foundation/URLCache.swift

Lines changed: 90 additions & 129 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

@@ -272,7 +273,9 @@ open class URLCache : NSObject {
272273
usage of the in-memory cache.
273274
@result the current usage of the in-memory cache of the receiver.
274275
*/
275-
open var currentMemoryUsage: Int { NSUnimplemented() }
276+
open var currentMemoryUsage: Int {
277+
return self.syncQ.sync { self.cacheSizeInMemory }
278+
}
276279

277280
/*!
278281
@method currentDiskUsage
@@ -282,19 +285,10 @@ open class URLCache : NSObject {
282285
usage of the on-disk cache.
283286
@result the current usage of the on-disk cache of the receiver.
284287
*/
285-
open var currentDiskUsage: Int { NSUnimplemented() }
286-
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-
}
288+
open var currentDiskUsage: Int {
289+
return self.syncQ.sync { self.persistence?.cacheSizeInDesk ?? 0 }
296290
}
297-
291+
298292
}
299293

300294
extension URLCache {
@@ -303,112 +297,79 @@ extension URLCache {
303297
public func removeCachedResponse(for dataTask: URLSessionDataTask) { NSUnimplemented() }
304298
}
305299

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)")
300+
fileprivate struct CachePersistence {
301+
302+
let path: String
303+
304+
var cacheSizeInDesk: Int {
305+
do {
306+
let subFiles = try FileManager.default.subpathsOfDirectory(atPath: path)
307+
var total: Int = 0
308+
for fileName in subFiles {
309+
let attributes = try FileManager.default.attributesOfItem(atPath: path.appending(fileName))
310+
total += (attributes[.size] as? Int) ?? 0
316311
}
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")
312+
return total
313+
} catch {
314+
return 0
344315
}
345316
}
346-
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
317+
318+
func setupCacheDirectory() {
319+
do {
320+
if FileManager.default.fileExists(atPath: path) {
321+
try FileManager.default.removeItem(atPath: path)
362322
}
323+
324+
try FileManager.default.createDirectory(atPath: path, withIntermediateDirectories: true)
325+
} catch {
326+
fatalError("Unable to create directories for URLCache: \(error.localizedDescription)")
363327
}
364-
365-
return true
366328
}
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-
}
329+
330+
func saveCachedResponse(_ response: CachedURLResponse, for request: URLRequest) {
331+
let archivedData = NSKeyedArchiver.archivedData(withRootObject: response)
332+
do {
333+
let cacheFileURL = urlForFileIdentifier(request.cacheFileIdentifier)
334+
try archivedData.write(to: cacheFileURL)
335+
} catch {
336+
fatalError("Unable to save cache data: \(error.localizedDescription)")
384337
}
385-
386-
return true
387338
}
388-
389-
}
390339

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 {
340+
func cachedResponse(for request: URLRequest) -> CachedURLResponse? {
341+
let url = urlForFileIdentifier(request.cacheFileIdentifier)
342+
guard let data = try? Data(contentsOf: url),
343+
let response = NSKeyedUnarchiver.unarchiveObject(with: data) as? CachedURLResponse else {
397344
return nil
398345
}
346+
347+
return response
399348
}
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
349+
350+
func deleteAllCaches() {
351+
do {
352+
try FileManager.default.removeItem(atPath: path)
353+
} catch {
354+
fatalError("Unable to flush database for URLCache: \(error.localizedDescription)")
355+
}
405356
}
406-
407-
mutating func close() {
408-
guard let db = database else { return }
409-
410-
sqlite3_close_v2(db)
411-
database = nil
357+
358+
private func urlForFileIdentifier(_ identifier: String) -> URL {
359+
return URL(fileURLWithPath: path + "/" + identifier)
412360
}
413-
361+
362+
}
363+
364+
fileprivate extension URLRequest {
365+
366+
var cacheFileIdentifier: String {
367+
guard let urlString = url?.absoluteString else {
368+
fatalError("Unable to create cache identifier for request: \(self)")
369+
}
370+
371+
let method = httpMethod ?? "GET"
372+
return urlString + method
373+
}
374+
414375
}

TestFoundation/TestURLCache.swift

Lines changed: 18 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -1,92 +1,41 @@
1+
// This source file is part of the Swift.org open source project
12
//
2-
// TestURLCache.swift
3-
// TestFoundation
3+
// Copyright (c) 2014 - 2019 Apple Inc. and the Swift project authors
4+
// Licensed under Apache License v2.0 with Runtime Library Exception
45
//
5-
// Created by Karthikkeyan Bala Sundaram on 3/8/19.
6-
// Copyright © 2019 Apple. All rights reserved.
6+
// See https://swift.org/LICENSE.txt for license information
7+
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
78
//
89

9-
import SQLite3
10-
1110
class TestURLCache: XCTestCase {
1211

1312
static var allTests: [(String, (TestURLCache) -> () throws -> Void)] {
1413
return [
1514
("test_cacheFileAndDirectorySetup", test_cacheFileAndDirectorySetup),
16-
("test_cacheDatabaseTables", test_cacheDatabaseTables),
17-
("test_cacheDatabaseIndices", test_cacheDatabaseIndices),
1815
]
1916
}
20-
17+
2118
private var cacheDirectoryPath: String {
2219
if let path = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first?.path {
2320
return "\(path)/org.swift.TestFoundation"
2421
} else {
2522
return "\(NSHomeDirectory())/Library/Caches/org.swift.TestFoundation"
2623
}
2724
}
28-
29-
private var cacheDatabasePath: String {
30-
return "\(cacheDirectoryPath)/Cache.db"
31-
}
32-
25+
3326
func test_cacheFileAndDirectorySetup() {
27+
// Test default directory
3428
let _ = URLCache.shared
35-
3629
XCTAssertTrue(FileManager.default.fileExists(atPath: cacheDirectoryPath))
37-
XCTAssertTrue(FileManager.default.fileExists(atPath: cacheDatabasePath))
38-
}
39-
40-
func test_cacheDatabaseTables() {
41-
let _ = URLCache.shared
42-
43-
var db: OpaquePointer? = nil
44-
let openDBResult = sqlite3_open_v2(cacheDatabasePath, &db, SQLITE_OPEN_CREATE | SQLITE_OPEN_READWRITE, nil)
45-
XCTAssertTrue(openDBResult == SQLITE_OK, "Unable to open database")
46-
47-
var statement: OpaquePointer? = nil
48-
let prepareResult = sqlite3_prepare_v2(db!, "select tbl_name from sqlite_master where type='table'", -1, &statement, nil)
49-
XCTAssertTrue(prepareResult == SQLITE_OK, "Unable to prepare list tables statement")
50-
51-
var tables = ["cfurl_cache_response": false, "cfurl_cache_receiver_data": false, "cfurl_cache_blob_data": false, "cfurl_cache_schema_version": false]
52-
while sqlite3_step(statement!) == SQLITE_ROW {
53-
let tableName = String(cString: sqlite3_column_text(statement!, 0))
54-
tables[tableName] = true
55-
}
56-
57-
let tablesNotExist = tables.filter({ !$0.value })
58-
if tablesNotExist.count == tables.count {
59-
XCTFail("No tables created for URLCache")
60-
}
61-
62-
XCTAssertTrue(tablesNotExist.count == 0, "Table(s) not created: \(tablesNotExist.map({ $0.key }).joined(separator: ", "))")
63-
sqlite3_close_v2(db!)
64-
}
65-
66-
func test_cacheDatabaseIndices() {
67-
let _ = URLCache.shared
68-
69-
var db: OpaquePointer? = nil
70-
let openDBResult = sqlite3_open_v2(cacheDatabasePath, &db, SQLITE_OPEN_CREATE | SQLITE_OPEN_READWRITE, nil)
71-
XCTAssertTrue(openDBResult == SQLITE_OK, "Unable to open database")
72-
73-
var statement: OpaquePointer? = nil
74-
let prepareResult = sqlite3_prepare_v2(db!, "select name from sqlite_master where type='index'", -1, &statement, nil)
75-
XCTAssertTrue(prepareResult == SQLITE_OK, "Unable to prepare list tables statement")
76-
77-
var indices = ["proto_props_index": false, "receiver_data_index": false, "request_key_index": false, "time_stamp_index": false]
78-
while sqlite3_step(statement!) == SQLITE_ROW {
79-
let name = String(cString: sqlite3_column_text(statement!, 0))
80-
indices[name] = true
81-
}
82-
83-
let indicesNotExist = indices.filter({ !$0.value })
84-
if indicesNotExist.count == indices.count {
85-
XCTFail("No index created for URLCache")
86-
}
87-
88-
XCTAssertTrue(indicesNotExist.count == 0, "Indices not created: \(indicesNotExist.map({ $0.key }).joined(separator: ", "))")
89-
sqlite3_close_v2(db!)
30+
31+
let fourMegaByte = 4 * 1024 * 1024
32+
let twentyMegaByte = 20 * 1024 * 1024
33+
34+
// Test with a custom directory
35+
let newPath = cacheDirectoryPath + ".test_cacheFileAndDirectorySetup/"
36+
URLCache.shared = URLCache(memoryCapacity: fourMegaByte, diskCapacity: twentyMegaByte, diskPath: newPath)
37+
XCTAssertTrue(FileManager.default.fileExists(atPath: newPath))
38+
XCTAssertFalse(FileManager.default.fileExists(atPath: cacheDirectoryPath))
9039
}
91-
40+
9241
}

0 commit comments

Comments
 (0)