Explorar o código

Add no throw version of disk storage

onevcat %!s(int64=5) %!d(string=hai) anos
pai
achega
d3ae6e8a2f

+ 50 - 19
Sources/Cache/DiskStorage.swift

@@ -50,40 +50,44 @@ public enum DiskStorage {
         var maybeCached : Set<String>?
         let maybeCachedCheckingQueue = DispatchQueue(label: "com.onevcat.Kingfisher.maybeCachedCheckingQueue")
 
+        // `false` if the storage initialized with an error. This prevents unexpected forcibly crash when creating
+        // storage in the default cache.
+        private var storageReady: Bool = true
+
         /// Creates a disk storage with the given `DiskStorage.Config`.
         ///
         /// - Parameter config: The config used for this disk storage.
         /// - Throws: An error if the folder for storage cannot be got or created.
-        public init(config: Config) throws {
+        public convenience init(config: Config) throws {
+            self.init(noThrowConfig: config, creatingDirectory: false)
+            try prepareDirectory()
+        }
 
+        // If `creatingDirectory` is `false`, the directory preparation will be skipped.
+        // We need to call `prepareDirectory` manually after this returns.
+        init(noThrowConfig config: Config, creatingDirectory: Bool) {
             var config = config
 
-            let url: URL
-            if let directory = config.directory {
-                url = directory
-            } else {
-                url = try config.fileManager.url(
-                    for: .cachesDirectory,
-                    in: .userDomainMask,
-                    appropriateFor: nil,
-                    create: true)
-            }
-
-            let cacheName = "com.onevcat.Kingfisher.ImageCache.\(config.name)"
-            directoryURL = config.cachePathBlock(url, cacheName)
+            let creation = Creation(config)
+            self.directoryURL = creation.directoryURL
 
             // Break any possible retain cycle set by outside.
             config.cachePathBlock = nil
-
             self.config = config
 
-            metaChangingQueue = DispatchQueue(label: cacheName)
-            try prepareDirectory()
+            metaChangingQueue = DispatchQueue(label: creation.cacheName)
+            setupCacheChecking()
 
+            if creatingDirectory {
+                try? prepareDirectory()
+            }
+        }
+
+        private func setupCacheChecking() {
             maybeCachedCheckingQueue.async {
                 do {
                     self.maybeCached = Set()
-                    try config.fileManager.contentsOfDirectory(atPath: self.directoryURL.path).forEach { fileName in
+                    try self.config.fileManager.contentsOfDirectory(atPath: self.directoryURL.path).forEach { fileName in
                         self.maybeCached?.insert(fileName)
                     }
                 } catch {
@@ -95,7 +99,7 @@ public enum DiskStorage {
         }
 
         // Creates the storage folder.
-        func prepareDirectory() throws {
+        private func prepareDirectory() throws {
             let fileManager = config.fileManager
             let path = directoryURL.path
 
@@ -107,6 +111,7 @@ public enum DiskStorage {
                     withIntermediateDirectories: true,
                     attributes: nil)
             } catch {
+                self.storageReady = false
                 throw KingfisherError.cacheError(reason: .cannotCreateDirectory(path: path, error: error))
             }
         }
@@ -116,6 +121,10 @@ public enum DiskStorage {
             forKey key: String,
             expiration: StorageExpiration? = nil) throws
         {
+            guard storageReady else {
+                throw KingfisherError.cacheError(reason: .diskStorageIsNotReady(cacheURL: directoryURL))
+            }
+
             let expiration = expiration ?? config.expiration
             // The expiration indicates that already expired, no need to store.
             guard !expiration.isExpired else { return }
@@ -171,6 +180,10 @@ public enum DiskStorage {
             actuallyLoad: Bool,
             extendingExpiration: ExpirationExtending) throws -> T?
         {
+            guard storageReady else {
+                throw KingfisherError.cacheError(reason: .diskStorageIsNotReady(cacheURL: directoryURL))
+            }
+
             let fileManager = config.fileManager
             let fileURL = cacheFileURL(forKey: key)
             let filePath = fileURL.path
@@ -495,3 +508,21 @@ extension DiskStorage {
     }
 }
 
+extension DiskStorage {
+    struct Creation {
+        let directoryURL: URL
+        let cacheName: String
+
+        init(_ config: Config) {
+            let url: URL
+            if let directory = config.directory {
+                url = directory
+            } else {
+                url = config.fileManager.urls(for: .cachesDirectory, in: .userDomainMask)[0]
+            }
+
+            cacheName = "com.onevcat.Kingfisher.ImageCache.\(config.name)"
+            directoryURL = config.cachePathBlock(url, cacheName)
+        }
+    }
+}

+ 30 - 6
Sources/Cache/ImageCache.swift

@@ -145,7 +145,8 @@ open class ImageCache {
     /// The default `ImageCache` object. Kingfisher will use this cache for its related methods if there is no
     /// other cache specified. The `name` of this default cache is "default", and you should not use this name
     /// for any of your customize cache.
-    public static let `default` = ImageCache(name: "default")
+    public static let `default` = ImageCache(noThrowName: "default")
+
 
     // MARK: Public Properties
     /// The `MemoryStorage.Backend` object used in this cache. This storage holds loaded images in memory with a
@@ -212,6 +213,7 @@ open class ImageCache {
     /// - Parameter name: The name of cache object. It is used to setup disk cache directories and IO queue.
     ///                   You should not use the same `name` for different caches, otherwise, the disk storage would
     ///                   be conflicting to each other. The `name` should not be an empty string.
+    @available(*, deprecated, message: "When used for the first time while the disk is full, a crash would happen. Use `init(noThrowName:)` or the throwable `init(name:cacheDirectoryURL:) throws` instead.", renamed: "init(noThrowName:)")
     public convenience init(name: String) {
         try! self.init(name: name, cacheDirectoryURL: nil, diskCachePathClosure: nil)
     }
@@ -233,16 +235,38 @@ open class ImageCache {
     public convenience init(
         name: String,
         cacheDirectoryURL: URL?,
-        diskCachePathClosure: DiskCachePathClosure? = nil) throws
+        diskCachePathClosure: DiskCachePathClosure? = nil
+    ) throws
+    {
+        if name.isEmpty {
+            fatalError("[Kingfisher] You should specify a name for the cache. A cache with empty name is not permitted.")
+        }
+
+        let memoryStorage = ImageCache.createMemoryStorage()
+
+        let config = ImageCache.createConfig(
+            name: name, cacheDirectoryURL: cacheDirectoryURL, diskCachePathClosure: diskCachePathClosure
+        )
+        let diskStorage = try DiskStorage.Backend<Data>(config: config)
+        self.init(memoryStorage: memoryStorage, diskStorage: diskStorage)
+    }
+
+    convenience init(
+        noThrowName name: String,
+        cacheDirectoryURL: URL? = nil,
+        diskCachePathClosure: DiskCachePathClosure? = nil
+    )
     {
         if name.isEmpty {
             fatalError("[Kingfisher] You should specify a name for the cache. A cache with empty name is not permitted.")
         }
 
         let memoryStorage = ImageCache.createMemoryStorage()
-        let diskStorage = try ImageCache.createDiskStorage(
+
+        let config = ImageCache.createConfig(
             name: name, cacheDirectoryURL: cacheDirectoryURL, diskCachePathClosure: diskCachePathClosure
         )
+        let diskStorage = DiskStorage.Backend<Data>(noThrowConfig: config, creatingDirectory: true)
         self.init(memoryStorage: memoryStorage, diskStorage: diskStorage)
     }
 
@@ -254,11 +278,11 @@ open class ImageCache {
         return memoryStorage
     }
 
-    private static func createDiskStorage(
+    private static func createConfig(
         name: String,
         cacheDirectoryURL: URL?,
         diskCachePathClosure: DiskCachePathClosure? = nil
-    ) throws -> DiskStorage.Backend<Data>
+    ) -> DiskStorage.Config
     {
         var diskConfig = DiskStorage.Config(
             name: name,
@@ -268,7 +292,7 @@ open class ImageCache {
         if let closure = diskCachePathClosure {
             diskConfig.cachePathBlock = closure
         }
-        return try DiskStorage.Backend<Data>(config: diskConfig)
+        return diskConfig
     }
     
     deinit {

+ 12 - 0
Sources/General/KingfisherError.swift

@@ -157,6 +157,14 @@ public enum KingfisherError: Error {
         /// - error: The underlying error originally thrown by Foundation when setting the `attributes` to the disk
         ///          file at `filePath`.
         case cannotSetCacheFileAttribute(filePath: String, attributes: [FileAttributeKey : Any], error: Error)
+
+        /// The disk storage of cache is not ready. Code 3011.
+        ///
+        /// This is usually due to extremely lack of space on disk storage, and
+        /// Kingfisher failed even when creating the cache folder. The disk storage will be in unusable state. Normally,
+        /// ask user to free some spaces and restart the app to make the disk storage work again.
+        /// - cacheURL: The intended URL which should be the storage folder.
+        case diskStorageIsNotReady(cacheURL: URL)
     }
     
     
@@ -382,6 +390,9 @@ extension KingfisherError.CacheErrorReason {
         case .cannotSetCacheFileAttribute(let filePath, let attributes, let error):
             return "Cannot set file attribute for the cache file at path: \(filePath), attributes: \(attributes)." +
                    "Underlying foundation error: \(error)."
+        case .diskStorageIsNotReady(let cacheURL):
+            return "The disk storage is not ready to use yet at URL: '\(cacheURL)'. " +
+                "This is usually caused by extremely lack of disk space. Ask users to free up some space and restart the app."
         }
     }
     
@@ -397,6 +408,7 @@ extension KingfisherError.CacheErrorReason {
         case .cannotSerializeImage: return 3008
         case .cannotCreateCacheFile: return 3009
         case .cannotSetCacheFileAttribute: return 3010
+        case .diskStorageIsNotReady: return 3011
         }
     }
 }