Skip to content

[SR-10374] URLCache: init method and first time sqlite database setup implemented #2000

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

4 changes: 4 additions & 0 deletions Foundation.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
@@ -74,6 +74,7 @@
15FF00CC22934AD7004AD205 /* libCFURLSessionInterface.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 15FF00CA229348F2004AD205 /* libCFURLSessionInterface.a */; };
15FF00CE22934B78004AD205 /* module.map in Headers */ = {isa = PBXBuildFile; fileRef = 15FF00CD22934B49004AD205 /* module.map */; settings = {ATTRIBUTES = (Public, ); }; };
231503DB1D8AEE5D0061694D /* TestDecimal.swift in Sources */ = {isa = PBXBuildFile; fileRef = 231503DA1D8AEE5D0061694D /* TestDecimal.swift */; };
25EB1806223334D30053EE59 /* TestURLCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 25EB1805223334D30053EE59 /* TestURLCache.swift */; };
294E3C1D1CC5E19300E4F44C /* TestNSAttributedString.swift in Sources */ = {isa = PBXBuildFile; fileRef = 294E3C1C1CC5E19300E4F44C /* TestNSAttributedString.swift */; };
2EBE67A51C77BF0E006583D5 /* TestDateFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2EBE67A31C77BF05006583D5 /* TestDateFormatter.swift */; };
3E55A2331F52463B00082000 /* TestUnit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3E55A2321F52463B00082000 /* TestUnit.swift */; };
@@ -646,6 +647,7 @@
15FF00CD22934B49004AD205 /* module.map */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.module-map"; name = module.map; path = CoreFoundation/URL.subproj/module.map; sourceTree = SOURCE_ROOT; };
22B9C1E01C165D7A00DECFF9 /* TestDate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestDate.swift; sourceTree = "<group>"; };
231503DA1D8AEE5D0061694D /* TestDecimal.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestDecimal.swift; sourceTree = "<group>"; };
25EB1805223334D30053EE59 /* TestURLCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestURLCache.swift; sourceTree = "<group>"; };
294E3C1C1CC5E19300E4F44C /* TestNSAttributedString.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestNSAttributedString.swift; sourceTree = "<group>"; };
2EBE67A31C77BF05006583D5 /* TestDateFormatter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestDateFormatter.swift; sourceTree = "<group>"; };
3E55A2321F52463B00082000 /* TestUnit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestUnit.swift; sourceTree = "<group>"; };
@@ -1777,6 +1779,7 @@
03B6F5831F15F339004F25AF /* TestURLProtocol.swift */,
3E55A2321F52463B00082000 /* TestUnit.swift */,
7D8BD738225ED1480057CF37 /* TestMeasurement.swift */,
25EB1805223334D30053EE59 /* TestURLCache.swift */,
);
name = Tests;
sourceTree = "<group>";
@@ -2834,6 +2837,7 @@
5B13B33E1C582D4C00651CE2 /* TestProcessInfo.swift in Sources */,
5B13B33F1C582D4C00651CE2 /* TestPropertyListSerialization.swift in Sources */,
5B13B32C1C582D4C00651CE2 /* TestDate.swift in Sources */,
25EB1806223334D30053EE59 /* TestURLCache.swift in Sources */,
C7DE1FCC21EEE67200174F35 /* TestUUID.swift in Sources */,
231503DB1D8AEE5D0061694D /* TestDecimal.swift in Sources */,
7900433C1CACD33E00ECCBF1 /* TestNSPredicate.swift in Sources */,
143 changes: 138 additions & 5 deletions Foundation/URLCache.swift
Original file line number Diff line number Diff line change
@@ -128,6 +128,12 @@ open class CachedURLResponse : NSObject, NSSecureCoding, NSCopying {

open class URLCache : NSObject {

private static let sharedSyncQ = DispatchQueue(label: "org.swift.URLCache.sharedSyncQ")
private static var sharedCache: URLCache?

private let syncQ = DispatchQueue(label: "org.swift.URLCache.syncQ")
private var persistence: _CachePersistence?

/*!
@method sharedURLCache
@abstract Returns the shared URLCache instance.
@@ -147,13 +153,37 @@ open class URLCache : NSObject {
*/
open class var shared: URLCache {
get {
NSUnimplemented()
return sharedSyncQ.sync {
if let cache = sharedCache {
return cache
} else {
guard var cacheDirectoryUrl = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first else {
fatalError("Unable to find cache directory")
}

let fourMegaByte = 4 * 1024 * 1024
let twentyMegaByte = 20 * 1024 * 1024
cacheDirectoryUrl.appendPathComponent(ProcessInfo.processInfo.processName)
let cache = URLCache(memoryCapacity: fourMegaByte, diskCapacity: twentyMegaByte, diskPath: cacheDirectoryUrl.path)
sharedCache = cache
cache.persistence?.setupCacheDirectory()
return cache
}
}
}
set {
NSUnimplemented()
guard newValue !== sharedCache else { return }

sharedSyncQ.sync {
sharedCache = newValue
newValue.persistence?.setupCacheDirectory()
}
}
}

private var allCaches: [String: CachedURLResponse] = [:]
private var cacheSizeInMemory: Int = 0

/*!
@method initWithMemoryCapacity:diskCapacity:diskPath:
@abstract Initializes an URLCache with the given capacity and
@@ -167,7 +197,17 @@ open class URLCache : NSObject {
@result an initialized URLCache, with the given capacity, backed
by disk.
*/
public init(memoryCapacity: Int, diskCapacity: Int, diskPath path: String?) { NSUnimplemented() }
public init(memoryCapacity: Int, diskCapacity: Int, diskPath path: String?) {
self.memoryCapacity = memoryCapacity
self.diskCapacity = diskCapacity

// As per the function of URLCache, `diskCapacity` of `0` is assumed as no-persistence.
if diskCapacity > 0, let _path = path {
self.persistence = _CachePersistence(path: _path)
}

super.init()
}

/*!
@method cachedResponseForRequest:
@@ -238,7 +278,9 @@ open class URLCache : NSObject {
usage of the in-memory cache.
@result the current usage of the in-memory cache of the receiver.
*/
open var currentMemoryUsage: Int { NSUnimplemented() }
open var currentMemoryUsage: Int {
return self.syncQ.sync { self.cacheSizeInMemory }
}

/*!
@method currentDiskUsage
@@ -248,11 +290,102 @@ open class URLCache : NSObject {
usage of the on-disk cache.
@result the current usage of the on-disk cache of the receiver.
*/
open var currentDiskUsage: Int { NSUnimplemented() }
open var currentDiskUsage: Int {
return self.syncQ.sync { self.persistence?.cacheSizeInDisk ?? 0 }
}

}

extension URLCache {
public func storeCachedResponse(_ cachedResponse: CachedURLResponse, for dataTask: URLSessionDataTask) { NSUnimplemented() }
public func getCachedResponse(for dataTask: URLSessionDataTask, completionHandler: (CachedURLResponse?) -> Void) { NSUnimplemented() }
public func removeCachedResponse(for dataTask: URLSessionDataTask) { NSUnimplemented() }
}

fileprivate struct _CachePersistence {

let path: String

// FIXME: Create a stored property
// Update this value as the cache added and evicted
var cacheSizeInDisk: Int {
do {
let subFiles = try FileManager.default.subpathsOfDirectory(atPath: path)
var total: Int = 0
for fileName in subFiles {
let attributes = try FileManager.default.attributesOfItem(atPath: path.appending(fileName))
total += (attributes[.size] as? Int) ?? 0
}
return total
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there plan to keep this value updated as cached responses are added and evicted? Feels like with a big cache and a slow disk, each of these calls might take a while.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let me add a FIXME comment, I will create a separate PR for optimizing this. My main focus in this PR is to create an initial setup and kick start the implementation. Hope that's ok.

} catch {
return 0
}
}

func setupCacheDirectory() {
try? FileManager.default.createDirectory(atPath: path, withIntermediateDirectories: true)
}

func saveCachedResponse(_ response: CachedURLResponse, for request: URLRequest) {
do {
if let archivedData = try? NSKeyedArchiver.archivedData(withRootObject: response, requiringSecureCoding: true),
let fileIdentifier = request.cacheFileIdentifier {
let cacheFileURL = urlForFileIdentifier(fileIdentifier)
try archivedData.write(to: cacheFileURL)
}
} catch {
fatalError("Unable to save cache data: \(error.localizedDescription)")
}
}

func cachedResponse(for request: URLRequest) -> CachedURLResponse? {
guard let fileIdentifier = request.cacheFileIdentifier else {
return nil
}

let url = urlForFileIdentifier(fileIdentifier)
guard let data = try? Data(contentsOf: url),
let response = try? NSKeyedUnarchiver.unarchivedObject(ofClasses:[CachedURLResponse.self], from: data) as? CachedURLResponse else {
return nil
}

return response
}

private func urlForFileIdentifier(_ identifier: String) -> URL {
return URL(fileURLWithPath: path).appendingPathComponent(identifier)
}

}

extension URLRequest {

fileprivate var cacheFileIdentifier: String? {
guard let scheme = self.url?.scheme, scheme == "http" || scheme == "https",
let method = httpMethod, !method.isEmpty,
let urlString = url?.absoluteString else {
return nil
}

var hashString = "\(abs(method.hashValue))-\(abs(urlString.hashValue))"

if let userAgent = self.allHTTPHeaderFields?["User-Agent"], !userAgent.isEmpty {
hashString.append("\(abs(userAgent.hashValue))")
}

if let acceptLanguage = self.allHTTPHeaderFields?["Accept-Language"], !acceptLanguage.isEmpty {
hashString.append("-\(abs(acceptLanguage.hashValue))")
}

if let range = self.allHTTPHeaderFields?["Range"], !range.isEmpty {
hashString.append("-\(abs(range.hashValue))")
}

if let data = self.httpBody, !data.isEmpty {
hashString.append("-\(abs(data.hashValue))")
}

return hashString
}

}
42 changes: 42 additions & 0 deletions TestFoundation/TestURLCache.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2014 - 2019 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See https://swift.org/LICENSE.txt for license information
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
//

class TestURLCache: XCTestCase {

static var allTests: [(String, (TestURLCache) -> () throws -> Void)] {
return [
("test_cacheFileAndDirectorySetup", test_cacheFileAndDirectorySetup),
]
}

private var cacheDirectoryPath: String {
guard var cacheDirectoryUrl = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first else {
fatalError("Unable to find cache directory")
}

cacheDirectoryUrl.appendPathComponent(ProcessInfo.processInfo.processName)
return cacheDirectoryUrl.path
}

func test_cacheFileAndDirectorySetup() {
// Test default directory
let _ = URLCache.shared
XCTAssertTrue(FileManager.default.fileExists(atPath: cacheDirectoryPath))

let fourMegaByte = 4 * 1024 * 1024
let twentyMegaByte = 20 * 1024 * 1024

// Test with a custom directory
let newPath = cacheDirectoryPath + ".test_cacheFileAndDirectorySetup/"
URLCache.shared = URLCache(memoryCapacity: fourMegaByte, diskCapacity: twentyMegaByte, diskPath: newPath)
XCTAssertTrue(FileManager.default.fileExists(atPath: newPath))
XCTAssertTrue(FileManager.default.fileExists(atPath: cacheDirectoryPath))
}

}
1 change: 1 addition & 0 deletions TestFoundation/main.swift
Original file line number Diff line number Diff line change
@@ -81,6 +81,7 @@ var allTestCases = [
testCase(TestTimer.allTests),
testCase(TestTimeZone.allTests),
testCase(TestURL.allTests),
testCase(TestURLCache.allTests),
testCase(TestURLComponents.allTests),
testCase(TestURLCredential.allTests),
testCase(TestURLProtectionSpace.allTests),