@@ -12,6 +12,7 @@ import SwiftFoundation
12
12
#else
13
13
import Foundation
14
14
#endif
15
+ import SQLite3
15
16
16
17
/*!
17
18
@enum URLCache.StoragePolicy
@@ -128,6 +129,26 @@ open class CachedURLResponse : NSObject, NSSecureCoding, NSCopying {
128
129
129
130
open class URLCache : NSObject {
130
131
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
+
131
152
/*!
132
153
@method sharedURLCache
133
154
@abstract Returns the shared URLCache instance.
@@ -147,10 +168,22 @@ open class URLCache : NSObject {
147
168
*/
148
169
open class var shared : URLCache {
149
170
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
+ }
151
184
}
152
185
set {
153
- NSUnimplemented ( )
186
+ sharedSyncQ . sync { sharedCache = newValue }
154
187
}
155
188
}
156
189
@@ -167,7 +200,13 @@ open class URLCache : NSObject {
167
200
@result an initialized URLCache, with the given capacity, backed
168
201
by disk.
169
202
*/
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
+ }
171
210
172
211
/*!
173
212
@method cachedResponseForRequest:
@@ -249,10 +288,132 @@ open class URLCache : NSObject {
249
288
@result the current usage of the on-disk cache of the receiver.
250
289
*/
251
290
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
+
252
303
}
253
304
254
305
extension URLCache {
255
306
public func storeCachedResponse( _ cachedResponse: CachedURLResponse , for dataTask: URLSessionDataTask ) { NSUnimplemented ( ) }
256
307
public func getCachedResponse( for dataTask: URLSessionDataTask , completionHandler: ( CachedURLResponse ? ) -> Void ) { NSUnimplemented ( ) }
257
308
public func removeCachedResponse( for dataTask: URLSessionDataTask ) { NSUnimplemented ( ) }
258
309
}
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
+ }
0 commit comments