Skip to content

Commit f661c45

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 e12c2d8 commit f661c45

File tree

4 files changed

+261
-3
lines changed

4 files changed

+261
-3
lines changed

Foundation.xcodeproj/project.pbxproj

+5
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
159884921DCC877700E3314C /* TestHTTPCookieStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 159884911DCC877700E3314C /* TestHTTPCookieStorage.swift */; };
3535
15F10CDC218909BF00D88114 /* TestNSCalendar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 15F10CDB218909BF00D88114 /* TestNSCalendar.swift */; };
3636
231503DB1D8AEE5D0061694D /* TestDecimal.swift in Sources */ = {isa = PBXBuildFile; fileRef = 231503DA1D8AEE5D0061694D /* TestDecimal.swift */; };
37+
25EB1806223334D30053EE59 /* TestURLCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 25EB1805223334D30053EE59 /* TestURLCache.swift */; };
3738
294E3C1D1CC5E19300E4F44C /* TestNSAttributedString.swift in Sources */ = {isa = PBXBuildFile; fileRef = 294E3C1C1CC5E19300E4F44C /* TestNSAttributedString.swift */; };
3839
2EBE67A51C77BF0E006583D5 /* TestDateFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2EBE67A31C77BF05006583D5 /* TestDateFormatter.swift */; };
3940
3E55A2331F52463B00082000 /* TestUnit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3E55A2321F52463B00082000 /* TestUnit.swift */; };
@@ -574,6 +575,7 @@
574575
15F10CDB218909BF00D88114 /* TestNSCalendar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestNSCalendar.swift; sourceTree = "<group>"; };
575576
22B9C1E01C165D7A00DECFF9 /* TestDate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestDate.swift; sourceTree = "<group>"; };
576577
231503DA1D8AEE5D0061694D /* TestDecimal.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestDecimal.swift; sourceTree = "<group>"; };
578+
25EB1805223334D30053EE59 /* TestURLCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestURLCache.swift; sourceTree = "<group>"; };
577579
294E3C1C1CC5E19300E4F44C /* TestNSAttributedString.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestNSAttributedString.swift; sourceTree = "<group>"; };
578580
2EBE67A31C77BF05006583D5 /* TestDateFormatter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestDateFormatter.swift; sourceTree = "<group>"; };
579581
3E55A2321F52463B00082000 /* TestUnit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestUnit.swift; sourceTree = "<group>"; };
@@ -1656,6 +1658,7 @@
16561658
5B6F17961C48631C00935030 /* TestUtils.swift */,
16571659
03B6F5831F15F339004F25AF /* TestURLProtocol.swift */,
16581660
3E55A2321F52463B00082000 /* TestUnit.swift */,
1661+
25EB1805223334D30053EE59 /* TestURLCache.swift */,
16591662
);
16601663
name = Tests;
16611664
sourceTree = "<group>";
@@ -2240,6 +2243,7 @@
22402243
developmentRegion = English;
22412244
hasScannedForEncodings = 0;
22422245
knownRegions = (
2246+
English,
22432247
en,
22442248
Base,
22452249
);
@@ -2622,6 +2626,7 @@
26222626
5B13B33E1C582D4C00651CE2 /* TestProcessInfo.swift in Sources */,
26232627
5B13B33F1C582D4C00651CE2 /* TestPropertyListSerialization.swift in Sources */,
26242628
5B13B32C1C582D4C00651CE2 /* TestDate.swift in Sources */,
2629+
25EB1806223334D30053EE59 /* TestURLCache.swift in Sources */,
26252630
C7DE1FCC21EEE67200174F35 /* TestUUID.swift in Sources */,
26262631
231503DB1D8AEE5D0061694D /* TestDecimal.swift in Sources */,
26272632
7900433C1CACD33E00ECCBF1 /* TestNSPredicate.swift in Sources */,

Foundation/URLCache.swift

+164-3
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
88
//
99

10+
import SQLite3
1011

1112
/*!
1213
@enum URLCache.StoragePolicy
@@ -123,6 +124,26 @@ open class CachedURLResponse : NSObject, NSSecureCoding, NSCopying {
123124

124125
open class URLCache : NSObject {
125126

127+
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+
}
142+
143+
private let syncQ = DispatchQueue(label: "org.swift.URLCache.syncQ")
144+
private let _baseDiskPath: String?
145+
private var _databaseClient: _CacheSQLiteClient?
146+
126147
/*!
127148
@method sharedURLCache
128149
@abstract Returns the shared URLCache instance.
@@ -142,10 +163,22 @@ open class URLCache : NSObject {
142163
*/
143164
open class var shared: URLCache {
144165
get {
145-
NSUnimplemented()
166+
return sharedSyncQ.sync {
167+
if let cache = sharedCache {
168+
return cache
169+
} else {
170+
let fourMegaByte = 4 * 1024 * 1024
171+
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)
175+
sharedCache = cache
176+
return cache
177+
}
178+
}
146179
}
147180
set {
148-
NSUnimplemented()
181+
sharedSyncQ.sync { sharedCache = newValue }
149182
}
150183
}
151184

@@ -162,7 +195,13 @@ open class URLCache : NSObject {
162195
@result an initialized URLCache, with the given capacity, backed
163196
by disk.
164197
*/
165-
public init(memoryCapacity: Int, diskCapacity: Int, diskPath path: String?) { NSUnimplemented() }
198+
public init(memoryCapacity: Int, diskCapacity: Int, diskPath path: String?) {
199+
self.memoryCapacity = memoryCapacity
200+
self.diskCapacity = diskCapacity
201+
self._baseDiskPath = path
202+
203+
super.init()
204+
}
166205

167206
/*!
168207
@method cachedResponseForRequest:
@@ -244,10 +283,132 @@ open class URLCache : NSObject {
244283
@result the current usage of the on-disk cache of the receiver.
245284
*/
246285
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+
}
296+
}
297+
247298
}
248299

249300
extension URLCache {
250301
public func storeCachedResponse(_ cachedResponse: CachedURLResponse, for dataTask: URLSessionDataTask) { NSUnimplemented() }
251302
public func getCachedResponse(for dataTask: URLSessionDataTask, completionHandler: (CachedURLResponse?) -> Void) { NSUnimplemented() }
252303
public func removeCachedResponse(for dataTask: URLSessionDataTask) { NSUnimplemented() }
253304
}
305+
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)")
316+
}
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")
344+
}
345+
}
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
362+
}
363+
}
364+
365+
return true
366+
}
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+
}
384+
}
385+
386+
return true
387+
}
388+
389+
}
390+
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 {
397+
return nil
398+
}
399+
}
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
405+
}
406+
407+
mutating func close() {
408+
guard let db = database else { return }
409+
410+
sqlite3_close_v2(db)
411+
database = nil
412+
}
413+
414+
}

TestFoundation/TestURLCache.swift

+91
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

+1
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ XCTMain([
7878
testCase(TestTimer.allTests),
7979
testCase(TestTimeZone.allTests),
8080
testCase(TestURL.allTests),
81+
testCase(TestURLCache.allTests),
8182
testCase(TestURLComponents.allTests),
8283
testCase(TestURLCredential.allTests),
8384
testCase(TestURLProtectionSpace.allTests),

0 commit comments

Comments
 (0)