diff --git a/Foundation.xcodeproj/project.pbxproj b/Foundation.xcodeproj/project.pbxproj index 70d733b68e..6ec28e2c21 100644 --- a/Foundation.xcodeproj/project.pbxproj +++ b/Foundation.xcodeproj/project.pbxproj @@ -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 = ""; }; 231503DA1D8AEE5D0061694D /* TestDecimal.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestDecimal.swift; sourceTree = ""; }; + 25EB1805223334D30053EE59 /* TestURLCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestURLCache.swift; sourceTree = ""; }; 294E3C1C1CC5E19300E4F44C /* TestNSAttributedString.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestNSAttributedString.swift; sourceTree = ""; }; 2EBE67A31C77BF05006583D5 /* TestDateFormatter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestDateFormatter.swift; sourceTree = ""; }; 3E55A2321F52463B00082000 /* TestUnit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestUnit.swift; sourceTree = ""; }; @@ -1777,6 +1779,7 @@ 03B6F5831F15F339004F25AF /* TestURLProtocol.swift */, 3E55A2321F52463B00082000 /* TestUnit.swift */, 7D8BD738225ED1480057CF37 /* TestMeasurement.swift */, + 25EB1805223334D30053EE59 /* TestURLCache.swift */, ); name = Tests; sourceTree = ""; @@ -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 */, diff --git a/Foundation/URLCache.swift b/Foundation/URLCache.swift index 7afb0c2432..8b188c3a34 100644 --- a/Foundation/URLCache.swift +++ b/Foundation/URLCache.swift @@ -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,7 +290,10 @@ 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 { @@ -256,3 +301,91 @@ extension URLCache { 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 + } 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 + } + +} diff --git a/TestFoundation/TestURLCache.swift b/TestFoundation/TestURLCache.swift new file mode 100644 index 0000000000..ad36187847 --- /dev/null +++ b/TestFoundation/TestURLCache.swift @@ -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)) + } + +} diff --git a/TestFoundation/main.swift b/TestFoundation/main.swift index 1464f1cdab..089b60b656 100644 --- a/TestFoundation/main.swift +++ b/TestFoundation/main.swift @@ -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),