Skip to content

Commit 9698315

Browse files
committed
URLCache: init method and first time sqlite database setup implemented
* init method implemented * URLCache.shared singleton object created with 4MB of memory space and 20MB of disk space * Directory and database file Cache.db file created under local directory * Sqlite Tables and Indices created in database * Unit tests added for URLCache to verify directory, file, tables and indices
1 parent 329d35b commit 9698315

File tree

4 files changed

+260
-3
lines changed

4 files changed

+260
-3
lines changed

Foundation.xcodeproj/project.pbxproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@
7474
15FF00CC22934AD7004AD205 /* libCFURLSessionInterface.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 15FF00CA229348F2004AD205 /* libCFURLSessionInterface.a */; };
7575
15FF00CE22934B78004AD205 /* module.map in Headers */ = {isa = PBXBuildFile; fileRef = 15FF00CD22934B49004AD205 /* module.map */; settings = {ATTRIBUTES = (Public, ); }; };
7676
231503DB1D8AEE5D0061694D /* TestDecimal.swift in Sources */ = {isa = PBXBuildFile; fileRef = 231503DA1D8AEE5D0061694D /* TestDecimal.swift */; };
77+
25EB1806223334D30053EE59 /* TestURLCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 25EB1805223334D30053EE59 /* TestURLCache.swift */; };
7778
294E3C1D1CC5E19300E4F44C /* TestNSAttributedString.swift in Sources */ = {isa = PBXBuildFile; fileRef = 294E3C1C1CC5E19300E4F44C /* TestNSAttributedString.swift */; };
7879
2EBE67A51C77BF0E006583D5 /* TestDateFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2EBE67A31C77BF05006583D5 /* TestDateFormatter.swift */; };
7980
3E55A2331F52463B00082000 /* TestUnit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3E55A2321F52463B00082000 /* TestUnit.swift */; };
@@ -646,6 +647,7 @@
646647
15FF00CD22934B49004AD205 /* module.map */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.module-map"; name = module.map; path = CoreFoundation/URL.subproj/module.map; sourceTree = SOURCE_ROOT; };
647648
22B9C1E01C165D7A00DECFF9 /* TestDate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestDate.swift; sourceTree = "<group>"; };
648649
231503DA1D8AEE5D0061694D /* TestDecimal.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestDecimal.swift; sourceTree = "<group>"; };
650+
25EB1805223334D30053EE59 /* TestURLCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestURLCache.swift; sourceTree = "<group>"; };
649651
294E3C1C1CC5E19300E4F44C /* TestNSAttributedString.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestNSAttributedString.swift; sourceTree = "<group>"; };
650652
2EBE67A31C77BF05006583D5 /* TestDateFormatter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestDateFormatter.swift; sourceTree = "<group>"; };
651653
3E55A2321F52463B00082000 /* TestUnit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestUnit.swift; sourceTree = "<group>"; };
@@ -1777,6 +1779,7 @@
17771779
03B6F5831F15F339004F25AF /* TestURLProtocol.swift */,
17781780
3E55A2321F52463B00082000 /* TestUnit.swift */,
17791781
7D8BD738225ED1480057CF37 /* TestMeasurement.swift */,
1782+
25EB1805223334D30053EE59 /* TestURLCache.swift */,
17801783
);
17811784
name = Tests;
17821785
sourceTree = "<group>";
@@ -2834,6 +2837,7 @@
28342837
5B13B33E1C582D4C00651CE2 /* TestProcessInfo.swift in Sources */,
28352838
5B13B33F1C582D4C00651CE2 /* TestPropertyListSerialization.swift in Sources */,
28362839
5B13B32C1C582D4C00651CE2 /* TestDate.swift in Sources */,
2840+
25EB1806223334D30053EE59 /* TestURLCache.swift in Sources */,
28372841
C7DE1FCC21EEE67200174F35 /* TestUUID.swift in Sources */,
28382842
231503DB1D8AEE5D0061694D /* TestDecimal.swift in Sources */,
28392843
7900433C1CACD33E00ECCBF1 /* TestNSPredicate.swift in Sources */,

Foundation/URLCache.swift

Lines changed: 164 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import SwiftFoundation
1212
#else
1313
import Foundation
1414
#endif
15+
import SQLite3
1516

1617
/*!
1718
@enum URLCache.StoragePolicy
@@ -128,6 +129,26 @@ open class CachedURLResponse : NSObject, NSSecureCoding, NSCopying {
128129

129130
open class URLCache : NSObject {
130131

132+
private static let sharedSyncQ = DispatchQueue(label: "org.swift.URLCache.sharedSyncQ")
133+
134+
private static var sharedCache: URLCache? {
135+
willSet {
136+
URLCache.sharedCache?.syncQ.sync {
137+
URLCache.sharedCache?._databaseClient?.close()
138+
URLCache.sharedCache?.flushDatabase()
139+
}
140+
}
141+
didSet {
142+
URLCache.sharedCache?.syncQ.sync {
143+
URLCache.sharedCache?.setupCacheDatabaseIfNotExist()
144+
}
145+
}
146+
}
147+
148+
private let syncQ = DispatchQueue(label: "org.swift.URLCache.syncQ")
149+
private let _baseDiskPath: String?
150+
private var _databaseClient: _CacheSQLiteClient?
151+
131152
/*!
132153
@method sharedURLCache
133154
@abstract Returns the shared URLCache instance.
@@ -147,10 +168,22 @@ open class URLCache : NSObject {
147168
*/
148169
open class var shared: URLCache {
149170
get {
150-
NSUnimplemented()
171+
return sharedSyncQ.sync {
172+
if let cache = sharedCache {
173+
return cache
174+
} else {
175+
let fourMegaByte = 4 * 1024 * 1024
176+
let twentyMegaByte = 20 * 1024 * 1024
177+
let cacheDirectoryPath = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first?.path ?? "\(NSHomeDirectory())/Library/Caches/"
178+
let path = "\(cacheDirectoryPath)\(Bundle.main.bundleIdentifier ?? UUID().uuidString)"
179+
let cache = URLCache(memoryCapacity: fourMegaByte, diskCapacity: twentyMegaByte, diskPath: path)
180+
sharedCache = cache
181+
return cache
182+
}
183+
}
151184
}
152185
set {
153-
NSUnimplemented()
186+
sharedSyncQ.sync { sharedCache = newValue }
154187
}
155188
}
156189

@@ -167,7 +200,13 @@ open class URLCache : NSObject {
167200
@result an initialized URLCache, with the given capacity, backed
168201
by disk.
169202
*/
170-
public init(memoryCapacity: Int, diskCapacity: Int, diskPath path: String?) { NSUnimplemented() }
203+
public init(memoryCapacity: Int, diskCapacity: Int, diskPath path: String?) {
204+
self.memoryCapacity = memoryCapacity
205+
self.diskCapacity = diskCapacity
206+
self._baseDiskPath = path
207+
208+
super.init()
209+
}
171210

172211
/*!
173212
@method cachedResponseForRequest:
@@ -249,10 +288,132 @@ open class URLCache : NSObject {
249288
@result the current usage of the on-disk cache of the receiver.
250289
*/
251290
open var currentDiskUsage: Int { NSUnimplemented() }
291+
292+
private func flushDatabase() {
293+
guard let path = _baseDiskPath else { return }
294+
295+
do {
296+
let dbPath = path.appending("/Cache.db")
297+
try FileManager.default.removeItem(atPath: dbPath)
298+
} catch {
299+
fatalError("Unable to flush database for URLCache: \(error.localizedDescription)")
300+
}
301+
}
302+
252303
}
253304

254305
extension URLCache {
255306
public func storeCachedResponse(_ cachedResponse: CachedURLResponse, for dataTask: URLSessionDataTask) { NSUnimplemented() }
256307
public func getCachedResponse(for dataTask: URLSessionDataTask, completionHandler: (CachedURLResponse?) -> Void) { NSUnimplemented() }
257308
public func removeCachedResponse(for dataTask: URLSessionDataTask) { NSUnimplemented() }
258309
}
310+
311+
extension URLCache {
312+
313+
private func setupCacheDatabaseIfNotExist() {
314+
guard let path = _baseDiskPath else { return }
315+
316+
if !FileManager.default.fileExists(atPath: path) {
317+
do {
318+
try FileManager.default.createDirectory(atPath: path, withIntermediateDirectories: true)
319+
} catch {
320+
fatalError("Unable to create directories for URLCache: \(error.localizedDescription)")
321+
}
322+
}
323+
324+
// Close the currently opened database connection(if any), before creating/replacing the db file
325+
_databaseClient?.close()
326+
327+
let dbPath = path.appending("/Cache.db")
328+
if !FileManager.default.createFile(atPath: dbPath, contents: nil, attributes: nil) {
329+
fatalError("Unable to setup database for URLCache")
330+
}
331+
332+
_databaseClient = _CacheSQLiteClient(databasePath: dbPath)
333+
if _databaseClient == nil {
334+
_databaseClient?.close()
335+
flushDatabase()
336+
fatalError("Unable to setup database for URLCache")
337+
}
338+
339+
if !createTables() {
340+
_databaseClient?.close()
341+
flushDatabase()
342+
fatalError("Unable to setup database for URLCache: Tables not created")
343+
}
344+
345+
if !createIndicesForTables() {
346+
_databaseClient?.close()
347+
flushDatabase()
348+
fatalError("Unable to setup database for URLCache: Indices not created for tables")
349+
}
350+
}
351+
352+
private func createTables() -> Bool {
353+
guard _databaseClient != nil else {
354+
fatalError("Cannot create table before database setup")
355+
}
356+
357+
let tableSQLs = [
358+
"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)",
359+
"CREATE TABLE cfurl_cache_receiver_data(entry_ID INTEGER PRIMARY KEY, isDataOnFS INTEGER, receiver_data BLOB)",
360+
"CREATE TABLE cfurl_cache_blob_data(entry_ID INTEGER PRIMARY KEY, response_object BLOB, request_object BLOB, proto_props BLOB, user_info BLOB)",
361+
"CREATE TABLE cfurl_cache_schema_version(schema_version INTEGER)"
362+
]
363+
364+
for sql in tableSQLs {
365+
if let isSuccess = _databaseClient?.execute(sql: sql), !isSuccess {
366+
return false
367+
}
368+
}
369+
370+
return true
371+
}
372+
373+
private func createIndicesForTables() -> Bool {
374+
guard _databaseClient != nil else {
375+
fatalError("Cannot create table before database setup")
376+
}
377+
378+
let indicesSQLs = [
379+
"CREATE INDEX proto_props_index ON cfurl_cache_blob_data(entry_ID)",
380+
"CREATE INDEX receiver_data_index ON cfurl_cache_receiver_data(entry_ID)",
381+
"CREATE INDEX request_key_index ON cfurl_cache_response(request_key)",
382+
"CREATE INDEX time_stamp_index ON cfurl_cache_response(time_stamp)"
383+
]
384+
385+
for sql in indicesSQLs {
386+
if let isSuccess = _databaseClient?.execute(sql: sql), !isSuccess {
387+
return false
388+
}
389+
}
390+
391+
return true
392+
}
393+
394+
}
395+
396+
fileprivate struct _CacheSQLiteClient {
397+
398+
private var database: OpaquePointer?
399+
400+
init?(databasePath: String) {
401+
if sqlite3_open_v2(databasePath, &database, SQLITE_OPEN_CREATE | SQLITE_OPEN_READWRITE, nil) != SQLITE_OK {
402+
return nil
403+
}
404+
}
405+
406+
func execute(sql: String) -> Bool {
407+
guard let db = database else { return false }
408+
409+
return sqlite3_exec(db, sql, nil, nil, nil) == SQLITE_OK
410+
}
411+
412+
mutating func close() {
413+
guard let db = database else { return }
414+
415+
sqlite3_close_v2(db)
416+
database = nil
417+
}
418+
419+
}

TestFoundation/TestURLCache.swift

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
//
2+
// TestURLCache.swift
3+
// TestFoundation
4+
//
5+
// Created by Karthikkeyan Bala Sundaram on 3/8/19.
6+
// Copyright © 2019 Apple. All rights reserved.
7+
//
8+
9+
import SQLite3
10+
11+
class TestURLCache: XCTestCase {
12+
13+
static var allTests: [(String, (TestURLCache) -> () throws -> Void)] {
14+
return [
15+
("test_cacheFileAndDirectorySetup", test_cacheFileAndDirectorySetup),
16+
("test_cacheDatabaseTables", test_cacheDatabaseTables),
17+
("test_cacheDatabaseIndices", test_cacheDatabaseIndices),
18+
]
19+
}
20+
21+
private var cacheDirectoryPath: String {
22+
if let path = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first?.path {
23+
return "\(path)/org.swift.TestFoundation"
24+
} else {
25+
return "\(NSHomeDirectory())/Library/Caches/org.swift.TestFoundation"
26+
}
27+
}
28+
29+
private var cacheDatabasePath: String {
30+
return "\(cacheDirectoryPath)/Cache.db"
31+
}
32+
33+
func test_cacheFileAndDirectorySetup() {
34+
let _ = URLCache.shared
35+
36+
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+
}
90+
91+
}

TestFoundation/main.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ var allTestCases = [
8181
testCase(TestTimer.allTests),
8282
testCase(TestTimeZone.allTests),
8383
testCase(TestURL.allTests),
84+
testCase(TestURLCache.allTests),
8485
testCase(TestURLComponents.allTests),
8586
testCase(TestURLCredential.allTests),
8687
testCase(TestURLProtectionSpace.allTests),

0 commit comments

Comments
 (0)