SDDiskCache.m 13 KB

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