123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334 |
- /*
- * This file is part of the SDWebImage package.
- * (c) Olivier Poitrey <rs@dailymotion.com>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- #import "SDDiskCache.h"
- #import "SDImageCacheConfig.h"
- #import "SDFileAttributeHelper.h"
- #import <CommonCrypto/CommonDigest.h>
- static NSString * const SDDiskCacheExtendedAttributeName = @"com.hackemist.SDDiskCache";
- @interface SDDiskCache ()
- @property (nonatomic, copy) NSString *diskCachePath;
- @property (nonatomic, strong, nonnull) NSFileManager *fileManager;
- @end
- @implementation SDDiskCache
- - (instancetype)init {
- NSAssert(NO, @"Use `initWithCachePath:` with the disk cache path");
- return nil;
- }
- #pragma mark - SDcachePathForKeyDiskCache Protocol
- - (instancetype)initWithCachePath:(NSString *)cachePath config:(nonnull SDImageCacheConfig *)config {
- if (self = [super init]) {
- _diskCachePath = cachePath;
- _config = config;
- [self commonInit];
- }
- return self;
- }
- - (void)commonInit {
- if (self.config.fileManager) {
- self.fileManager = self.config.fileManager;
- } else {
- self.fileManager = [NSFileManager new];
- }
-
- [self createDirectory];
- }
- - (BOOL)containsDataForKey:(NSString *)key {
- NSParameterAssert(key);
- NSString *filePath = [self cachePathForKey:key];
- BOOL exists = [self.fileManager fileExistsAtPath:filePath];
-
- // fallback because of https://github.com/rs/SDWebImage/pull/976 that added the extension to the disk file name
- // checking the key with and without the extension
- if (!exists) {
- exists = [self.fileManager fileExistsAtPath:filePath.stringByDeletingPathExtension];
- }
-
- return exists;
- }
- - (NSData *)dataForKey:(NSString *)key {
- NSParameterAssert(key);
- NSString *filePath = [self cachePathForKey:key];
- NSData *data = [NSData dataWithContentsOfFile:filePath options:self.config.diskCacheReadingOptions error:nil];
- if (data) {
- return data;
- }
-
- // fallback because of https://github.com/rs/SDWebImage/pull/976 that added the extension to the disk file name
- // checking the key with and without the extension
- data = [NSData dataWithContentsOfFile:filePath.stringByDeletingPathExtension options:self.config.diskCacheReadingOptions error:nil];
- if (data) {
- return data;
- }
-
- return nil;
- }
- - (void)setData:(NSData *)data forKey:(NSString *)key {
- NSParameterAssert(data);
- NSParameterAssert(key);
-
- // get cache Path for image key
- NSString *cachePathForKey = [self cachePathForKey:key];
- // transform to NSURL
- NSURL *fileURL = [NSURL fileURLWithPath:cachePathForKey isDirectory:NO];
-
- [data writeToURL:fileURL options:self.config.diskCacheWritingOptions error:nil];
- }
- - (NSData *)extendedDataForKey:(NSString *)key {
- NSParameterAssert(key);
-
- // get cache Path for image key
- NSString *cachePathForKey = [self cachePathForKey:key];
-
- NSData *extendedData = [SDFileAttributeHelper extendedAttribute:SDDiskCacheExtendedAttributeName atPath:cachePathForKey traverseLink:NO error:nil];
-
- return extendedData;
- }
- - (void)setExtendedData:(NSData *)extendedData forKey:(NSString *)key {
- NSParameterAssert(key);
- // get cache Path for image key
- NSString *cachePathForKey = [self cachePathForKey:key];
-
- if (!extendedData) {
- // Remove
- [SDFileAttributeHelper removeExtendedAttribute:SDDiskCacheExtendedAttributeName atPath:cachePathForKey traverseLink:NO error:nil];
- } else {
- // Override
- [SDFileAttributeHelper setExtendedAttribute:SDDiskCacheExtendedAttributeName value:extendedData atPath:cachePathForKey traverseLink:NO overwrite:YES error:nil];
- }
- }
- - (void)removeDataForKey:(NSString *)key {
- NSParameterAssert(key);
- NSString *filePath = [self cachePathForKey:key];
- [self.fileManager removeItemAtPath:filePath error:nil];
- }
- - (void)removeAllData {
- [self.fileManager removeItemAtPath:self.diskCachePath error:nil];
- [self createDirectory];
- }
- - (void)createDirectory {
- [self.fileManager createDirectoryAtPath:self.diskCachePath
- withIntermediateDirectories:YES
- attributes:nil
- error:NULL];
-
- // disable iCloud backup
- if (self.config.shouldDisableiCloud) {
- // ignore iCloud backup resource value error
- [[NSURL fileURLWithPath:self.diskCachePath isDirectory:YES] setResourceValue:@YES forKey:NSURLIsExcludedFromBackupKey error:nil];
- }
- }
- - (void)removeExpiredData {
- NSURL *diskCacheURL = [NSURL fileURLWithPath:self.diskCachePath isDirectory:YES];
-
- // Compute content date key to be used for tests
- NSURLResourceKey cacheContentDateKey = NSURLContentModificationDateKey;
- switch (self.config.diskCacheExpireType) {
- case SDImageCacheConfigExpireTypeAccessDate:
- cacheContentDateKey = NSURLContentAccessDateKey;
- break;
- case SDImageCacheConfigExpireTypeModificationDate:
- cacheContentDateKey = NSURLContentModificationDateKey;
- break;
- case SDImageCacheConfigExpireTypeCreationDate:
- cacheContentDateKey = NSURLCreationDateKey;
- break;
- case SDImageCacheConfigExpireTypeChangeDate:
- cacheContentDateKey = NSURLAttributeModificationDateKey;
- break;
- default:
- break;
- }
-
- NSArray<NSString *> *resourceKeys = @[NSURLIsDirectoryKey, cacheContentDateKey, NSURLTotalFileAllocatedSizeKey];
-
- // This enumerator prefetches useful properties for our cache files.
- NSDirectoryEnumerator *fileEnumerator = [self.fileManager enumeratorAtURL:diskCacheURL
- includingPropertiesForKeys:resourceKeys
- options:NSDirectoryEnumerationSkipsHiddenFiles
- errorHandler:NULL];
-
- NSDate *expirationDate = (self.config.maxDiskAge < 0) ? nil: [NSDate dateWithTimeIntervalSinceNow:-self.config.maxDiskAge];
- NSMutableDictionary<NSURL *, NSDictionary<NSString *, id> *> *cacheFiles = [NSMutableDictionary dictionary];
- NSUInteger currentCacheSize = 0;
-
- // Enumerate all of the files in the cache directory. This loop has two purposes:
- //
- // 1. Removing files that are older than the expiration date.
- // 2. Storing file attributes for the size-based cleanup pass.
- NSMutableArray<NSURL *> *urlsToDelete = [[NSMutableArray alloc] init];
- for (NSURL *fileURL in fileEnumerator) {
- NSError *error;
- NSDictionary<NSString *, id> *resourceValues = [fileURL resourceValuesForKeys:resourceKeys error:&error];
-
- // Skip directories and errors.
- if (error || !resourceValues || [resourceValues[NSURLIsDirectoryKey] boolValue]) {
- continue;
- }
-
- // Remove files that are older than the expiration date;
- NSDate *modifiedDate = resourceValues[cacheContentDateKey];
- if (expirationDate && [[modifiedDate laterDate:expirationDate] isEqualToDate:expirationDate]) {
- [urlsToDelete addObject:fileURL];
- continue;
- }
-
- // Store a reference to this file and account for its total size.
- NSNumber *totalAllocatedSize = resourceValues[NSURLTotalFileAllocatedSizeKey];
- currentCacheSize += totalAllocatedSize.unsignedIntegerValue;
- cacheFiles[fileURL] = resourceValues;
- }
-
- for (NSURL *fileURL in urlsToDelete) {
- [self.fileManager removeItemAtURL:fileURL error:nil];
- }
-
- // If our remaining disk cache exceeds a configured maximum size, perform a second
- // size-based cleanup pass. We delete the oldest files first.
- NSUInteger maxDiskSize = self.config.maxDiskSize;
- if (maxDiskSize > 0 && currentCacheSize > maxDiskSize) {
- // Target half of our maximum cache size for this cleanup pass.
- const NSUInteger desiredCacheSize = maxDiskSize / 2;
-
- // Sort the remaining cache files by their last modification time or last access time (oldest first).
- NSArray<NSURL *> *sortedFiles = [cacheFiles keysSortedByValueWithOptions:NSSortConcurrent
- usingComparator:^NSComparisonResult(id obj1, id obj2) {
- return [obj1[cacheContentDateKey] compare:obj2[cacheContentDateKey]];
- }];
-
- // Delete files until we fall below our desired cache size.
- for (NSURL *fileURL in sortedFiles) {
- if ([self.fileManager removeItemAtURL:fileURL error:nil]) {
- NSDictionary<NSString *, id> *resourceValues = cacheFiles[fileURL];
- NSNumber *totalAllocatedSize = resourceValues[NSURLTotalFileAllocatedSizeKey];
- currentCacheSize -= totalAllocatedSize.unsignedIntegerValue;
-
- if (currentCacheSize < desiredCacheSize) {
- break;
- }
- }
- }
- }
- }
- - (nullable NSString *)cachePathForKey:(NSString *)key {
- NSParameterAssert(key);
- return [self cachePathForKey:key inPath:self.diskCachePath];
- }
- - (NSUInteger)totalSize {
- NSUInteger size = 0;
- NSDirectoryEnumerator *fileEnumerator = [self.fileManager enumeratorAtPath:self.diskCachePath];
- for (NSString *fileName in fileEnumerator) {
- NSString *filePath = [self.diskCachePath stringByAppendingPathComponent:fileName];
- NSDictionary<NSString *, id> *attrs = [self.fileManager attributesOfItemAtPath:filePath error:nil];
- size += [attrs fileSize];
- }
- return size;
- }
- - (NSUInteger)totalCount {
- NSUInteger count = 0;
- NSDirectoryEnumerator *fileEnumerator = [self.fileManager enumeratorAtPath:self.diskCachePath];
- count = fileEnumerator.allObjects.count;
- return count;
- }
- #pragma mark - Cache paths
- - (nullable NSString *)cachePathForKey:(nullable NSString *)key inPath:(nonnull NSString *)path {
- NSString *filename = SDDiskCacheFileNameForKey(key);
- return [path stringByAppendingPathComponent:filename];
- }
- - (void)moveCacheDirectoryFromPath:(nonnull NSString *)srcPath toPath:(nonnull NSString *)dstPath {
- NSParameterAssert(srcPath);
- NSParameterAssert(dstPath);
- // Check if old path is equal to new path
- if ([srcPath isEqualToString:dstPath]) {
- return;
- }
- BOOL isDirectory;
- // Check if old path is directory
- if (![self.fileManager fileExistsAtPath:srcPath isDirectory:&isDirectory] || !isDirectory) {
- return;
- }
- // Check if new path is directory
- if (![self.fileManager fileExistsAtPath:dstPath isDirectory:&isDirectory] || !isDirectory) {
- if (!isDirectory) {
- // New path is not directory, remove file
- [self.fileManager removeItemAtPath:dstPath error:nil];
- }
- NSString *dstParentPath = [dstPath stringByDeletingLastPathComponent];
- // Creates any non-existent parent directories as part of creating the directory in path
- if (![self.fileManager fileExistsAtPath:dstParentPath]) {
- [self.fileManager createDirectoryAtPath:dstParentPath withIntermediateDirectories:YES attributes:nil error:NULL];
- }
- // New directory does not exist, rename directory
- [self.fileManager moveItemAtPath:srcPath toPath:dstPath error:nil];
- // disable iCloud backup
- if (self.config.shouldDisableiCloud) {
- // ignore iCloud backup resource value error
- [[NSURL fileURLWithPath:dstPath isDirectory:YES] setResourceValue:@YES forKey:NSURLIsExcludedFromBackupKey error:nil];
- }
- } else {
- // New directory exist, merge the files
- NSDirectoryEnumerator *dirEnumerator = [self.fileManager enumeratorAtPath:srcPath];
- NSString *file;
- while ((file = [dirEnumerator nextObject])) {
- [self.fileManager moveItemAtPath:[srcPath stringByAppendingPathComponent:file] toPath:[dstPath stringByAppendingPathComponent:file] error:nil];
- }
- // Remove the old path
- [self.fileManager removeItemAtPath:srcPath error:nil];
- }
- }
- #pragma mark - Hash
- #define SD_MAX_FILE_EXTENSION_LENGTH (NAME_MAX - CC_MD5_DIGEST_LENGTH * 2 - 1)
- #pragma clang diagnostic push
- #pragma clang diagnostic ignored "-Wdeprecated-declarations"
- static inline NSString * _Nonnull SDDiskCacheFileNameForKey(NSString * _Nullable key) {
- const char *str = key.UTF8String;
- if (str == NULL) {
- str = "";
- }
- unsigned char r[CC_MD5_DIGEST_LENGTH];
- CC_MD5(str, (CC_LONG)strlen(str), r);
- NSURL *keyURL = [NSURL URLWithString:key];
- NSString *ext = keyURL ? keyURL.pathExtension : key.pathExtension;
- // File system has file name length limit, we need to check if ext is too long, we don't add it to the filename
- if (ext.length > SD_MAX_FILE_EXTENSION_LENGTH) {
- ext = nil;
- }
- NSString *filename = [NSString stringWithFormat:@"%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%@",
- r[0], r[1], r[2], r[3], r[4], r[5], r[6], r[7], r[8], r[9], r[10],
- r[11], r[12], r[13], r[14], r[15], ext.length == 0 ? @"" : [NSString stringWithFormat:@".%@", ext]];
- return filename;
- }
- #pragma clang diagnostic pop
- @end
|