DiskCache.swift 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237
  1. //
  2. // DiskCache.swift
  3. // Haneke
  4. //
  5. // Created by Hermes Pique on 8/10/14.
  6. // Copyright (c) 2014 Haneke. All rights reserved.
  7. //
  8. import Foundation
  9. open class DiskCache {
  10. open class func basePath() -> String {
  11. let cachesPath = NSSearchPathForDirectoriesInDomains(FileManager.SearchPathDirectory.cachesDirectory, FileManager.SearchPathDomainMask.userDomainMask, true)[0]
  12. let hanekePathComponent = HanekeGlobals.Domain
  13. let basePath = (cachesPath as NSString).appendingPathComponent(hanekePathComponent)
  14. // TODO: Do not recaculate basePath value
  15. return basePath
  16. }
  17. public let path: String
  18. open var size : UInt64 = 0
  19. open var capacity : UInt64 = 0 {
  20. didSet {
  21. self.cacheQueue.async(execute: {
  22. self.controlCapacity()
  23. })
  24. }
  25. }
  26. open lazy var cacheQueue : DispatchQueue = {
  27. let queueName = HanekeGlobals.Domain + "." + (self.path as NSString).lastPathComponent
  28. let cacheQueue = DispatchQueue(label: queueName, attributes: [])
  29. return cacheQueue
  30. }()
  31. public init(path: String, capacity: UInt64 = UINT64_MAX) {
  32. self.path = path
  33. self.capacity = capacity
  34. self.cacheQueue.async(execute: {
  35. self.calculateSize()
  36. self.controlCapacity()
  37. })
  38. }
  39. open func setData( _ getData: @autoclosure @escaping () -> Data?, key: String) {
  40. cacheQueue.async(execute: {
  41. if let data = getData() {
  42. self.setDataSync(data, key: key)
  43. } else {
  44. Log.error(message: "Failed to get data for key \(key)")
  45. }
  46. })
  47. }
  48. open func fetchData(key: String, failure fail: ((Error?) -> ())? = nil, success succeed: @escaping (Data) -> ()) {
  49. cacheQueue.async {
  50. let path = self.path(forKey: key)
  51. do {
  52. let data = try Data(contentsOf: URL(fileURLWithPath: path), options: Data.ReadingOptions())
  53. DispatchQueue.main.async {
  54. succeed(data)
  55. }
  56. self.updateDiskAccessDate(atPath: path)
  57. } catch {
  58. if let block = fail {
  59. DispatchQueue.main.async {
  60. block(error)
  61. }
  62. }
  63. }
  64. }
  65. }
  66. open func removeData(with key: String) {
  67. cacheQueue.async(execute: {
  68. let path = self.path(forKey: key)
  69. self.removeFile(atPath: path)
  70. })
  71. }
  72. open func removeAllData(_ completion: (() -> ())? = nil) {
  73. let fileManager = FileManager.default
  74. let cachePath = self.path
  75. cacheQueue.async(execute: {
  76. do {
  77. let contents = try fileManager.contentsOfDirectory(atPath: cachePath)
  78. for pathComponent in contents {
  79. let path = (cachePath as NSString).appendingPathComponent(pathComponent)
  80. do {
  81. try fileManager.removeItem(atPath: path)
  82. } catch {
  83. Log.error(message: "Failed to remove path \(path)", error: error)
  84. }
  85. }
  86. self.calculateSize()
  87. } catch {
  88. Log.error(message: "Failed to list directory", error: error)
  89. }
  90. if let completion = completion {
  91. DispatchQueue.main.async {
  92. completion()
  93. }
  94. }
  95. })
  96. }
  97. open func updateAccessDate( _ getData: @autoclosure @escaping () -> Data?, key: String) {
  98. cacheQueue.async(execute: {
  99. let path = self.path(forKey: key)
  100. let fileManager = FileManager.default
  101. if (!(fileManager.fileExists(atPath: path) && self.updateDiskAccessDate(atPath: path))){
  102. if let data = getData() {
  103. self.setDataSync(data, key: key)
  104. } else {
  105. Log.error(message: "Failed to get data for key \(key)")
  106. }
  107. }
  108. })
  109. }
  110. open func path(forKey key: String) -> String {
  111. let escapedFilename = key.escapedFilename()
  112. let filename = escapedFilename.count < Int(NAME_MAX) ? escapedFilename : key.MD5Filename()
  113. let keyPath = (self.path as NSString).appendingPathComponent(filename)
  114. return keyPath
  115. }
  116. // MARK: Private
  117. fileprivate func calculateSize() {
  118. let fileManager = FileManager.default
  119. size = 0
  120. let cachePath = self.path
  121. do {
  122. let contents = try fileManager.contentsOfDirectory(atPath: cachePath)
  123. for pathComponent in contents {
  124. let path = (cachePath as NSString).appendingPathComponent(pathComponent)
  125. do {
  126. let attributes: [FileAttributeKey: Any] = try fileManager.attributesOfItem(atPath: path)
  127. if let fileSize = attributes[FileAttributeKey.size] as? UInt64 {
  128. size += fileSize
  129. }
  130. } catch {
  131. Log.error(message: "Failed to list directory", error: error)
  132. }
  133. }
  134. } catch {
  135. Log.error(message: "Failed to list directory", error: error)
  136. }
  137. }
  138. fileprivate func controlCapacity() {
  139. if self.size <= self.capacity { return }
  140. let fileManager = FileManager.default
  141. let cachePath = self.path
  142. fileManager.enumerateContentsOfDirectory(atPath: cachePath, orderedByProperty: URLResourceKey.contentModificationDateKey.rawValue, ascending: true) { (URL : URL, _, stop : inout Bool) -> Void in
  143. self.removeFile(atPath: URL.path)
  144. stop = self.size <= self.capacity
  145. }
  146. }
  147. fileprivate func setDataSync(_ data: Data, key: String) {
  148. let path = self.path(forKey: key)
  149. let fileManager = FileManager.default
  150. let previousAttributes : [FileAttributeKey: Any]? = try? fileManager.attributesOfItem(atPath: path)
  151. do {
  152. try data.write(to: URL(fileURLWithPath: path), options: Data.WritingOptions.atomicWrite)
  153. } catch {
  154. Log.error(message: "Failed to write key \(key)", error: error)
  155. }
  156. if let attributes = previousAttributes {
  157. if let fileSize = attributes[FileAttributeKey.size] as? UInt64 {
  158. substract(size: fileSize)
  159. }
  160. }
  161. self.size += UInt64(data.count)
  162. self.controlCapacity()
  163. }
  164. @discardableResult fileprivate func updateDiskAccessDate(atPath path: String) -> Bool {
  165. let fileManager = FileManager.default
  166. let now = Date()
  167. do {
  168. try fileManager.setAttributes([FileAttributeKey.modificationDate : now], ofItemAtPath: path)
  169. return true
  170. } catch {
  171. Log.error(message: "Failed to update access date", error: error)
  172. return false
  173. }
  174. }
  175. fileprivate func removeFile(atPath path: String) {
  176. let fileManager = FileManager.default
  177. do {
  178. let attributes: [FileAttributeKey: Any] = try fileManager.attributesOfItem(atPath: path)
  179. do {
  180. try fileManager.removeItem(atPath: path)
  181. if let fileSize = attributes[FileAttributeKey.size] as? UInt64 {
  182. substract(size: fileSize)
  183. }
  184. } catch {
  185. Log.error(message: "Failed to remove file", error: error)
  186. }
  187. } catch {
  188. if isNoSuchFileError(error) {
  189. Log.debug(message: "File not found", error: error)
  190. } else {
  191. Log.error(message: "Failed to remove file", error: error)
  192. }
  193. }
  194. }
  195. fileprivate func substract(size : UInt64) {
  196. if (self.size >= size) {
  197. self.size -= size
  198. } else {
  199. Log.error(message: "Disk cache size (\(self.size)) is smaller than size to substract (\(size))")
  200. self.size = 0
  201. }
  202. }
  203. }
  204. private func isNoSuchFileError(_ error : Error?) -> Bool {
  205. if let error = error {
  206. return NSCocoaErrorDomain == (error as NSError).domain && (error as NSError).code == NSFileReadNoSuchFileError
  207. }
  208. return false
  209. }