diff --git a/Cache.podspec b/Cache.podspec index 9dfb648..5a6f4b0 100644 --- a/Cache.podspec +++ b/Cache.podspec @@ -1,7 +1,7 @@ Pod::Spec.new do |s| s.name = "Cache" s.summary = "Nothing but cache." - s.version = "6.0.0" + s.version = "7.0.1" s.homepage = "https://github.com/hyperoslo/Cache" s.license = 'MIT' s.author = { "Hyper Interaktiv AS" => "ios@hyper.no" } @@ -18,6 +18,7 @@ Pod::Spec.new do |s| s.osx.source_files = 'Source/{Mac,Shared}/**/*' s.tvos.source_files = 'Source/{iOS,Shared}/**/*' s.watchos.source_files = 'Source/{iOS,Shared}/**/*' + s.resources = 'Source/PrivacyInfo.xcprivacy' s.frameworks = 'Foundation' s.swift_version = '5.0' diff --git a/Cache.xcodeproj/project.pbxproj b/Cache.xcodeproj/project.pbxproj index 64d3441..16694be 100644 --- a/Cache.xcodeproj/project.pbxproj +++ b/Cache.xcodeproj/project.pbxproj @@ -11,6 +11,12 @@ 0E79164B250E2AA500A71666 /* Hasher+constantAccrossExecutions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E791649250E2AA500A71666 /* Hasher+constantAccrossExecutions.swift */; }; 0E79164C250E2AA500A71666 /* Hasher+constantAccrossExecutions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E791649250E2AA500A71666 /* Hasher+constantAccrossExecutions.swift */; }; 0E79164E250E2B0400A71666 /* Hasher+constantAccrossExecutions+Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E79164D250E2B0400A71666 /* Hasher+constantAccrossExecutions+Tests.swift */; }; + 2292AB7B252A14520031F3B9 /* URL+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2292AB7A252A14520031F3B9 /* URL+Extensions.swift */; }; + 2292AB7C252A14520031F3B9 /* URL+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2292AB7A252A14520031F3B9 /* URL+Extensions.swift */; }; + 2292AB7D252A14520031F3B9 /* URL+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2292AB7A252A14520031F3B9 /* URL+Extensions.swift */; }; + 2292AB85252A14F50031F3B9 /* FileManager+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2292AB84252A14F50031F3B9 /* FileManager+Extensions.swift */; }; + 2292AB86252A14F50031F3B9 /* FileManager+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2292AB84252A14F50031F3B9 /* FileManager+Extensions.swift */; }; + 2292AB87252A14F50031F3B9 /* FileManager+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2292AB84252A14F50031F3B9 /* FileManager+Extensions.swift */; }; BDEDD37D1DBCEB8A007416A6 /* Cache.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BDEDD3561DBCE5B1007416A6 /* Cache.framework */; }; D21B667C1F6A723C00125DE1 /* DataSerializer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2CF98531F694FFA00CE8F68 /* DataSerializer.swift */; }; D21B667E1F6A723C00125DE1 /* ExpirationMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2CF98551F694FFA00CE8F68 /* ExpirationMode.swift */; }; @@ -95,9 +101,6 @@ D28A1D241F6FFEF60030DF81 /* ObjectConverterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D285143E1F6FFE1F00C674D1 /* ObjectConverterTests.swift */; }; D28C9BAC1F67ECD400C180C1 /* TestHelper+iOS.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5291CDF1C28374800B702C9 /* TestHelper+iOS.swift */; }; D28C9BAF1F67EF8300C180C1 /* UIImage+ExtensionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5291DA01C28405900B702C9 /* UIImage+ExtensionsTests.swift */; }; - D292DAFD1F6A970B0060F614 /* Result.swift in Sources */ = {isa = PBXBuildFile; fileRef = D292DAFC1F6A970B0060F614 /* Result.swift */; }; - D292DAFE1F6A970B0060F614 /* Result.swift in Sources */ = {isa = PBXBuildFile; fileRef = D292DAFC1F6A970B0060F614 /* Result.swift */; }; - D292DAFF1F6A970B0060F614 /* Result.swift in Sources */ = {isa = PBXBuildFile; fileRef = D292DAFC1F6A970B0060F614 /* Result.swift */; }; D2CF981E1F69427C00CE8F68 /* TestCase+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2CF981B1F69427C00CE8F68 /* TestCase+Extensions.swift */; }; D2CF981F1F69427C00CE8F68 /* TestCase+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2CF981B1F69427C00CE8F68 /* TestCase+Extensions.swift */; }; D2CF98201F69427C00CE8F68 /* TestCase+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2CF981B1F69427C00CE8F68 /* TestCase+Extensions.swift */; }; @@ -213,6 +216,8 @@ /* Begin PBXFileReference section */ 0E791649250E2AA500A71666 /* Hasher+constantAccrossExecutions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Hasher+constantAccrossExecutions.swift"; sourceTree = ""; }; 0E79164D250E2B0400A71666 /* Hasher+constantAccrossExecutions+Tests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Hasher+constantAccrossExecutions+Tests.swift"; sourceTree = ""; }; + 2292AB7A252A14520031F3B9 /* URL+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URL+Extensions.swift"; sourceTree = ""; }; + 2292AB84252A14F50031F3B9 /* FileManager+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FileManager+Extensions.swift"; sourceTree = ""; }; BDEDD3561DBCE5B1007416A6 /* Cache.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Cache.framework; sourceTree = BUILT_PRODUCTS_DIR; }; BDEDD3781DBCEB8A007416A6 /* Cache-tvOS-Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "Cache-tvOS-Tests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; D221E5BB20D00D9300BC940E /* MemoryStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemoryStorage.swift; sourceTree = ""; }; @@ -232,7 +237,6 @@ D27014A420D129EB003B45C7 /* TransformerFactory.swift */ = {isa = PBXFileReference; indentWidth = 4; lastKnownFileType = sourcecode.swift; path = TransformerFactory.swift; sourceTree = ""; tabWidth = 4; }; D285143E1F6FFE1F00C674D1 /* ObjectConverterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ObjectConverterTests.swift; sourceTree = ""; }; D28897041F8B79B300C61DEE /* JSONDecoder+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "JSONDecoder+Extensions.swift"; sourceTree = ""; }; - D292DAFC1F6A970B0060F614 /* Result.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Result.swift; sourceTree = ""; }; D292DB001F6AA06B0060F614 /* SyncStorageTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SyncStorageTests.swift; sourceTree = ""; }; D292DB031F6AA0730060F614 /* AsyncStorageTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AsyncStorageTests.swift; sourceTree = ""; }; D2CF981B1F69427C00CE8F68 /* TestCase+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "TestCase+Extensions.swift"; sourceTree = ""; }; @@ -376,6 +380,8 @@ 0E791649250E2AA500A71666 /* Hasher+constantAccrossExecutions.swift */, D28897041F8B79B300C61DEE /* JSONDecoder+Extensions.swift */, D2CF98501F694FFA00CE8F68 /* Date+Extensions.swift */, + 2292AB7A252A14520031F3B9 /* URL+Extensions.swift */, + 2292AB84252A14F50031F3B9 /* FileManager+Extensions.swift */, ); path = Extensions; sourceTree = ""; @@ -389,7 +395,6 @@ D2CF98571F694FFA00CE8F68 /* ImageWrapper.swift */, D2CF98591F694FFA00CE8F68 /* StorageError.swift */, D2CF98861F695B8F00CE8F68 /* Types.swift */, - D292DAFC1F6A970B0060F614 /* Result.swift */, D2D4CC151FA3145000E4A2D5 /* MD5.swift */, D2D4CC1F1FA3411300E4A2D5 /* JSONDictionaryWrapper.swift */, D2D4CC231FA3426B00E4A2D5 /* JSONArrayWrapper.swift */, @@ -884,10 +889,11 @@ D2D4CC261FA3426B00E4A2D5 /* JSONArrayWrapper.swift in Sources */, D27014A720D129EB003B45C7 /* TransformerFactory.swift in Sources */, D270147E20D107DA003B45C7 /* SyncStorage.swift in Sources */, - D292DAFF1F6A970B0060F614 /* Result.swift in Sources */, + 2292AB7D252A14520031F3B9 /* URL+Extensions.swift in Sources */, D21B669A1F6A724300125DE1 /* Date+Extensions.swift in Sources */, 0E79164C250E2AA500A71666 /* Hasher+constantAccrossExecutions.swift in Sources */, D21B66891F6A723C00125DE1 /* ImageWrapper.swift in Sources */, + 2292AB87252A14F50031F3B9 /* FileManager+Extensions.swift in Sources */, D21B668B1F6A723C00125DE1 /* StorageError.swift in Sources */, D5A9D1B921134547005DBD3F /* ObservationToken.swift in Sources */, D270148A20D11040003B45C7 /* Storage+Transform.swift in Sources */, @@ -972,10 +978,11 @@ D2D4CC251FA3426B00E4A2D5 /* JSONArrayWrapper.swift in Sources */, D27014A620D129EB003B45C7 /* TransformerFactory.swift in Sources */, D270147D20D107DA003B45C7 /* SyncStorage.swift in Sources */, - D292DAFE1F6A970B0060F614 /* Result.swift in Sources */, + 2292AB7C252A14520031F3B9 /* URL+Extensions.swift in Sources */, D21B66991F6A724200125DE1 /* Date+Extensions.swift in Sources */, 0E79164B250E2AA500A71666 /* Hasher+constantAccrossExecutions.swift in Sources */, D21B66801F6A723C00125DE1 /* ImageWrapper.swift in Sources */, + 2292AB86252A14F50031F3B9 /* FileManager+Extensions.swift in Sources */, D21B66821F6A723C00125DE1 /* StorageError.swift in Sources */, D5A9D1B821134547005DBD3F /* ObservationToken.swift in Sources */, D270148920D11040003B45C7 /* Storage+Transform.swift in Sources */, @@ -1023,13 +1030,14 @@ D270148C20D113EA003B45C7 /* Optional+Extension.swift in Sources */, D2CF98601F694FFA00CE8F68 /* DiskConfig.swift in Sources */, D270147020D1018A003B45C7 /* Transformer.swift in Sources */, - D292DAFD1F6A970B0060F614 /* Result.swift in Sources */, D27014A520D129EB003B45C7 /* TransformerFactory.swift in Sources */, D2D4CC241FA3426B00E4A2D5 /* JSONArrayWrapper.swift in Sources */, + 2292AB7B252A14520031F3B9 /* URL+Extensions.swift in Sources */, D270147C20D107DA003B45C7 /* SyncStorage.swift in Sources */, D2CF98671F694FFA00CE8F68 /* Expiry.swift in Sources */, 0E79164A250E2AA500A71666 /* Hasher+constantAccrossExecutions.swift in Sources */, D270148820D11040003B45C7 /* Storage+Transform.swift in Sources */, + 2292AB85252A14F50031F3B9 /* FileManager+Extensions.swift in Sources */, D2CF986A1F694FFA00CE8F68 /* StorageError.swift in Sources */, D5A9D1B721134547005DBD3F /* ObservationToken.swift in Sources */, D5A138C11EB29BFA00881A20 /* UIImage+Extensions.swift in Sources */, @@ -1174,6 +1182,7 @@ SDKROOT = appletvos; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + TARGETED_DEVICE_FAMILY = 3; TVOS_DEPLOYMENT_TARGET = 12.0; }; name = Debug; @@ -1192,6 +1201,7 @@ PRODUCT_BUNDLE_IDENTIFIER = "no.hyper.Cache-tvOS-Tests"; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = appletvos; + TARGETED_DEVICE_FAMILY = 3; TVOS_DEPLOYMENT_TARGET = 12.0; }; name = Release; @@ -1273,7 +1283,7 @@ FRAMEWORK_SEARCH_PATHS = "$(inherited)"; INFOPLIST_FILE = "$(SRCROOT)/Tests/Mac/Info.plist"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks"; - MACOSX_DEPLOYMENT_TARGET = 10.13; + MACOSX_DEPLOYMENT_TARGET = 10.15; PRODUCT_BUNDLE_IDENTIFIER = "no.hyper.Cache-MacTests"; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = macosx; @@ -1288,7 +1298,7 @@ FRAMEWORK_SEARCH_PATHS = "$(inherited)"; INFOPLIST_FILE = "$(SRCROOT)/Tests/Mac/Info.plist"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks"; - MACOSX_DEPLOYMENT_TARGET = 10.13; + MACOSX_DEPLOYMENT_TARGET = 10.15; PRODUCT_BUNDLE_IDENTIFIER = "no.hyper.Cache-MacTests"; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = macosx; diff --git a/Cache.xcodeproj/xcshareddata/xcschemes/Cache-iOS.xcscheme b/Cache.xcodeproj/xcshareddata/xcschemes/Cache-iOS.xcscheme index 3c2c837..fc762f3 100644 --- a/Cache.xcodeproj/xcshareddata/xcschemes/Cache-iOS.xcscheme +++ b/Cache.xcodeproj/xcshareddata/xcschemes/Cache-iOS.xcscheme @@ -39,7 +39,6 @@ + transformer: TransformerFactory.forCodable(ofType: User.self) // Storage ) ``` @@ -86,13 +86,13 @@ All `Storage` now are generic by default, so you can get a type safety experienc If you want to change the type, `Cache` offers `transform` functions, look for `Transformer` and `TransformerFactory` for built-in transformers. ```swift -let storage: Storage = ... +let storage: Storage = ... storage.setObject(superman, forKey: "user") -let imageStorage = storage.transformImage() // Storage +let imageStorage = storage.transformImage() // Storage imageStorage.setObject(image, forKey: "image") -let stringStorage = storage.transformCodable(ofType: String.self) // Storage +let stringStorage = storage.transformCodable(ofType: String.self) // Storage stringStorage.setObject("hello world", forKey: "string") ``` @@ -247,42 +247,42 @@ You access Async APIs via `storage.async`, it is also thread safe, and you can u ```swift storage.async.setObject("Oslo", forKey: "my favorite city") { result in switch result { - case .value: + case .success: print("saved successfully") - case .error(let error): + case .failure(let error): print(error) } } storage.async.object(forKey: "my favorite city") { result in switch result { - case .value(let city): + case .success(let city): print("my favorite city is \(city)") - case .error(let error): + case .failure(let error): print(error) } } storage.async.existsObject(forKey: "my favorite city") { result in - if case .value(let exists) = result, exists { + if case .success(let exists) = result, exists { print("I have a favorite city") } } storage.async.removeAll() { result in switch result { - case .value: + case .success: print("removal completes") - case .error(let error): + case .failure(let error): print(error) } } storage.async.removeExpiredObjects() { result in switch result { - case .value: + case .success: print("removal completes") - case .error(let error): + case .failure(let error): print(error) } } @@ -381,7 +381,7 @@ This is how you perform object converting and saving with `Alamofire` ```swift Alamofire.request("https://gameofthrones.org/mostFavoriteCharacter").responseString { response in do { - let user = try JSONDecoder.decode(response.result.value, to: User.self) + let user = try JSONDecoder.decode(response.result.success, to: User.self) try storage.setObject(user, forKey: "most favorite character") } catch { print(error) @@ -391,17 +391,18 @@ Alamofire.request("https://gameofthrones.org/mostFavoriteCharacter").responseStr ## What about images -If you want to load image into `UIImageView` or `NSImageView`, then we also have a nice gift for you. It's called [Imaginary](https://github.com/hyperoslo/Imaginary) and uses `Cache` under the hood to make you life easier when it comes to working with remote images. +If you want to load image into `UIImageView` or `NSImageView`, then we also have a nice gift for you. It's called [Imaginary](https://github.com/hyperoslo/Imaginary) and uses `Cache` under the hood to make your life easier when it comes to working with remote images. ## Installation ### Cocoapods **Cache** is available through [CocoaPods](http://cocoapods.org). To install -it, simply add the following line to your Podfile: +it or update it, use the following line to your Podfile: ```ruby -pod 'Cache' +pod 'Cache', :git => 'https://github.com/hyperoslo/Cache.git' + ``` ### Carthage diff --git a/Source/Mac/NSImage+Extensions.swift b/Source/Mac/NSImage+Extensions.swift index 8ebbc9a..92d6dfc 100644 --- a/Source/Mac/NSImage+Extensions.swift +++ b/Source/Mac/NSImage+Extensions.swift @@ -1,4 +1,4 @@ -#if canImport(AppKit) +#if canImport(AppKit) && !targetEnvironment(macCatalyst) import AppKit /// Helper UIImage extension. diff --git a/Source/PrivacyInfo.xcprivacy b/Source/PrivacyInfo.xcprivacy new file mode 100644 index 0000000..934f6ed --- /dev/null +++ b/Source/PrivacyInfo.xcprivacy @@ -0,0 +1,19 @@ + + + + + NSPrivacyCollectedDataTypes + + NSPrivacyAccessedAPITypes + + + NSPrivacyAccessedAPITypeReasons + + C617.1 + + NSPrivacyAccessedAPIType + NSPrivacyAccessedAPICategoryFileTimestamp + + + + diff --git a/Source/Shared/Extensions/FileManager+Extensions.swift b/Source/Shared/Extensions/FileManager+Extensions.swift new file mode 100644 index 0000000..c0c6c4c --- /dev/null +++ b/Source/Shared/Extensions/FileManager+Extensions.swift @@ -0,0 +1,22 @@ + +import Foundation + +extension FileManager { + /** + Calculates the size of the directory at the provided `URL` + - parameter url: the directory url + - returns: the size of the provided directory in bytes + */ + public func sizeOfDirectory(at url: URL) -> Int? { + guard let enumerator = self.enumerator(at: url, includingPropertiesForKeys: [], options: [], errorHandler: { _, _ -> Bool in + return false + }) else { + return nil + } + var size = 0 + for case let url as URL in enumerator { + size += url.fileSize ?? 0 + } + return size + } +} diff --git a/Source/Shared/Extensions/URL+Extensions.swift b/Source/Shared/Extensions/URL+Extensions.swift new file mode 100644 index 0000000..8d7ec49 --- /dev/null +++ b/Source/Shared/Extensions/URL+Extensions.swift @@ -0,0 +1,14 @@ + +import Foundation + +extension URL { + /// Returns the file size of the file at the given `URL` in bytes + var fileSize: Int? { + do { + let file = try self.resourceValues(forKeys: [.totalFileAllocatedSizeKey, .fileAllocatedSizeKey]) + return file.totalFileAllocatedSize ?? file.fileAllocatedSize + } catch { + return nil + } + } +} diff --git a/Source/Shared/Library/Result.swift b/Source/Shared/Library/Result.swift deleted file mode 100644 index 4b80f49..0000000 --- a/Source/Shared/Library/Result.swift +++ /dev/null @@ -1,16 +0,0 @@ -import Foundation - -/// Used for callback in async operations. -public enum Result { - case value(T) - case error(Error) - - public func map(_ transform: (T) -> U) -> Result { - switch self { - case .value(let value): - return Result.value(transform(value)) - case .error(let error): - return Result.error(error) - } - } -} diff --git a/Source/Shared/Library/TransformerFactory.swift b/Source/Shared/Library/TransformerFactory.swift index e10a212..05fd6b4 100644 --- a/Source/Shared/Library/TransformerFactory.swift +++ b/Source/Shared/Library/TransformerFactory.swift @@ -9,7 +9,7 @@ public class TransformerFactory { return Transformer(toData: toData, fromData: fromData) } - #if os(iOS) || os(tvOS) || os(macOS) || os(watchOS) + #if os(iOS) || os(tvOS) || os(macOS) || os(watchOS) || os(visionOS) public static func forImage() -> Transformer { let toData: (Image) throws -> Data = { image in return try image.cache_toData().unwrapOrThrow(error: StorageError.transformerFail) diff --git a/Source/Shared/Library/Types.swift b/Source/Shared/Library/Types.swift index caf7456..52c03b2 100644 --- a/Source/Shared/Library/Types.swift +++ b/Source/Shared/Library/Types.swift @@ -1,4 +1,4 @@ -#if os(iOS) || os(tvOS) || os(watchOS) +#if os(iOS) || os(tvOS) || os(watchOS) || os(visionOS) import UIKit public typealias Image = UIImage #elseif os(watchOS) diff --git a/Source/Shared/Storage/AsyncStorage.swift b/Source/Shared/Storage/AsyncStorage.swift index 9a11994..4f1c1b0 100644 --- a/Source/Shared/Storage/AsyncStorage.swift +++ b/Source/Shared/Storage/AsyncStorage.swift @@ -14,34 +14,34 @@ public class AsyncStorage { } extension AsyncStorage { - public func entry(forKey key: Key, completion: @escaping (Result>) -> Void) { + public func entry(forKey key: Key, completion: @escaping (Result, Error>) -> Void) { serialQueue.async { [weak self] in guard let `self` = self else { - completion(Result.error(StorageError.deallocated)) + completion(.failure(StorageError.deallocated)) return } do { let anEntry = try self.innerStorage.entry(forKey: key) - completion(Result.value(anEntry)) + completion(.success(anEntry)) } catch { - completion(Result.error(error)) + completion(.failure(error)) } } } - public func removeObject(forKey key: Key, completion: @escaping (Result<()>) -> Void) { + public func removeObject(forKey key: Key, completion: @escaping (Result<(), Error>) -> Void) { serialQueue.async { [weak self] in guard let `self` = self else { - completion(Result.error(StorageError.deallocated)) + completion(.failure(StorageError.deallocated)) return } do { try self.innerStorage.removeObject(forKey: key) - completion(Result.value(())) + completion(.success(())) } catch { - completion(Result.error(error)) + completion(.failure(error)) } } } @@ -50,71 +50,82 @@ extension AsyncStorage { _ object: Value, forKey key: Key, expiry: Expiry? = nil, - completion: @escaping (Result<()>) -> Void) { + completion: @escaping (Result<(), Error>) -> Void) { serialQueue.async { [weak self] in guard let `self` = self else { - completion(Result.error(StorageError.deallocated)) + completion(.failure(StorageError.deallocated)) return } do { try self.innerStorage.setObject(object, forKey: key, expiry: expiry) - completion(Result.value(())) + completion(.success(())) } catch { - completion(Result.error(error)) + completion(.failure(error)) } } } - public func removeAll(completion: @escaping (Result<()>) -> Void) { + public func removeAll(completion: @escaping (Result<(), Error>) -> Void) { serialQueue.async { [weak self] in guard let `self` = self else { - completion(Result.error(StorageError.deallocated)) + completion(.failure(StorageError.deallocated)) return } do { try self.innerStorage.removeAll() - completion(Result.value(())) + completion(.success(())) } catch { - completion(Result.error(error)) + completion(.failure(error)) } } } - public func removeExpiredObjects(completion: @escaping (Result<()>) -> Void) { + public func removeExpiredObjects(completion: @escaping (Result<(), Error>) -> Void) { serialQueue.async { [weak self] in guard let `self` = self else { - completion(Result.error(StorageError.deallocated)) + completion(.failure(StorageError.deallocated)) return } do { try self.innerStorage.removeExpiredObjects() - completion(Result.value(())) + completion(.success(())) } catch { - completion(Result.error(error)) + completion(.failure(error)) } } } - public func object(forKey key: Key, completion: @escaping (Result) -> Void) { - entry(forKey: key, completion: { (result: Result>) in + public func object(forKey key: Key, completion: @escaping (Result) -> Void) { + entry(forKey: key, completion: { (result: Result, Error>) in completion(result.map({ entry in return entry.object })) }) } + @available(*, deprecated, renamed: "objectExists(forKey:completion:)") public func existsObject( forKey key: Key, - completion: @escaping (Result) -> Void) { - object(forKey: key, completion: { (result: Result) in + completion: @escaping (Result) -> Void) { + object(forKey: key, completion: { (result: Result) in completion(result.map({ _ in return true })) }) } + + public func objectExists( + forKey key: Key, + completion: @escaping (Result) -> Void) { + object(forKey: key, completion: { (result: Result) in + completion(result.map({ _ in + return true + })) + }) + } } public extension AsyncStorage { diff --git a/Source/Shared/Storage/DiskStorage.swift b/Source/Shared/Storage/DiskStorage.swift index 78adad5..d21a31f 100644 --- a/Source/Shared/Storage/DiskStorage.swift +++ b/Source/Shared/Storage/DiskStorage.swift @@ -58,9 +58,13 @@ final public class DiskStorage { } extension DiskStorage: StorageAware { + public var allKeys: [Key] { [] } + + public var allObjects: [Value] { [] } + public func entry(forKey key: Key) throws -> Entry { let filePath = makeFilePath(for: key) - let data = try Data(contentsOf: URL(fileURLWithPath: filePath)) + let data = try Data(contentsOf: URL(fileURLWithPath: filePath, isDirectory: false)) let attributes = try fileManager.attributesOfItem(atPath: filePath) let object = try transformer.fromData(data) @@ -95,7 +99,7 @@ extension DiskStorage: StorageAware { } public func removeExpiredObjects() throws { - let storageURL = URL(fileURLWithPath: path) + let storageURL = URL(fileURLWithPath: path, isDirectory: true) let resourceKeys: [URLResourceKey] = [ .isDirectoryKey, .contentModificationDateKey, @@ -141,6 +145,8 @@ extension DiskStorage: StorageAware { // Remove objects if storage size exceeds max size try removeResourceObjects(resourceObjects, totalSize: totalSize) } + + public func removeInMemoryObject(forKey key: Key) throws { } } extension DiskStorage { @@ -163,7 +169,7 @@ extension DiskStorage { */ func makeFileName(for key: Key) -> String { if let key = key as? String { - let fileExtension = URL(fileURLWithPath: key).pathExtension + let fileExtension = (key as NSString).pathExtension let fileName = MD5(key) switch fileExtension.isEmpty { @@ -188,20 +194,6 @@ extension DiskStorage { return "\(path)/\(makeFileName(for: key))" } - /// Calculates total disk cache size. - func totalSize() throws -> UInt64 { - var size: UInt64 = 0 - let contents = try fileManager.contentsOfDirectory(atPath: path) - for pathComponent in contents { - let filePath = NSString(string: path).appendingPathComponent(pathComponent) - let attributes = try fileManager.attributesOfItem(atPath: filePath) - if let fileSize = attributes[.size] as? UInt64 { - size += fileSize - } - } - return size - } - func createDirectory() throws { guard !fileManager.fileExists(atPath: path) else { return @@ -273,3 +265,13 @@ public extension DiskStorage { return storage } } + +public extension DiskStorage { + /// Calculates the total size of the cache directory in bytes. + var totalSize: Int? { + if let directory = URL(string: self.path), let size = self.fileManager.sizeOfDirectory(at: directory) { + return size + } + return nil + } +} diff --git a/Source/Shared/Storage/HybridStorage.swift b/Source/Shared/Storage/HybridStorage.swift index 6a529cc..9844803 100644 --- a/Source/Shared/Storage/HybridStorage.swift +++ b/Source/Shared/Storage/HybridStorage.swift @@ -26,6 +26,14 @@ public final class HybridStorage { } extension HybridStorage: StorageAware { + public var allKeys: [Key] { + memoryStorage.allKeys + } + + public var allObjects: [Value] { + memoryStorage.allObjects + } + public func entry(forKey key: Key) throws -> Entry { do { return try memoryStorage.entry(forKey: key) @@ -43,6 +51,11 @@ extension HybridStorage: StorageAware { notifyStorageObservers(about: .remove(key: key)) } + + public func removeInMemoryObject(forKey key: Key) throws { + memoryStorage.removeObject(forKey: key) + notifyStorageObservers(about: .removeInMemory(key: key)) + } public func setObject(_ object: Value, forKey key: Key, expiry: Expiry? = nil) throws { var keyChange: KeyChange? @@ -165,3 +178,10 @@ extension HybridStorage: KeyObservationRegistry { } } } + +public extension HybridStorage { + /// Returns the total size of the underlying DiskStorage in bytes. + var totalDiskStorageSize: Int? { + return self.diskStorage.totalSize + } +} diff --git a/Source/Shared/Storage/MemoryStorage.swift b/Source/Shared/Storage/MemoryStorage.swift index 6ab4b6d..567bf31 100644 --- a/Source/Shared/Storage/MemoryStorage.swift +++ b/Source/Shared/Storage/MemoryStorage.swift @@ -31,6 +31,14 @@ public class MemoryStorage: StorageAware { } extension MemoryStorage { + public var allKeys: [Key] { + Array(keys) + } + + public var allObjects: [Value] { + allKeys.compactMap { try? object(forKey: $0) } + } + public func setObject(_ object: Value, forKey key: Key, expiry: Expiry? = nil) { let capsule = MemoryCapsule(value: object, expiry: .date(expiry?.date ?? config.expiry.date)) cache.setObject(capsule, forKey: WrappedKey(key)) @@ -59,6 +67,11 @@ extension MemoryStorage { cache.removeObject(forKey: WrappedKey(key)) keys.remove(key) } + + public func removeInMemoryObject(forKey key: Key) throws { + cache.removeObject(forKey: WrappedKey(key)) + keys.remove(key) + } public func entry(forKey key: Key) throws -> Entry { guard let capsule = cache.object(forKey: WrappedKey(key)) else { diff --git a/Source/Shared/Storage/Storage.swift b/Source/Shared/Storage/Storage.swift index f3749cc..12b567f 100644 --- a/Source/Shared/Storage/Storage.swift +++ b/Source/Shared/Storage/Storage.swift @@ -43,6 +43,18 @@ public final class Storage { } extension Storage: StorageAware { + public func removeInMemoryObject(forKey key: Key) throws { + try self.syncStorage.removeInMemoryObject(forKey: key) + } + + public var allKeys: [Key] { + self.syncStorage.allKeys + } + + public var allObjects: [Value] { + self.syncStorage.allObjects + } + public func entry(forKey key: Key) throws -> Entry { return try self.syncStorage.entry(forKey: key) } @@ -108,3 +120,10 @@ extension Storage: KeyObservationRegistry { hybridStorage.removeAllKeyObservers() } } + +public extension Storage { + /// Returns the total size of the DiskStorage of the underlying HybridStorage in bytes. + var totalDiskStorageSize: Int? { + return self.hybridStorage.diskStorage.totalSize + } +} diff --git a/Source/Shared/Storage/StorageAware.swift b/Source/Shared/Storage/StorageAware.swift index ede372f..6ab7b3f 100644 --- a/Source/Shared/Storage/StorageAware.swift +++ b/Source/Shared/Storage/StorageAware.swift @@ -4,6 +4,17 @@ import Foundation public protocol StorageAware { associatedtype Key: Hashable associatedtype Value + + /** + Get all keys in the storage + */ + var allKeys: [Key] { get } + + /** + Get all objects from the storage + */ + var allObjects: [Value] { get } + /** Tries to retrieve the object from the storage. - Parameter key: Unique key to identify the object in the cache @@ -36,8 +47,15 @@ public protocol StorageAware { Check if an object exist by the given key. - Parameter key: Unique key to identify the object. */ + @available(*, deprecated, renamed: "objectExists(forKey:)") func existsObject(forKey key: Key) throws -> Bool + /** + Check if an object exist by the given key. + - Parameter key: Unique key to identify the object. + */ + func objectExists(forKey key: Key) -> Bool + /** Removes all objects from the cache storage. */ @@ -53,6 +71,11 @@ public protocol StorageAware { - Parameter key: Unique key to identify the object. */ func isExpiredObject(forKey key: Key) throws -> Bool + /** + Removes the object by the given key from cache in memory only. + - Parameter key: Unique key to identify the object. + */ + func removeInMemoryObject(forKey key: Key) throws } public extension StorageAware { @@ -69,6 +92,15 @@ public extension StorageAware { } } + func objectExists(forKey key: Key) -> Bool { + do { + let _: Value = try object(forKey: key) + return true + } catch { + return false + } + } + func isExpiredObject(forKey key: Key) throws -> Bool { do { let entry = try self.entry(forKey: key) diff --git a/Source/Shared/Storage/StorageObservationRegistry.swift b/Source/Shared/Storage/StorageObservationRegistry.swift index abd4927..788631e 100644 --- a/Source/Shared/Storage/StorageObservationRegistry.swift +++ b/Source/Shared/Storage/StorageObservationRegistry.swift @@ -28,11 +28,12 @@ public enum StorageChange: Equatable { case remove(key: Key) case removeAll case removeExpired + case removeInMemory(key: Key) } public func == (lhs: StorageChange, rhs: StorageChange) -> Bool { switch (lhs, rhs) { - case (.add(let key1), .add(let key2)), (.remove(let key1), .remove(let key2)): + case (.add(let key1), .add(let key2)), (.remove(let key1), .remove(let key2)), (.removeInMemory(let key1), .removeInMemory(let key2)): return key1 == key2 case (.removeAll, .removeAll), (.removeExpired, .removeExpired): return true diff --git a/Source/Shared/Storage/SyncStorage.swift b/Source/Shared/Storage/SyncStorage.swift index 6555842..b7bab94 100644 --- a/Source/Shared/Storage/SyncStorage.swift +++ b/Source/Shared/Storage/SyncStorage.swift @@ -14,12 +14,27 @@ public class SyncStorage { } extension SyncStorage: StorageAware { + public var allKeys: [Key] { + var keys: [Key]! + serialQueue.sync { + keys = self.innerStorage.allKeys + } + return keys + } + + public var allObjects: [Value] { + var objects: [Value]! + serialQueue.sync { + objects = self.innerStorage.allObjects + } + return objects + } + public func entry(forKey key: Key) throws -> Entry { var entry: Entry! try serialQueue.sync { entry = try innerStorage.entry(forKey: key) } - return entry } @@ -46,6 +61,13 @@ extension SyncStorage: StorageAware { try innerStorage.removeExpiredObjects() } } + + public func removeInMemoryObject(forKey key: Key) throws { + try serialQueue.sync { + try self.innerStorage.removeInMemoryObject(forKey: key) + } + } + } public extension SyncStorage { diff --git a/Tests/iOS/Tests/Storage/AsyncStorageTests.swift b/Tests/iOS/Tests/Storage/AsyncStorageTests.swift index 7ced640..1251d15 100644 --- a/Tests/iOS/Tests/Storage/AsyncStorageTests.swift +++ b/Tests/iOS/Tests/Storage/AsyncStorageTests.swift @@ -25,7 +25,7 @@ final class AsyncStorageTests: XCTestCase { storage.setObject(user, forKey: "user", completion: { _ in }) storage.object(forKey: "user", completion: { result in switch result { - case .value(let cachedUser): + case .success(let cachedUser): XCTAssertEqual(cachedUser, self.user) expectation.fulfill() default: @@ -50,9 +50,9 @@ final class AsyncStorageTests: XCTestCase { } then("all are removed") { - intStorage.existsObject(forKey: "key-99", completion: { result in + intStorage.objectExists(forKey: "key-99", completion: { result in switch result { - case .value: + case .success: XCTFail() default: expectation.fulfill() diff --git a/Tests/iOS/Tests/Storage/StorageTests.swift b/Tests/iOS/Tests/Storage/StorageTests.swift index f267f19..80f3f18 100644 --- a/Tests/iOS/Tests/Storage/StorageTests.swift +++ b/Tests/iOS/Tests/Storage/StorageTests.swift @@ -33,7 +33,7 @@ final class StorageTests: XCTestCase { storage.async.object(forKey: "user", completion: { result in switch result { - case .value(let cachedUser): + case .success(let cachedUser): XCTAssertEqual(cachedUser, self.user) expectation.fulfill() default: diff --git a/Tests/iOS/Tests/Storage/SyncStorageTests.swift b/Tests/iOS/Tests/Storage/SyncStorageTests.swift index 4c055b3..8d31f82 100644 --- a/Tests/iOS/Tests/Storage/SyncStorageTests.swift +++ b/Tests/iOS/Tests/Storage/SyncStorageTests.swift @@ -40,8 +40,8 @@ final class SyncStorageTests: XCTestCase { try intStorage.removeAll() } - try then("all are removed") { - XCTAssertFalse(try intStorage.existsObject(forKey: "key-99")) + then("all are removed") { + XCTAssertFalse(intStorage.objectExists(forKey: "key-99")) } } }