SDDiskCache.m 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291
  1. /*
  2. * This file is part of the SDWebImage package.
  3. * (c) Olivier Poitrey <rs@dailymotion.com>
  4. *
  5. * For the full copyright and license information, please view the LICENSE
  6. * file that was distributed with this source code.
  7. */
  8. #import "SDDiskCache.h"
  9. #import "SDImageCacheConfig.h"
  10. #import <CommonCrypto/CommonDigest.h>
  11. @interface SDDiskCache ()
  12. @property (nonatomic, copy) NSString *diskCachePath;
  13. @property (nonatomic, strong, nonnull) NSFileManager *fileManager;
  14. @end
  15. @implementation SDDiskCache
  16. - (instancetype)init {
  17. NSAssert(NO, @"Use `initWithCachePath:` with the disk cache path");
  18. return nil;
  19. }
  20. #pragma mark - SDcachePathForKeyDiskCache Protocol
  21. - (instancetype)initWithCachePath:(NSString *)cachePath config:(nonnull SDImageCacheConfig *)config {
  22. if (self = [super init]) {
  23. _diskCachePath = cachePath;
  24. _config = config;
  25. [self commonInit];
  26. }
  27. return self;
  28. }
  29. - (void)commonInit {
  30. if (self.config.fileManager) {
  31. self.fileManager = self.config.fileManager;
  32. } else {
  33. self.fileManager = [NSFileManager new];
  34. }
  35. }
  36. - (BOOL)containsDataForKey:(NSString *)key {
  37. NSParameterAssert(key);
  38. NSString *filePath = [self cachePathForKey:key];
  39. BOOL exists = [self.fileManager fileExistsAtPath:filePath];
  40. // fallback because of https://github.com/rs/SDWebImage/pull/976 that added the extension to the disk file name
  41. // checking the key with and without the extension
  42. if (!exists) {
  43. exists = [self.fileManager fileExistsAtPath:filePath.stringByDeletingPathExtension];
  44. }
  45. return exists;
  46. }
  47. - (NSData *)dataForKey:(NSString *)key {
  48. NSParameterAssert(key);
  49. NSString *filePath = [self cachePathForKey:key];
  50. NSData *data = [NSData dataWithContentsOfFile:filePath options:self.config.diskCacheReadingOptions error:nil];
  51. if (data) {
  52. return data;
  53. }
  54. // fallback because of https://github.com/rs/SDWebImage/pull/976 that added the extension to the disk file name
  55. // checking the key with and without the extension
  56. data = [NSData dataWithContentsOfFile:filePath.stringByDeletingPathExtension options:self.config.diskCacheReadingOptions error:nil];
  57. if (data) {
  58. return data;
  59. }
  60. return nil;
  61. }
  62. - (void)setData:(NSData *)data forKey:(NSString *)key {
  63. NSParameterAssert(data);
  64. NSParameterAssert(key);
  65. if (![self.fileManager fileExistsAtPath:self.diskCachePath]) {
  66. [self.fileManager createDirectoryAtPath:self.diskCachePath withIntermediateDirectories:YES attributes:nil error:NULL];
  67. }
  68. // get cache Path for image key
  69. NSString *cachePathForKey = [self cachePathForKey:key];
  70. // transform to NSUrl
  71. NSURL *fileURL = [NSURL fileURLWithPath:cachePathForKey];
  72. [data writeToURL:fileURL options:self.config.diskCacheWritingOptions error:nil];
  73. // disable iCloud backup
  74. if (self.config.shouldDisableiCloud) {
  75. // ignore iCloud backup resource value error
  76. [fileURL setResourceValue:@YES forKey:NSURLIsExcludedFromBackupKey error:nil];
  77. }
  78. }
  79. - (void)removeDataForKey:(NSString *)key {
  80. NSParameterAssert(key);
  81. NSString *filePath = [self cachePathForKey:key];
  82. [self.fileManager removeItemAtPath:filePath error:nil];
  83. }
  84. - (void)removeAllData {
  85. [self.fileManager removeItemAtPath:self.diskCachePath error:nil];
  86. [self.fileManager createDirectoryAtPath:self.diskCachePath
  87. withIntermediateDirectories:YES
  88. attributes:nil
  89. error:NULL];
  90. }
  91. - (void)removeExpiredData {
  92. NSURL *diskCacheURL = [NSURL fileURLWithPath:self.diskCachePath isDirectory:YES];
  93. // Compute content date key to be used for tests
  94. NSURLResourceKey cacheContentDateKey = NSURLContentModificationDateKey;
  95. switch (self.config.diskCacheExpireType) {
  96. case SDImageCacheConfigExpireTypeAccessDate:
  97. cacheContentDateKey = NSURLContentAccessDateKey;
  98. break;
  99. case SDImageCacheConfigExpireTypeModificationDate:
  100. cacheContentDateKey = NSURLContentModificationDateKey;
  101. break;
  102. default:
  103. break;
  104. }
  105. NSArray<NSString *> *resourceKeys = @[NSURLIsDirectoryKey, cacheContentDateKey, NSURLTotalFileAllocatedSizeKey];
  106. // This enumerator prefetches useful properties for our cache files.
  107. NSDirectoryEnumerator *fileEnumerator = [self.fileManager enumeratorAtURL:diskCacheURL
  108. includingPropertiesForKeys:resourceKeys
  109. options:NSDirectoryEnumerationSkipsHiddenFiles
  110. errorHandler:NULL];
  111. NSDate *expirationDate = (self.config.maxDiskAge < 0) ? nil: [NSDate dateWithTimeIntervalSinceNow:-self.config.maxDiskAge];
  112. NSMutableDictionary<NSURL *, NSDictionary<NSString *, id> *> *cacheFiles = [NSMutableDictionary dictionary];
  113. NSUInteger currentCacheSize = 0;
  114. // Enumerate all of the files in the cache directory. This loop has two purposes:
  115. //
  116. // 1. Removing files that are older than the expiration date.
  117. // 2. Storing file attributes for the size-based cleanup pass.
  118. NSMutableArray<NSURL *> *urlsToDelete = [[NSMutableArray alloc] init];
  119. for (NSURL *fileURL in fileEnumerator) {
  120. NSError *error;
  121. NSDictionary<NSString *, id> *resourceValues = [fileURL resourceValuesForKeys:resourceKeys error:&error];
  122. // Skip directories and errors.
  123. if (error || !resourceValues || [resourceValues[NSURLIsDirectoryKey] boolValue]) {
  124. continue;
  125. }
  126. // Remove files that are older than the expiration date;
  127. NSDate *modifiedDate = resourceValues[cacheContentDateKey];
  128. if (expirationDate && [[modifiedDate laterDate:expirationDate] isEqualToDate:expirationDate]) {
  129. [urlsToDelete addObject:fileURL];
  130. continue;
  131. }
  132. // Store a reference to this file and account for its total size.
  133. NSNumber *totalAllocatedSize = resourceValues[NSURLTotalFileAllocatedSizeKey];
  134. currentCacheSize += totalAllocatedSize.unsignedIntegerValue;
  135. cacheFiles[fileURL] = resourceValues;
  136. }
  137. for (NSURL *fileURL in urlsToDelete) {
  138. [self.fileManager removeItemAtURL:fileURL error:nil];
  139. }
  140. // If our remaining disk cache exceeds a configured maximum size, perform a second
  141. // size-based cleanup pass. We delete the oldest files first.
  142. NSUInteger maxDiskSize = self.config.maxDiskSize;
  143. if (maxDiskSize > 0 && currentCacheSize > maxDiskSize) {
  144. // Target half of our maximum cache size for this cleanup pass.
  145. const NSUInteger desiredCacheSize = maxDiskSize / 2;
  146. // Sort the remaining cache files by their last modification time or last access time (oldest first).
  147. NSArray<NSURL *> *sortedFiles = [cacheFiles keysSortedByValueWithOptions:NSSortConcurrent
  148. usingComparator:^NSComparisonResult(id obj1, id obj2) {
  149. return [obj1[cacheContentDateKey] compare:obj2[cacheContentDateKey]];
  150. }];
  151. // Delete files until we fall below our desired cache size.
  152. for (NSURL *fileURL in sortedFiles) {
  153. if ([self.fileManager removeItemAtURL:fileURL error:nil]) {
  154. NSDictionary<NSString *, id> *resourceValues = cacheFiles[fileURL];
  155. NSNumber *totalAllocatedSize = resourceValues[NSURLTotalFileAllocatedSizeKey];
  156. currentCacheSize -= totalAllocatedSize.unsignedIntegerValue;
  157. if (currentCacheSize < desiredCacheSize) {
  158. break;
  159. }
  160. }
  161. }
  162. }
  163. }
  164. - (nullable NSString *)cachePathForKey:(NSString *)key {
  165. NSParameterAssert(key);
  166. return [self cachePathForKey:key inPath:self.diskCachePath];
  167. }
  168. - (NSUInteger)totalSize {
  169. NSUInteger size = 0;
  170. NSDirectoryEnumerator *fileEnumerator = [self.fileManager enumeratorAtPath:self.diskCachePath];
  171. for (NSString *fileName in fileEnumerator) {
  172. NSString *filePath = [self.diskCachePath stringByAppendingPathComponent:fileName];
  173. NSDictionary<NSString *, id> *attrs = [self.fileManager attributesOfItemAtPath:filePath error:nil];
  174. size += [attrs fileSize];
  175. }
  176. return size;
  177. }
  178. - (NSUInteger)totalCount {
  179. NSUInteger count = 0;
  180. NSDirectoryEnumerator *fileEnumerator = [self.fileManager enumeratorAtPath:self.diskCachePath];
  181. count = fileEnumerator.allObjects.count;
  182. return count;
  183. }
  184. #pragma mark - Cache paths
  185. - (nullable NSString *)cachePathForKey:(nullable NSString *)key inPath:(nonnull NSString *)path {
  186. NSString *filename = SDDiskCacheFileNameForKey(key);
  187. return [path stringByAppendingPathComponent:filename];
  188. }
  189. - (void)moveCacheDirectoryFromPath:(nonnull NSString *)srcPath toPath:(nonnull NSString *)dstPath {
  190. NSParameterAssert(srcPath);
  191. NSParameterAssert(dstPath);
  192. // Check if old path is equal to new path
  193. if ([srcPath isEqualToString:dstPath]) {
  194. return;
  195. }
  196. BOOL isDirectory;
  197. // Check if old path is directory
  198. if (![self.fileManager fileExistsAtPath:srcPath isDirectory:&isDirectory] || !isDirectory) {
  199. return;
  200. }
  201. // Check if new path is directory
  202. if (![self.fileManager fileExistsAtPath:dstPath isDirectory:&isDirectory] || !isDirectory) {
  203. if (!isDirectory) {
  204. // New path is not directory, remove file
  205. [self.fileManager removeItemAtPath:dstPath error:nil];
  206. }
  207. NSString *dstParentPath = [dstPath stringByDeletingLastPathComponent];
  208. // Creates any non-existent parent directories as part of creating the directory in path
  209. if (![self.fileManager fileExistsAtPath:dstParentPath]) {
  210. [self.fileManager createDirectoryAtPath:dstParentPath withIntermediateDirectories:YES attributes:nil error:NULL];
  211. }
  212. // New directory does not exist, rename directory
  213. [self.fileManager moveItemAtPath:srcPath toPath:dstPath error:nil];
  214. } else {
  215. // New directory exist, merge the files
  216. NSDirectoryEnumerator *dirEnumerator = [self.fileManager enumeratorAtPath:srcPath];
  217. NSString *file;
  218. while ((file = [dirEnumerator nextObject])) {
  219. [self.fileManager moveItemAtPath:[srcPath stringByAppendingPathComponent:file] toPath:[dstPath stringByAppendingPathComponent:file] error:nil];
  220. }
  221. // Remove the old path
  222. [self.fileManager removeItemAtPath:srcPath error:nil];
  223. }
  224. }
  225. #pragma mark - Hash
  226. #define SD_MAX_FILE_EXTENSION_LENGTH (NAME_MAX - CC_MD5_DIGEST_LENGTH * 2 - 1)
  227. static inline NSString * _Nonnull SDDiskCacheFileNameForKey(NSString * _Nullable key) {
  228. const char *str = key.UTF8String;
  229. if (str == NULL) {
  230. str = "";
  231. }
  232. unsigned char r[CC_MD5_DIGEST_LENGTH];
  233. CC_MD5(str, (CC_LONG)strlen(str), r);
  234. NSURL *keyURL = [NSURL URLWithString:key];
  235. NSString *ext = keyURL ? keyURL.pathExtension : key.pathExtension;
  236. // File system has file name length limit, we need to check if ext is too long, we don't add it to the filename
  237. if (ext.length > SD_MAX_FILE_EXTENSION_LENGTH) {
  238. ext = nil;
  239. }
  240. NSString *filename = [NSString stringWithFormat:@"%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%@",
  241. r[0], r[1], r[2], r[3], r[4], r[5], r[6], r[7], r[8], r[9], r[10],
  242. r[11], r[12], r[13], r[14], r[15], ext.length == 0 ? @"" : [NSString stringWithFormat:@".%@", ext]];
  243. return filename;
  244. }
  245. @end