123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237 |
- //
- // DiskCache.swift
- // Haneke
- //
- // Created by Hermes Pique on 8/10/14.
- // Copyright (c) 2014 Haneke. All rights reserved.
- //
- import Foundation
- open class DiskCache {
-
- open class func basePath() -> String {
- let cachesPath = NSSearchPathForDirectoriesInDomains(FileManager.SearchPathDirectory.cachesDirectory, FileManager.SearchPathDomainMask.userDomainMask, true)[0]
- let hanekePathComponent = HanekeGlobals.Domain
- let basePath = (cachesPath as NSString).appendingPathComponent(hanekePathComponent)
- // TODO: Do not recaculate basePath value
- return basePath
- }
-
- public let path: String
- open var size : UInt64 = 0
- open var capacity : UInt64 = 0 {
- didSet {
- self.cacheQueue.async(execute: {
- self.controlCapacity()
- })
- }
- }
- open lazy var cacheQueue : DispatchQueue = {
- let queueName = HanekeGlobals.Domain + "." + (self.path as NSString).lastPathComponent
- let cacheQueue = DispatchQueue(label: queueName, attributes: [])
- return cacheQueue
- }()
-
- public init(path: String, capacity: UInt64 = UINT64_MAX) {
- self.path = path
- self.capacity = capacity
- self.cacheQueue.async(execute: {
- self.calculateSize()
- self.controlCapacity()
- })
- }
-
- open func setData( _ getData: @autoclosure @escaping () -> Data?, key: String) {
- cacheQueue.async(execute: {
- if let data = getData() {
- self.setDataSync(data, key: key)
- } else {
- Log.error(message: "Failed to get data for key \(key)")
- }
- })
- }
-
- open func fetchData(key: String, failure fail: ((Error?) -> ())? = nil, success succeed: @escaping (Data) -> ()) {
- cacheQueue.async {
- let path = self.path(forKey: key)
- do {
- let data = try Data(contentsOf: URL(fileURLWithPath: path), options: Data.ReadingOptions())
- DispatchQueue.main.async {
- succeed(data)
- }
- self.updateDiskAccessDate(atPath: path)
- } catch {
- if let block = fail {
- DispatchQueue.main.async {
- block(error)
- }
- }
- }
- }
- }
- open func removeData(with key: String) {
- cacheQueue.async(execute: {
- let path = self.path(forKey: key)
- self.removeFile(atPath: path)
- })
- }
-
- open func removeAllData(_ completion: (() -> ())? = nil) {
- let fileManager = FileManager.default
- let cachePath = self.path
- cacheQueue.async(execute: {
- do {
- let contents = try fileManager.contentsOfDirectory(atPath: cachePath)
- for pathComponent in contents {
- let path = (cachePath as NSString).appendingPathComponent(pathComponent)
- do {
- try fileManager.removeItem(atPath: path)
- } catch {
- Log.error(message: "Failed to remove path \(path)", error: error)
- }
- }
- self.calculateSize()
- } catch {
- Log.error(message: "Failed to list directory", error: error)
- }
- if let completion = completion {
- DispatchQueue.main.async {
- completion()
- }
- }
- })
- }
- open func updateAccessDate( _ getData: @autoclosure @escaping () -> Data?, key: String) {
- cacheQueue.async(execute: {
- let path = self.path(forKey: key)
- let fileManager = FileManager.default
- if (!(fileManager.fileExists(atPath: path) && self.updateDiskAccessDate(atPath: path))){
- if let data = getData() {
- self.setDataSync(data, key: key)
- } else {
- Log.error(message: "Failed to get data for key \(key)")
- }
- }
- })
- }
- open func path(forKey key: String) -> String {
- let escapedFilename = key.escapedFilename()
- let filename = escapedFilename.count < Int(NAME_MAX) ? escapedFilename : key.MD5Filename()
- let keyPath = (self.path as NSString).appendingPathComponent(filename)
- return keyPath
- }
-
- // MARK: Private
-
- fileprivate func calculateSize() {
- let fileManager = FileManager.default
- size = 0
- let cachePath = self.path
- do {
- let contents = try fileManager.contentsOfDirectory(atPath: cachePath)
- for pathComponent in contents {
- let path = (cachePath as NSString).appendingPathComponent(pathComponent)
- do {
- let attributes: [FileAttributeKey: Any] = try fileManager.attributesOfItem(atPath: path)
- if let fileSize = attributes[FileAttributeKey.size] as? UInt64 {
- size += fileSize
- }
- } catch {
- Log.error(message: "Failed to list directory", error: error)
- }
- }
-
- } catch {
- Log.error(message: "Failed to list directory", error: error)
- }
- }
-
- fileprivate func controlCapacity() {
- if self.size <= self.capacity { return }
-
- let fileManager = FileManager.default
- let cachePath = self.path
- fileManager.enumerateContentsOfDirectory(atPath: cachePath, orderedByProperty: URLResourceKey.contentModificationDateKey.rawValue, ascending: true) { (URL : URL, _, stop : inout Bool) -> Void in
-
- self.removeFile(atPath: URL.path)
- stop = self.size <= self.capacity
- }
- }
-
- fileprivate func setDataSync(_ data: Data, key: String) {
- let path = self.path(forKey: key)
- let fileManager = FileManager.default
- let previousAttributes : [FileAttributeKey: Any]? = try? fileManager.attributesOfItem(atPath: path)
-
- do {
- try data.write(to: URL(fileURLWithPath: path), options: Data.WritingOptions.atomicWrite)
- } catch {
- Log.error(message: "Failed to write key \(key)", error: error)
- }
-
- if let attributes = previousAttributes {
- if let fileSize = attributes[FileAttributeKey.size] as? UInt64 {
- substract(size: fileSize)
- }
- }
- self.size += UInt64(data.count)
- self.controlCapacity()
- }
-
- @discardableResult fileprivate func updateDiskAccessDate(atPath path: String) -> Bool {
- let fileManager = FileManager.default
- let now = Date()
- do {
- try fileManager.setAttributes([FileAttributeKey.modificationDate : now], ofItemAtPath: path)
- return true
- } catch {
- Log.error(message: "Failed to update access date", error: error)
- return false
- }
- }
-
- fileprivate func removeFile(atPath path: String) {
- let fileManager = FileManager.default
- do {
- let attributes: [FileAttributeKey: Any] = try fileManager.attributesOfItem(atPath: path)
- do {
- try fileManager.removeItem(atPath: path)
- if let fileSize = attributes[FileAttributeKey.size] as? UInt64 {
- substract(size: fileSize)
- }
- } catch {
- Log.error(message: "Failed to remove file", error: error)
- }
- } catch {
- if isNoSuchFileError(error) {
- Log.debug(message: "File not found", error: error)
- } else {
- Log.error(message: "Failed to remove file", error: error)
- }
- }
- }
- fileprivate func substract(size : UInt64) {
- if (self.size >= size) {
- self.size -= size
- } else {
- Log.error(message: "Disk cache size (\(self.size)) is smaller than size to substract (\(size))")
- self.size = 0
- }
- }
- }
- private func isNoSuchFileError(_ error : Error?) -> Bool {
- if let error = error {
- return NSCocoaErrorDomain == (error as NSError).domain && (error as NSError).code == NSFileReadNoSuchFileError
- }
- return false
- }
|