ソースを参照

Add support for force cache file extension

onevcat 1 年間 前
コミット
880fbff88a

+ 35 - 19
Sources/Cache/DiskStorage.swift

@@ -146,7 +146,9 @@ public enum DiskStorage {
             value: T,
             forKey key: String,
             expiration: StorageExpiration? = nil,
-            writeOptions: Data.WritingOptions = []) throws
+            writeOptions: Data.WritingOptions = [],
+            forcedExtension: String? = nil
+        ) throws
         {
             guard storageReady else {
                 throw KingfisherError.cacheError(reason: .diskStorageIsNotReady(cacheURL: directoryURL))
@@ -163,7 +165,7 @@ public enum DiskStorage {
                 throw KingfisherError.cacheError(reason: .cannotConvertToData(object: value, error: error))
             }
 
-            let fileURL = cacheFileURL(forKey: key)
+            let fileURL = cacheFileURL(forKey: key, forcedExtension: forcedExtension)
             do {
                 try data.write(to: fileURL, options: writeOptions)
             } catch {
@@ -215,22 +217,34 @@ public enum DiskStorage {
         ///   - extendingExpiration: The expiration policy used by this retrieval action.
         /// - Throws: An error during converting the data to a value or during the operation of disk files.
         /// - Returns: The value under `key` if it is valid and found in the storage; otherwise, `nil`.
-        public func value(forKey key: String, extendingExpiration: ExpirationExtending = .cacheTime) throws -> T? {
-            try value(forKey: key, referenceDate: Date(), actuallyLoad: true, extendingExpiration: extendingExpiration)
+        public func value(
+            forKey key: String,
+            forcedExtension: String? = nil,
+            extendingExpiration: ExpirationExtending = .cacheTime
+        ) throws -> T? {
+            try value(
+                forKey: key,
+                referenceDate: Date(),
+                actuallyLoad: true,
+                extendingExpiration: extendingExpiration,
+                forcedExtension: forcedExtension
+            )
         }
 
         func value(
             forKey key: String,
             referenceDate: Date,
             actuallyLoad: Bool,
-            extendingExpiration: ExpirationExtending) throws -> T?
+            extendingExpiration: ExpirationExtending,
+            forcedExtension: String?
+        ) throws -> T?
         {
             guard storageReady else {
                 throw KingfisherError.cacheError(reason: .diskStorageIsNotReady(cacheURL: directoryURL))
             }
 
             let fileManager = config.fileManager
-            let fileURL = cacheFileURL(forKey: key)
+            let fileURL = cacheFileURL(forKey: key, forcedExtension: forcedExtension)
             let filePath = fileURL.path
 
             let fileMaybeCached = maybeCachedCheckingQueue.sync {
@@ -276,8 +290,8 @@ public enum DiskStorage {
         ///
         /// > This method does not actually load the data from disk, so it is faster than directly loading the cached
         /// value by checking the nullability of the ``DiskStorage/Backend/value(forKey:extendingExpiration:)`` method.
-        public func isCached(forKey key: String) -> Bool {
-            return isCached(forKey: key, referenceDate: Date())
+        public func isCached(forKey key: String, forcedExtension: String? = nil) -> Bool {
+            return isCached(forKey: key, referenceDate: Date(), forcedExtension: forcedExtension)
         }
 
         /// Determines whether there is valid cached data under a given key and a reference date.
@@ -291,13 +305,14 @@ public enum DiskStorage {
         /// If you pass `Date()` as the `referenceDate`, this method is identical to
         /// ``DiskStorage/Backend/isCached(forKey:)``. Use the `referenceDate` to determine whether the cache is still
         /// valid for a future date.
-        public func isCached(forKey key: String, referenceDate: Date) -> Bool {
+        public func isCached(forKey key: String, referenceDate: Date, forcedExtension: String? = nil) -> Bool {
             do {
                 let result = try value(
                     forKey: key,
                     referenceDate: referenceDate,
                     actuallyLoad: false,
-                    extendingExpiration: .none
+                    extendingExpiration: .none,
+                    forcedExtension: forcedExtension
                 )
                 return result != nil
             } catch {
@@ -308,8 +323,8 @@ public enum DiskStorage {
         /// Removes a value from a specified key.
         /// - Parameter key: The cache key of the value.
         /// - Throws: An error during the removal of the value.
-        public func remove(forKey key: String) throws {
-            let fileURL = cacheFileURL(forKey: key)
+        public func remove(forKey key: String, forcedExtension: String? = nil) throws {
+            let fileURL = cacheFileURL(forKey: key, forcedExtension: forcedExtension)
             try removeFile(at: fileURL)
         }
 
@@ -338,23 +353,24 @@ public enum DiskStorage {
         ///
         /// This method does not guarantee that an image is already cached at the returned URL. It just provides the URL 
         /// where the image should be if it exists in the disk storage, with the given key.
-        public func cacheFileURL(forKey key: String) -> URL {
-            let fileName = cacheFileName(forKey: key)
+        public func cacheFileURL(forKey key: String, forcedExtension: String? = nil) -> URL {
+            let fileName = cacheFileName(forKey: key, forcedExtension: forcedExtension)
             return directoryURL.appendingPathComponent(fileName, isDirectory: false)
         }
-
-        func cacheFileName(forKey key: String) -> String {
+        
+        func cacheFileName(forKey key: String, forcedExtension: String? = nil) -> String {
+            // TODO: Bad code... Consider refactoring.
             if config.usesHashedFileName {
                 let hashedKey = key.kf.sha256
-                if let ext = config.pathExtension {
+                if let ext = forcedExtension ?? config.pathExtension {
                     return "\(hashedKey).\(ext)"
                 } else if config.autoExtAfterHashedFileName,
-                          let ext = key.kf.ext {
+                          let ext = forcedExtension ?? key.kf.ext {
                     return "\(hashedKey).\(ext)"
                 }
                 return hashedKey
             } else {
-                if let ext = config.pathExtension {
+                if let ext = forcedExtension ?? config.pathExtension {
                     return "\(key).\(ext)"
                 }
                 return key

+ 69 - 23
Sources/Cache/ImageCache.swift

@@ -365,6 +365,7 @@ open class ImageCache: @unchecked Sendable {
                 self.syncStoreToDisk(
                     data,
                     forKey: key,
+                    forcedExtension: options.forcedExtension,
                     processorIdentifier: identifier,
                     callbackQueue: callbackQueue,
                     expiration: options.diskCacheExpiration,
@@ -410,6 +411,7 @@ open class ImageCache: @unchecked Sendable {
         original: Data? = nil,
         forKey key: String,
         processorIdentifier identifier: String = "",
+        forcedExtension: String? = nil,
         cacheSerializer serializer: any CacheSerializer = DefaultCacheSerializer.default,
         toDisk: Bool = true,
         callbackQueue: CallbackQueue = .untouch,
@@ -426,16 +428,24 @@ open class ImageCache: @unchecked Sendable {
         let options = KingfisherParsedOptionsInfo([
             .processor(TempProcessor(identifier: identifier)),
             .cacheSerializer(serializer),
-            .callbackQueue(callbackQueue)
+            .callbackQueue(callbackQueue),
+            .forcedCacheFileExtension(forcedExtension)
         ])
-        store(image, original: original, forKey: key, options: options,
-              toDisk: toDisk, completionHandler: completionHandler)
+        store(
+            image,
+            original: original,
+            forKey: key,
+            options: options,
+            toDisk: toDisk,
+            completionHandler: completionHandler
+        )
     }
     
     open func storeToDisk(
         _ data: Data,
         forKey key: String,
         processorIdentifier identifier: String = "",
+        forcedExtension: String? = nil,
         expiration: StorageExpiration? = nil,
         callbackQueue: CallbackQueue = .untouch,
         completionHandler: (@Sendable (CacheStoreResult) -> Void)? = nil)
@@ -444,16 +454,19 @@ open class ImageCache: @unchecked Sendable {
             self.syncStoreToDisk(
                 data,
                 forKey: key,
+                forcedExtension: forcedExtension,
                 processorIdentifier: identifier,
                 callbackQueue: callbackQueue,
                 expiration: expiration,
-                completionHandler: completionHandler)
+                completionHandler: completionHandler
+            )
         }
     }
     
     private func syncStoreToDisk(
         _ data: Data,
         forKey key: String,
+        forcedExtension: String?,
         processorIdentifier identifier: String = "",
         callbackQueue: CallbackQueue = .untouch,
         expiration: StorageExpiration? = nil,
@@ -463,7 +476,13 @@ open class ImageCache: @unchecked Sendable {
         let computedKey = key.computedKey(with: identifier)
         let result: CacheStoreResult
         do {
-            try self.diskStorage.store(value: data, forKey: computedKey, expiration: expiration, writeOptions: writeOptions)
+            try self.diskStorage.store(
+                value: data,
+                forKey: computedKey,
+                expiration: expiration,
+                writeOptions: writeOptions,
+                forcedExtension: forcedExtension
+            )
             result = CacheStoreResult(memoryCacheResult: .success(()), diskCacheResult: .success(()))
         } catch {
             let diskError: KingfisherError
@@ -501,6 +520,7 @@ open class ImageCache: @unchecked Sendable {
     open func removeImage(
         forKey key: String,
         processorIdentifier identifier: String = "",
+        forcedExtension: String? = nil,
         fromMemory: Bool = true,
         fromDisk: Bool = true,
         callbackQueue: CallbackQueue = .untouch,
@@ -510,6 +530,7 @@ open class ImageCache: @unchecked Sendable {
         removeImage(
             forKey: key,
             processorIdentifier: identifier,
+            forcedExtension: forcedExtension,
             fromMemory: fromMemory,
             fromDisk: fromDisk,
             callbackQueue: callbackQueue,
@@ -517,12 +538,14 @@ open class ImageCache: @unchecked Sendable {
         )
     }
     
-    func removeImage(forKey key: String,
-                          processorIdentifier identifier: String = "",
-                          fromMemory: Bool = true,
-                          fromDisk: Bool = true,
-                          callbackQueue: CallbackQueue = .untouch,
-                          completionHandler: (@Sendable ((any Error)?) -> Void)? = nil)
+    func removeImage(
+        forKey key: String,
+        processorIdentifier identifier: String = "",
+        forcedExtension: String?,
+        fromMemory: Bool = true,
+        fromDisk: Bool = true,
+        callbackQueue: CallbackQueue = .untouch,
+        completionHandler: (@Sendable ((any Error)?) -> Void)? = nil)
     {
         let computedKey = key.computedKey(with: identifier)
 
@@ -539,7 +562,7 @@ open class ImageCache: @unchecked Sendable {
         if fromDisk {
             ioQueue.async{
                 do {
-                    try self.diskStorage.remove(forKey: computedKey)
+                    try self.diskStorage.remove(forKey: computedKey, forcedExtension: forcedExtension)
                     callHandler(nil)
                 } catch {
                     callHandler(error)
@@ -687,7 +710,11 @@ open class ImageCache: @unchecked Sendable {
         loadingQueue.execute {
             do {
                 var image: KFCrossPlatformImage? = nil
-                if let data = try self.diskStorage.value(forKey: computedKey, extendingExpiration: options.diskCacheAccessExtendingExpiration) {
+                if let data = try self.diskStorage.value(
+                    forKey: computedKey,
+                    forcedExtension: options.forcedExtension,
+                    extendingExpiration: options.diskCacheAccessExtendingExpiration
+                ) {
                     image = options.cacheSerializer.image(with: data, options: options)
                 }
                 if options.backgroundDecode {
@@ -865,11 +892,13 @@ open class ImageCache: @unchecked Sendable {
     /// image is not in the cache or that it has already expired.
     open func imageCachedType(
         forKey key: String,
-        processorIdentifier identifier: String = DefaultImageProcessor.default.identifier) -> CacheType
+        processorIdentifier identifier: String = DefaultImageProcessor.default.identifier,
+        forcedExtension: String? = nil
+    ) -> CacheType
     {
         let computedKey = key.computedKey(with: identifier)
         if memoryStorage.isCached(forKey: computedKey) { return .memory }
-        if diskStorage.isCached(forKey: computedKey) { return .disk }
+        if diskStorage.isCached(forKey: computedKey, forcedExtension: forcedExtension) { return .disk }
         return .none
     }
     
@@ -886,9 +915,11 @@ open class ImageCache: @unchecked Sendable {
     ///  ``ImageCache/imageCachedType(forKey:processorIdentifier:)`` instead.
     public func isCached(
         forKey key: String,
-        processorIdentifier identifier: String = DefaultImageProcessor.default.identifier) -> Bool
+        processorIdentifier identifier: String = DefaultImageProcessor.default.identifier,
+        forcedExtension: String? = nil
+    ) -> Bool
     {
-        return imageCachedType(forKey: key, processorIdentifier: identifier).cached
+        return imageCachedType(forKey: key, processorIdentifier: identifier, forcedExtension: forcedExtension).cached
     }
     
     /// Retrieves the hash used as the cache file name for the key.
@@ -904,10 +935,12 @@ open class ImageCache: @unchecked Sendable {
     /// needed.
     open func hash(
         forKey key: String,
-        processorIdentifier identifier: String = DefaultImageProcessor.default.identifier) -> String
+        processorIdentifier identifier: String = DefaultImageProcessor.default.identifier,
+        forcedExtension: String? = nil
+    ) -> String
     {
         let computedKey = key.computedKey(with: identifier)
-        return diskStorage.cacheFileName(forKey: computedKey)
+        return diskStorage.cacheFileName(forKey: computedKey, forcedExtension: forcedExtension)
     }
     
     /// Calculates the size taken by the disk storage.
@@ -948,18 +981,25 @@ open class ImageCache: @unchecked Sendable {
     /// cached under that key on disk if necessary.
     open func cachePath(
         forKey key: String,
-        processorIdentifier identifier: String = DefaultImageProcessor.default.identifier) -> String
+        processorIdentifier identifier: String = DefaultImageProcessor.default.identifier,
+        forcedExtension: String? = nil
+    ) -> String
     {
         let computedKey = key.computedKey(with: identifier)
-        return diskStorage.cacheFileURL(forKey: computedKey).path
+        return diskStorage.cacheFileURL(forKey: computedKey, forcedExtension: forcedExtension).path
     }
     
     open func cacheFileURLIfOnDisk(
         forKey key: String,
-        processorIdentifier identifier: String = DefaultImageProcessor.default.identifier) -> URL?
+        processorIdentifier identifier: String = DefaultImageProcessor.default.identifier,
+        forcedExtension: String? = nil
+    ) -> URL?
     {
         let computedKey = key.computedKey(with: identifier)
-        return diskStorage.isCached(forKey: computedKey) ? diskStorage.cacheFileURL(forKey: computedKey) : nil
+        return diskStorage.isCached(
+            forKey: computedKey,
+            forcedExtension: forcedExtension
+        ) ? diskStorage.cacheFileURL(forKey: computedKey, forcedExtension: forcedExtension) : nil
     }
     
     // MARK: - Concurrency
@@ -1012,6 +1052,7 @@ open class ImageCache: @unchecked Sendable {
         original: Data? = nil,
         forKey key: String,
         processorIdentifier identifier: String = "",
+        forcedExtension: String? = nil,
         cacheSerializer serializer: any CacheSerializer = DefaultCacheSerializer.default,
         toDisk: Bool = true
     ) async throws {
@@ -1021,6 +1062,7 @@ open class ImageCache: @unchecked Sendable {
                 original: original,
                 forKey: key,
                 processorIdentifier: identifier,
+                forcedExtension: forcedExtension,
                 cacheSerializer: serializer,
                 toDisk: toDisk) {
                     // Only `diskCacheResult` can fail
@@ -1033,6 +1075,7 @@ open class ImageCache: @unchecked Sendable {
         _ data: Data,
         forKey key: String,
         processorIdentifier identifier: String = "",
+        forcedExtension: String? = nil,
         expiration: StorageExpiration? = nil
     ) async throws
     {
@@ -1041,6 +1084,7 @@ open class ImageCache: @unchecked Sendable {
                 data,
                 forKey: key,
                 processorIdentifier: identifier,
+                forcedExtension: forcedExtension,
                 expiration: expiration) {
                     // Only `diskCacheResult` can fail
                     continuation.resume(with: $0.diskCacheResult)
@@ -1061,6 +1105,7 @@ open class ImageCache: @unchecked Sendable {
     open func removeImage(
         forKey key: String,
         processorIdentifier identifier: String = "",
+        forcedExtension: String? = nil,
         fromMemory: Bool = true,
         fromDisk: Bool = true
     ) async throws {
@@ -1068,6 +1113,7 @@ open class ImageCache: @unchecked Sendable {
             removeImage(
                 forKey: key,
                 processorIdentifier: identifier,
+                forcedExtension: forcedExtension,
                 fromMemory: fromMemory,
                 fromDisk: fromDisk,
                 completionHandler: { error in

+ 5 - 6
Sources/General/ImageSource/LivePhotoSource.swift

@@ -31,6 +31,7 @@ public struct LivePhotoResource: Sendable {
     public enum FileType: Sendable {
         case heic
         case mov
+        case other(String)
     }
     
     public let resource: any Resource
@@ -53,12 +54,10 @@ public struct LivePhotoResource: Sendable {
 extension Resource {
     var guessedFileType: LivePhotoResource.FileType {
         let pathExtension = downloadURL.pathExtension.lowercased()
-        switch pathExtension {
-        case "mov": return .mov
-        case "heic": return .heic
-        default:
-            assertionFailure("Explicit file type is necessary in the download URL as its extension. Otherwise, set the file type of the LivePhoto resource manually with `LivePhotoSource.init(resources:)`.")
-            return .heic
+        return switch pathExtension {
+        case "mov": .mov
+        case "heic": .heic
+        default: .other(pathExtension)
         }
     }
 }

+ 4 - 0
Sources/General/KingfisherOptionsInfo.swift

@@ -347,6 +347,8 @@ public enum KingfisherOptionsInfoItem: Sendable {
     /// If not set or if the associated optional ``Source`` value is `nil`, the device's Low Data Mode will be ignored,
     /// and the original source will be loaded following the system default behavior.
     case lowDataMode(Source?)
+    
+    case forcedCacheFileExtension(String?)
 }
 
 // MARK: - KingfisherParsedOptionsInfo
@@ -397,6 +399,7 @@ public struct KingfisherParsedOptionsInfo: Sendable {
     public var alternativeSources: [Source]? = nil
     public var retryStrategy: (any RetryStrategy)? = nil
     public var lowDataModeSource: Source? = nil
+    public var forcedExtension: String? = nil
 
     var onDataReceived: [any DataReceivingSideEffect]? = nil
     
@@ -440,6 +443,7 @@ public struct KingfisherParsedOptionsInfo: Sendable {
             case .alternativeSources(let sources): alternativeSources = sources
             case .retryStrategy(let strategy): retryStrategy = strategy
             case .lowDataMode(let source): lowDataModeSource = source
+            case .forcedCacheFileExtension(let ext): forcedExtension = ext
             }
         }
 

+ 2 - 1
Sources/Networking/ImagePrefetcher.swift

@@ -309,7 +309,8 @@ public class ImagePrefetcher: CustomStringConvertible, @unchecked Sendable {
         
         let cacheType = manager.cache.imageCachedType(
             forKey: source.cacheKey,
-            processorIdentifier: optionsInfo.processor.identifier)
+            processorIdentifier: optionsInfo.processor.identifier
+        )
         switch cacheType {
         case .memory:
             append(cached: source)