SDDiskCache.m 13 KB

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