// // YYKVStorage.m // YYKit // // Created by ibireme on 15/4/22. // Copyright (c) 2015 ibireme. // // This source code is licensed under the MIT-style license found in the // LICENSE file in the root directory of this source tree. // #import "YYKVStorage.h" #import "UIApplication+YYAdd.h" #import #import #if __has_include() #import #else #import "sqlite3.h" #endif static const NSUInteger kMaxErrorRetryCount = 8; static const NSTimeInterval kMinRetryTimeInterval = 2.0; static const int kPathLengthMax = PATH_MAX - 64; static NSString *const kDBFileName = @"manifest.sqlite"; static NSString *const kDBShmFileName = @"manifest.sqlite-shm"; static NSString *const kDBWalFileName = @"manifest.sqlite-wal"; static NSString *const kDataDirectoryName = @"data"; static NSString *const kTrashDirectoryName = @"trash"; /* File: /path/ /manifest.sqlite /manifest.sqlite-shm /manifest.sqlite-wal /data/ /e10adc3949ba59abbe56e057f20f883e /e10adc3949ba59abbe56e057f20f883e /trash/ /unused_file_or_folder SQL: create table if not exists manifest ( key text, filename text, size integer, inline_data blob, modification_time integer, last_access_time integer, extended_data blob, primary key(key) ); create index if not exists last_access_time_idx on manifest(last_access_time); */ @implementation YYKVStorageItem @end @implementation YYKVStorage { dispatch_queue_t _trashQueue; NSString *_path; NSString *_dbPath; NSString *_dataPath; NSString *_trashPath; sqlite3 *_db; CFMutableDictionaryRef _dbStmtCache; NSTimeInterval _dbLastOpenErrorTime; NSUInteger _dbOpenErrorCount; } #pragma mark - db - (BOOL)_dbOpen { if (_db) return YES; int result = sqlite3_open(_dbPath.UTF8String, &_db); if (result == SQLITE_OK) { CFDictionaryKeyCallBacks keyCallbacks = kCFCopyStringDictionaryKeyCallBacks; CFDictionaryValueCallBacks valueCallbacks = {0}; _dbStmtCache = CFDictionaryCreateMutable(CFAllocatorGetDefault(), 0, &keyCallbacks, &valueCallbacks); _dbLastOpenErrorTime = 0; _dbOpenErrorCount = 0; return YES; } else { _db = NULL; if (_dbStmtCache) CFRelease(_dbStmtCache); _dbStmtCache = NULL; _dbLastOpenErrorTime = CACurrentMediaTime(); _dbOpenErrorCount++; if (_errorLogsEnabled) { NSLog(@"%s line:%d sqlite open failed (%d).", __FUNCTION__, __LINE__, result); } return NO; } } - (BOOL)_dbClose { if (!_db) return YES; int result = 0; BOOL retry = NO; BOOL stmtFinalized = NO; if (_dbStmtCache) CFRelease(_dbStmtCache); _dbStmtCache = NULL; do { retry = NO; result = sqlite3_close(_db); if (result == SQLITE_BUSY || result == SQLITE_LOCKED) { if (!stmtFinalized) { stmtFinalized = YES; sqlite3_stmt *stmt; while ((stmt = sqlite3_next_stmt(_db, nil)) != 0) { sqlite3_finalize(stmt); retry = YES; } } } else if (result != SQLITE_OK) { if (_errorLogsEnabled) { NSLog(@"%s line:%d sqlite close failed (%d).", __FUNCTION__, __LINE__, result); } } } while (retry); _db = NULL; return YES; } - (BOOL)_dbCheck { if (!_db) { if (_dbOpenErrorCount < kMaxErrorRetryCount && CACurrentMediaTime() - _dbLastOpenErrorTime > kMinRetryTimeInterval) { return [self _dbOpen] && [self _dbInitialize]; } else { return NO; } } return YES; } - (BOOL)_dbInitialize { NSString *sql = @"pragma journal_mode = wal; pragma synchronous = normal; create table if not exists manifest (key text, filename text, size integer, inline_data blob, modification_time integer, last_access_time integer, extended_data blob, primary key(key)); create index if not exists last_access_time_idx on manifest(last_access_time);"; return [self _dbExecute:sql]; } - (void)_dbCheckpoint { if (![self _dbCheck]) return; // Cause a checkpoint to occur, merge `sqlite-wal` file to `sqlite` file. sqlite3_wal_checkpoint(_db, NULL); } - (BOOL)_dbExecute:(NSString *)sql { if (sql.length == 0) return NO; if (![self _dbCheck]) return NO; char *error = NULL; int result = sqlite3_exec(_db, sql.UTF8String, NULL, NULL, &error); if (error) { if (_errorLogsEnabled) NSLog(@"%s line:%d sqlite exec error (%d): %s", __FUNCTION__, __LINE__, result, error); sqlite3_free(error); } return result == SQLITE_OK; } - (sqlite3_stmt *)_dbPrepareStmt:(NSString *)sql { if (![self _dbCheck] || sql.length == 0 || !_dbStmtCache) return NULL; sqlite3_stmt *stmt = (sqlite3_stmt *)CFDictionaryGetValue(_dbStmtCache, (__bridge const void *)(sql)); if (!stmt) { int result = sqlite3_prepare_v2(_db, sql.UTF8String, -1, &stmt, NULL); if (result != SQLITE_OK) { if (_errorLogsEnabled) NSLog(@"%s line:%d sqlite stmt prepare error (%d): %s", __FUNCTION__, __LINE__, result, sqlite3_errmsg(_db)); return NULL; } CFDictionarySetValue(_dbStmtCache, (__bridge const void *)(sql), stmt); } else { sqlite3_reset(stmt); } return stmt; } - (NSString *)_dbJoinedKeys:(NSArray *)keys { NSMutableString *string = [NSMutableString new]; for (NSUInteger i = 0,max = keys.count; i < max; i++) { [string appendString:@"?"]; if (i + 1 != max) { [string appendString:@","]; } } return string; } - (void)_dbBindJoinedKeys:(NSArray *)keys stmt:(sqlite3_stmt *)stmt fromIndex:(int)index{ for (int i = 0, max = (int)keys.count; i < max; i++) { NSString *key = keys[i]; sqlite3_bind_text(stmt, index + i, key.UTF8String, -1, NULL); } } - (BOOL)_dbSaveWithKey:(NSString *)key value:(NSData *)value fileName:(NSString *)fileName extendedData:(NSData *)extendedData { NSString *sql = @"insert or replace into manifest (key, filename, size, inline_data, modification_time, last_access_time, extended_data) values (?1, ?2, ?3, ?4, ?5, ?6, ?7);"; sqlite3_stmt *stmt = [self _dbPrepareStmt:sql]; if (!stmt) return NO; int timestamp = (int)time(NULL); sqlite3_bind_text(stmt, 1, key.UTF8String, -1, NULL); sqlite3_bind_text(stmt, 2, fileName.UTF8String, -1, NULL); sqlite3_bind_int(stmt, 3, (int)value.length); if (fileName.length == 0) { sqlite3_bind_blob(stmt, 4, value.bytes, (int)value.length, 0); } else { sqlite3_bind_blob(stmt, 4, NULL, 0, 0); } sqlite3_bind_int(stmt, 5, timestamp); sqlite3_bind_int(stmt, 6, timestamp); sqlite3_bind_blob(stmt, 7, extendedData.bytes, (int)extendedData.length, 0); int result = sqlite3_step(stmt); if (result != SQLITE_DONE) { if (_errorLogsEnabled) NSLog(@"%s line:%d sqlite insert error (%d): %s", __FUNCTION__, __LINE__, result, sqlite3_errmsg(_db)); return NO; } return YES; } - (BOOL)_dbUpdateAccessTimeWithKey:(NSString *)key { NSString *sql = @"update manifest set last_access_time = ?1 where key = ?2;"; sqlite3_stmt *stmt = [self _dbPrepareStmt:sql]; if (!stmt) return NO; sqlite3_bind_int(stmt, 1, (int)time(NULL)); sqlite3_bind_text(stmt, 2, key.UTF8String, -1, NULL); int result = sqlite3_step(stmt); if (result != SQLITE_DONE) { if (_errorLogsEnabled) NSLog(@"%s line:%d sqlite update error (%d): %s", __FUNCTION__, __LINE__, result, sqlite3_errmsg(_db)); return NO; } return YES; } - (BOOL)_dbUpdateAccessTimeWithKeys:(NSArray *)keys { if (![self _dbCheck]) return NO; int t = (int)time(NULL); NSString *sql = [[NSString alloc] initWithFormat:@"update manifest set last_access_time = %d where key in (%@);", t, [self _dbJoinedKeys:keys]]; sqlite3_stmt *stmt = NULL; int result = sqlite3_prepare_v2(_db, sql.UTF8String, -1, &stmt, NULL); if (result != SQLITE_OK) { if (_errorLogsEnabled) NSLog(@"%s line:%d sqlite stmt prepare error (%d): %s", __FUNCTION__, __LINE__, result, sqlite3_errmsg(_db)); return NO; } [self _dbBindJoinedKeys:keys stmt:stmt fromIndex:1]; result = sqlite3_step(stmt); sqlite3_finalize(stmt); if (result != SQLITE_DONE) { if (_errorLogsEnabled) NSLog(@"%s line:%d sqlite update error (%d): %s", __FUNCTION__, __LINE__, result, sqlite3_errmsg(_db)); return NO; } return YES; } - (BOOL)_dbDeleteItemWithKey:(NSString *)key { NSString *sql = @"delete from manifest where key = ?1;"; sqlite3_stmt *stmt = [self _dbPrepareStmt:sql]; if (!stmt) return NO; sqlite3_bind_text(stmt, 1, key.UTF8String, -1, NULL); int result = sqlite3_step(stmt); if (result != SQLITE_DONE) { if (_errorLogsEnabled) NSLog(@"%s line:%d db delete error (%d): %s", __FUNCTION__, __LINE__, result, sqlite3_errmsg(_db)); return NO; } return YES; } - (BOOL)_dbDeleteItemWithKeys:(NSArray *)keys { if (![self _dbCheck]) return NO; NSString *sql = [[NSString alloc] initWithFormat:@"delete from manifest where key in (%@);", [self _dbJoinedKeys:keys]]; sqlite3_stmt *stmt = NULL; int result = sqlite3_prepare_v2(_db, sql.UTF8String, -1, &stmt, NULL); if (result != SQLITE_OK) { if (_errorLogsEnabled) NSLog(@"%s line:%d sqlite stmt prepare error (%d): %s", __FUNCTION__, __LINE__, result, sqlite3_errmsg(_db)); return NO; } [self _dbBindJoinedKeys:keys stmt:stmt fromIndex:1]; result = sqlite3_step(stmt); sqlite3_finalize(stmt); if (result == SQLITE_ERROR) { if (_errorLogsEnabled) NSLog(@"%s line:%d sqlite delete error (%d): %s", __FUNCTION__, __LINE__, result, sqlite3_errmsg(_db)); return NO; } return YES; } - (BOOL)_dbDeleteItemsWithSizeLargerThan:(int)size { NSString *sql = @"delete from manifest where size > ?1;"; sqlite3_stmt *stmt = [self _dbPrepareStmt:sql]; if (!stmt) return NO; sqlite3_bind_int(stmt, 1, size); int result = sqlite3_step(stmt); if (result != SQLITE_DONE) { if (_errorLogsEnabled) NSLog(@"%s line:%d sqlite delete error (%d): %s", __FUNCTION__, __LINE__, result, sqlite3_errmsg(_db)); return NO; } return YES; } - (BOOL)_dbDeleteItemsWithTimeEarlierThan:(int)time { NSString *sql = @"delete from manifest where last_access_time < ?1;"; sqlite3_stmt *stmt = [self _dbPrepareStmt:sql]; if (!stmt) return NO; sqlite3_bind_int(stmt, 1, time); int result = sqlite3_step(stmt); if (result != SQLITE_DONE) { if (_errorLogsEnabled) NSLog(@"%s line:%d sqlite delete error (%d): %s", __FUNCTION__, __LINE__, result, sqlite3_errmsg(_db)); return NO; } return YES; } - (YYKVStorageItem *)_dbGetItemFromStmt:(sqlite3_stmt *)stmt excludeInlineData:(BOOL)excludeInlineData { int i = 0; char *key = (char *)sqlite3_column_text(stmt, i++); char *filename = (char *)sqlite3_column_text(stmt, i++); int size = sqlite3_column_int(stmt, i++); const void *inline_data = excludeInlineData ? NULL : sqlite3_column_blob(stmt, i); int inline_data_bytes = excludeInlineData ? 0 : sqlite3_column_bytes(stmt, i++); int modification_time = sqlite3_column_int(stmt, i++); int last_access_time = sqlite3_column_int(stmt, i++); const void *extended_data = sqlite3_column_blob(stmt, i); int extended_data_bytes = sqlite3_column_bytes(stmt, i++); YYKVStorageItem *item = [YYKVStorageItem new]; if (key) item.key = [NSString stringWithUTF8String:key]; if (filename && *filename != 0) item.filename = [NSString stringWithUTF8String:filename]; item.size = size; if (inline_data_bytes > 0 && inline_data) item.value = [NSData dataWithBytes:inline_data length:inline_data_bytes]; item.modTime = modification_time; item.accessTime = last_access_time; if (extended_data_bytes > 0 && extended_data) item.extendedData = [NSData dataWithBytes:extended_data length:extended_data_bytes]; return item; } - (YYKVStorageItem *)_dbGetItemWithKey:(NSString *)key excludeInlineData:(BOOL)excludeInlineData { NSString *sql = excludeInlineData ? @"select key, filename, size, modification_time, last_access_time, extended_data from manifest where key = ?1;" : @"select key, filename, size, inline_data, modification_time, last_access_time, extended_data from manifest where key = ?1;"; sqlite3_stmt *stmt = [self _dbPrepareStmt:sql]; if (!stmt) return nil; sqlite3_bind_text(stmt, 1, key.UTF8String, -1, NULL); YYKVStorageItem *item = nil; int result = sqlite3_step(stmt); if (result == SQLITE_ROW) { item = [self _dbGetItemFromStmt:stmt excludeInlineData:excludeInlineData]; } else { if (result != SQLITE_DONE) { if (_errorLogsEnabled) NSLog(@"%s line:%d sqlite query error (%d): %s", __FUNCTION__, __LINE__, result, sqlite3_errmsg(_db)); } } return item; } - (NSMutableArray *)_dbGetItemWithKeys:(NSArray *)keys excludeInlineData:(BOOL)excludeInlineData { if (![self _dbCheck]) return nil; NSString *sql; if (excludeInlineData) { sql = [[NSString alloc] initWithFormat:@"select key, filename, size, modification_time, last_access_time, extended_data from manifest where key in (%@);", [self _dbJoinedKeys:keys]]; } else { sql = [[NSString alloc] initWithFormat:@"select key, filename, size, inline_data, modification_time, last_access_time, extended_data from manifest where key in (%@)", [self _dbJoinedKeys:keys]]; } sqlite3_stmt *stmt = NULL; int result = sqlite3_prepare_v2(_db, sql.UTF8String, -1, &stmt, NULL); if (result != SQLITE_OK) { if (_errorLogsEnabled) NSLog(@"%s line:%d sqlite stmt prepare error (%d): %s", __FUNCTION__, __LINE__, result, sqlite3_errmsg(_db)); return nil; } [self _dbBindJoinedKeys:keys stmt:stmt fromIndex:1]; NSMutableArray *items = [NSMutableArray new]; do { result = sqlite3_step(stmt); if (result == SQLITE_ROW) { YYKVStorageItem *item = [self _dbGetItemFromStmt:stmt excludeInlineData:excludeInlineData]; if (item) [items addObject:item]; } else if (result == SQLITE_DONE) { break; } else { if (_errorLogsEnabled) NSLog(@"%s line:%d sqlite query error (%d): %s", __FUNCTION__, __LINE__, result, sqlite3_errmsg(_db)); items = nil; break; } } while (1); sqlite3_finalize(stmt); return items; } - (NSData *)_dbGetValueWithKey:(NSString *)key { NSString *sql = @"select inline_data from manifest where key = ?1;"; sqlite3_stmt *stmt = [self _dbPrepareStmt:sql]; if (!stmt) return nil; sqlite3_bind_text(stmt, 1, key.UTF8String, -1, NULL); int result = sqlite3_step(stmt); if (result == SQLITE_ROW) { const void *inline_data = sqlite3_column_blob(stmt, 0); int inline_data_bytes = sqlite3_column_bytes(stmt, 0); if (!inline_data || inline_data_bytes <= 0) return nil; return [NSData dataWithBytes:inline_data length:inline_data_bytes]; } else { if (result != SQLITE_DONE) { if (_errorLogsEnabled) NSLog(@"%s line:%d sqlite query error (%d): %s", __FUNCTION__, __LINE__, result, sqlite3_errmsg(_db)); } return nil; } } - (NSString *)_dbGetFilenameWithKey:(NSString *)key { NSString *sql = @"select filename from manifest where key = ?1;"; sqlite3_stmt *stmt = [self _dbPrepareStmt:sql]; if (!stmt) return nil; sqlite3_bind_text(stmt, 1, key.UTF8String, -1, NULL); int result = sqlite3_step(stmt); if (result == SQLITE_ROW) { char *filename = (char *)sqlite3_column_text(stmt, 0); if (filename && *filename != 0) { return [NSString stringWithUTF8String:filename]; } } else { if (result != SQLITE_DONE) { if (_errorLogsEnabled) NSLog(@"%s line:%d sqlite query error (%d): %s", __FUNCTION__, __LINE__, result, sqlite3_errmsg(_db)); } } return nil; } - (NSMutableArray *)_dbGetFilenameWithKeys:(NSArray *)keys { if (![self _dbCheck]) return nil; NSString *sql = [[NSString alloc] initWithFormat:@"select filename from manifest where key in (%@);", [self _dbJoinedKeys:keys]]; sqlite3_stmt *stmt = NULL; int result = sqlite3_prepare_v2(_db, sql.UTF8String, -1, &stmt, NULL); if (result != SQLITE_OK) { if (_errorLogsEnabled) NSLog(@"%s line:%d sqlite stmt prepare error (%d): %s", __FUNCTION__, __LINE__, result, sqlite3_errmsg(_db)); return nil; } [self _dbBindJoinedKeys:keys stmt:stmt fromIndex:1]; NSMutableArray *filenames = [NSMutableArray new]; do { result = sqlite3_step(stmt); if (result == SQLITE_ROW) { char *filename = (char *)sqlite3_column_text(stmt, 0); if (filename && *filename != 0) { NSString *name = [NSString stringWithUTF8String:filename]; if (name) [filenames addObject:name]; } } else if (result == SQLITE_DONE) { break; } else { if (_errorLogsEnabled) NSLog(@"%s line:%d sqlite query error (%d): %s", __FUNCTION__, __LINE__, result, sqlite3_errmsg(_db)); filenames = nil; break; } } while (1); sqlite3_finalize(stmt); return filenames; } - (NSMutableArray *)_dbGetFilenamesWithSizeLargerThan:(int)size { NSString *sql = @"select filename from manifest where size > ?1 and filename is not null;"; sqlite3_stmt *stmt = [self _dbPrepareStmt:sql]; if (!stmt) return nil; sqlite3_bind_int(stmt, 1, size); NSMutableArray *filenames = [NSMutableArray new]; do { int result = sqlite3_step(stmt); if (result == SQLITE_ROW) { char *filename = (char *)sqlite3_column_text(stmt, 0); if (filename && *filename != 0) { NSString *name = [NSString stringWithUTF8String:filename]; if (name) [filenames addObject:name]; } } else if (result == SQLITE_DONE) { break; } else { if (_errorLogsEnabled) NSLog(@"%s line:%d sqlite query error (%d): %s", __FUNCTION__, __LINE__, result, sqlite3_errmsg(_db)); filenames = nil; break; } } while (1); return filenames; } - (NSMutableArray *)_dbGetFilenamesWithTimeEarlierThan:(int)time { NSString *sql = @"select filename from manifest where last_access_time < ?1 and filename is not null;"; sqlite3_stmt *stmt = [self _dbPrepareStmt:sql]; if (!stmt) return nil; sqlite3_bind_int(stmt, 1, time); NSMutableArray *filenames = [NSMutableArray new]; do { int result = sqlite3_step(stmt); if (result == SQLITE_ROW) { char *filename = (char *)sqlite3_column_text(stmt, 0); if (filename && *filename != 0) { NSString *name = [NSString stringWithUTF8String:filename]; if (name) [filenames addObject:name]; } } else if (result == SQLITE_DONE) { break; } else { if (_errorLogsEnabled) NSLog(@"%s line:%d sqlite query error (%d): %s", __FUNCTION__, __LINE__, result, sqlite3_errmsg(_db)); filenames = nil; break; } } while (1); return filenames; } - (NSMutableArray *)_dbGetItemSizeInfoOrderByTimeAscWithLimit:(int)count { NSString *sql = @"select key, filename, size from manifest order by last_access_time asc limit ?1;"; sqlite3_stmt *stmt = [self _dbPrepareStmt:sql]; if (!stmt) return nil; sqlite3_bind_int(stmt, 1, count); NSMutableArray *items = [NSMutableArray new]; do { int result = sqlite3_step(stmt); if (result == SQLITE_ROW) { char *key = (char *)sqlite3_column_text(stmt, 0); char *filename = (char *)sqlite3_column_text(stmt, 1); int size = sqlite3_column_int(stmt, 2); NSString *keyStr = key ? [NSString stringWithUTF8String:key] : nil; if (keyStr) { YYKVStorageItem *item = [YYKVStorageItem new]; item.key = key ? [NSString stringWithUTF8String:key] : nil; item.filename = filename ? [NSString stringWithUTF8String:filename] : nil; item.size = size; [items addObject:item]; } } else if (result == SQLITE_DONE) { break; } else { if (_errorLogsEnabled) NSLog(@"%s line:%d sqlite query error (%d): %s", __FUNCTION__, __LINE__, result, sqlite3_errmsg(_db)); items = nil; break; } } while (1); return items; } - (int)_dbGetItemCountWithKey:(NSString *)key { NSString *sql = @"select count(key) from manifest where key = ?1;"; sqlite3_stmt *stmt = [self _dbPrepareStmt:sql]; if (!stmt) return -1; sqlite3_bind_text(stmt, 1, key.UTF8String, -1, NULL); int result = sqlite3_step(stmt); if (result != SQLITE_ROW) { if (_errorLogsEnabled) NSLog(@"%s line:%d sqlite query error (%d): %s", __FUNCTION__, __LINE__, result, sqlite3_errmsg(_db)); return -1; } return sqlite3_column_int(stmt, 0); } - (int)_dbGetTotalItemSize { NSString *sql = @"select sum(size) from manifest;"; sqlite3_stmt *stmt = [self _dbPrepareStmt:sql]; if (!stmt) return -1; int result = sqlite3_step(stmt); if (result != SQLITE_ROW) { if (_errorLogsEnabled) NSLog(@"%s line:%d sqlite query error (%d): %s", __FUNCTION__, __LINE__, result, sqlite3_errmsg(_db)); return -1; } return sqlite3_column_int(stmt, 0); } - (int)_dbGetTotalItemCount { NSString *sql = @"select count(*) from manifest;"; sqlite3_stmt *stmt = [self _dbPrepareStmt:sql]; if (!stmt) return -1; int result = sqlite3_step(stmt); if (result != SQLITE_ROW) { if (_errorLogsEnabled) NSLog(@"%s line:%d sqlite query error (%d): %s", __FUNCTION__, __LINE__, result, sqlite3_errmsg(_db)); return -1; } return sqlite3_column_int(stmt, 0); } #pragma mark - file - (BOOL)_fileWriteWithName:(NSString *)filename data:(NSData *)data { NSString *path = [_dataPath stringByAppendingPathComponent:filename]; return [data writeToFile:path atomically:NO]; } - (NSData *)_fileReadWithName:(NSString *)filename { NSString *path = [_dataPath stringByAppendingPathComponent:filename]; NSData *data = [NSData dataWithContentsOfFile:path]; return data; } - (BOOL)_fileDeleteWithName:(NSString *)filename { NSString *path = [_dataPath stringByAppendingPathComponent:filename]; return [[NSFileManager defaultManager] removeItemAtPath:path error:NULL]; } - (BOOL)_fileMoveAllToTrash { CFUUIDRef uuidRef = CFUUIDCreate(NULL); CFStringRef uuid = CFUUIDCreateString(NULL, uuidRef); CFRelease(uuidRef); NSString *tmpPath = [_trashPath stringByAppendingPathComponent:(__bridge NSString *)(uuid)]; BOOL suc = [[NSFileManager defaultManager] moveItemAtPath:_dataPath toPath:tmpPath error:nil]; if (suc) { suc = [[NSFileManager defaultManager] createDirectoryAtPath:_dataPath withIntermediateDirectories:YES attributes:nil error:NULL]; } CFRelease(uuid); return suc; } - (void)_fileEmptyTrashInBackground { NSString *trashPath = _trashPath; dispatch_queue_t queue = _trashQueue; dispatch_async(queue, ^{ NSFileManager *manager = [NSFileManager new]; NSArray *directoryContents = [manager contentsOfDirectoryAtPath:trashPath error:NULL]; for (NSString *path in directoryContents) { NSString *fullPath = [trashPath stringByAppendingPathComponent:path]; [manager removeItemAtPath:fullPath error:NULL]; } }); } #pragma mark - private /** Delete all files and empty in background. Make sure the db is closed. */ - (void)_reset { [[NSFileManager defaultManager] removeItemAtPath:[_path stringByAppendingPathComponent:kDBFileName] error:nil]; [[NSFileManager defaultManager] removeItemAtPath:[_path stringByAppendingPathComponent:kDBShmFileName] error:nil]; [[NSFileManager defaultManager] removeItemAtPath:[_path stringByAppendingPathComponent:kDBWalFileName] error:nil]; [self _fileMoveAllToTrash]; [self _fileEmptyTrashInBackground]; } #pragma mark - public - (instancetype)init { @throw [NSException exceptionWithName:@"YYKVStorage init error" reason:@"Please use the designated initializer and pass the 'path' and 'type'." userInfo:nil]; return [self initWithPath:@"" type:YYKVStorageTypeFile]; } - (instancetype)initWithPath:(NSString *)path type:(YYKVStorageType)type { if (path.length == 0 || path.length > kPathLengthMax) { NSLog(@"YYKVStorage init error: invalid path: [%@].", path); return nil; } if (type > YYKVStorageTypeMixed) { NSLog(@"YYKVStorage init error: invalid type: %lu.", (unsigned long)type); return nil; } self = [super init]; _path = path.copy; _type = type; _dataPath = [path stringByAppendingPathComponent:kDataDirectoryName]; _trashPath = [path stringByAppendingPathComponent:kTrashDirectoryName]; _trashQueue = dispatch_queue_create("com.ibireme.cache.disk.trash", DISPATCH_QUEUE_SERIAL); _dbPath = [path stringByAppendingPathComponent:kDBFileName]; _errorLogsEnabled = YES; NSError *error = nil; if (![[NSFileManager defaultManager] createDirectoryAtPath:path withIntermediateDirectories:YES attributes:nil error:&error] || ![[NSFileManager defaultManager] createDirectoryAtPath:[path stringByAppendingPathComponent:kDataDirectoryName] withIntermediateDirectories:YES attributes:nil error:&error] || ![[NSFileManager defaultManager] createDirectoryAtPath:[path stringByAppendingPathComponent:kTrashDirectoryName] withIntermediateDirectories:YES attributes:nil error:&error]) { NSLog(@"YYKVStorage init error:%@", error); return nil; } if (![self _dbOpen] || ![self _dbInitialize]) { // db file may broken... [self _dbClose]; [self _reset]; // rebuild if (![self _dbOpen] || ![self _dbInitialize]) { [self _dbClose]; NSLog(@"YYKVStorage init error: fail to open sqlite db."); return nil; } } [self _fileEmptyTrashInBackground]; // empty the trash if failed at last time return self; } - (void)dealloc { UIBackgroundTaskIdentifier taskID = [[UIApplication sharedExtensionApplication] beginBackgroundTaskWithExpirationHandler:^{}]; [self _dbClose]; if (taskID != UIBackgroundTaskInvalid) { [[UIApplication sharedExtensionApplication] endBackgroundTask:taskID]; } } - (BOOL)saveItem:(YYKVStorageItem *)item { return [self saveItemWithKey:item.key value:item.value filename:item.filename extendedData:item.extendedData]; } - (BOOL)saveItemWithKey:(NSString *)key value:(NSData *)value { return [self saveItemWithKey:key value:value filename:nil extendedData:nil]; } - (BOOL)saveItemWithKey:(NSString *)key value:(NSData *)value filename:(NSString *)filename extendedData:(NSData *)extendedData { if (key.length == 0 || value.length == 0) return NO; if (_type == YYKVStorageTypeFile && filename.length == 0) { return NO; } if (filename.length) { if (![self _fileWriteWithName:filename data:value]) { return NO; } if (![self _dbSaveWithKey:key value:value fileName:filename extendedData:extendedData]) { [self _fileDeleteWithName:filename]; return NO; } return YES; } else { if (_type != YYKVStorageTypeSQLite) { NSString *filename = [self _dbGetFilenameWithKey:key]; if (filename) { [self _fileDeleteWithName:filename]; } } return [self _dbSaveWithKey:key value:value fileName:nil extendedData:extendedData]; } } - (BOOL)removeItemForKey:(NSString *)key { if (key.length == 0) return NO; switch (_type) { case YYKVStorageTypeSQLite: { return [self _dbDeleteItemWithKey:key]; } break; case YYKVStorageTypeFile: case YYKVStorageTypeMixed: { NSString *filename = [self _dbGetFilenameWithKey:key]; if (filename) { [self _fileDeleteWithName:filename]; } return [self _dbDeleteItemWithKey:key]; } break; default: return NO; } } - (BOOL)removeItemForKeys:(NSArray *)keys { if (keys.count == 0) return NO; switch (_type) { case YYKVStorageTypeSQLite: { return [self _dbDeleteItemWithKeys:keys]; } break; case YYKVStorageTypeFile: case YYKVStorageTypeMixed: { NSArray *filenames = [self _dbGetFilenameWithKeys:keys]; for (NSString *filename in filenames) { [self _fileDeleteWithName:filename]; } return [self _dbDeleteItemWithKeys:keys]; } break; default: return NO; } } - (BOOL)removeItemsLargerThanSize:(int)size { if (size == INT_MAX) return YES; if (size <= 0) return [self removeAllItems]; switch (_type) { case YYKVStorageTypeSQLite: { if ([self _dbDeleteItemsWithSizeLargerThan:size]) { [self _dbCheckpoint]; return YES; } } break; case YYKVStorageTypeFile: case YYKVStorageTypeMixed: { NSArray *filenames = [self _dbGetFilenamesWithSizeLargerThan:size]; for (NSString *name in filenames) { [self _fileDeleteWithName:name]; } if ([self _dbDeleteItemsWithSizeLargerThan:size]) { [self _dbCheckpoint]; return YES; } } break; } return NO; } - (BOOL)removeItemsEarlierThanTime:(int)time { if (time <= 0) return YES; if (time == INT_MAX) return [self removeAllItems]; switch (_type) { case YYKVStorageTypeSQLite: { if ([self _dbDeleteItemsWithTimeEarlierThan:time]) { [self _dbCheckpoint]; return YES; } } break; case YYKVStorageTypeFile: case YYKVStorageTypeMixed: { NSArray *filenames = [self _dbGetFilenamesWithTimeEarlierThan:time]; for (NSString *name in filenames) { [self _fileDeleteWithName:name]; } if ([self _dbDeleteItemsWithTimeEarlierThan:time]) { [self _dbCheckpoint]; return YES; } } break; } return NO; } - (BOOL)removeItemsToFitSize:(int)maxSize { if (maxSize == INT_MAX) return YES; if (maxSize <= 0) return [self removeAllItems]; int total = [self _dbGetTotalItemSize]; if (total < 0) return NO; if (total <= maxSize) return YES; NSArray *items = nil; BOOL suc = NO; do { int perCount = 16; items = [self _dbGetItemSizeInfoOrderByTimeAscWithLimit:perCount]; for (YYKVStorageItem *item in items) { if (total > maxSize) { if (item.filename) { [self _fileDeleteWithName:item.filename]; } suc = [self _dbDeleteItemWithKey:item.key]; total -= item.size; } else { break; } if (!suc) break; } } while (total > maxSize && items.count > 0 && suc); if (suc) [self _dbCheckpoint]; return suc; } - (BOOL)removeItemsToFitCount:(int)maxCount { if (maxCount == INT_MAX) return YES; if (maxCount <= 0) return [self removeAllItems]; int total = [self _dbGetTotalItemCount]; if (total < 0) return NO; if (total <= maxCount) return YES; NSArray *items = nil; BOOL suc = NO; do { int perCount = 16; items = [self _dbGetItemSizeInfoOrderByTimeAscWithLimit:perCount]; for (YYKVStorageItem *item in items) { if (total > maxCount) { if (item.filename) { [self _fileDeleteWithName:item.filename]; } suc = [self _dbDeleteItemWithKey:item.key]; total--; } else { break; } if (!suc) break; } } while (total > maxCount && items.count > 0 && suc); if (suc) [self _dbCheckpoint]; return suc; } - (BOOL)removeAllItems { if (![self _dbClose]) return NO; [self _reset]; if (![self _dbOpen]) return NO; if (![self _dbInitialize]) return NO; return YES; } - (void)removeAllItemsWithProgressBlock:(void(^)(int removedCount, int totalCount))progress endBlock:(void(^)(BOOL error))end { int total = [self _dbGetTotalItemCount]; if (total <= 0) { if (end) end(total < 0); } else { int left = total; int perCount = 32; NSArray *items = nil; BOOL suc = NO; do { items = [self _dbGetItemSizeInfoOrderByTimeAscWithLimit:perCount]; for (YYKVStorageItem *item in items) { if (left > 0) { if (item.filename) { [self _fileDeleteWithName:item.filename]; } suc = [self _dbDeleteItemWithKey:item.key]; left--; } else { break; } if (!suc) break; } if (progress) progress(total - left, total); } while (left > 0 && items.count > 0 && suc); if (suc) [self _dbCheckpoint]; if (end) end(!suc); } } - (YYKVStorageItem *)getItemForKey:(NSString *)key { if (key.length == 0) return nil; YYKVStorageItem *item = [self _dbGetItemWithKey:key excludeInlineData:NO]; if (item) { [self _dbUpdateAccessTimeWithKey:key]; if (item.filename) { item.value = [self _fileReadWithName:item.filename]; if (!item.value) { [self _dbDeleteItemWithKey:key]; item = nil; } } } return item; } - (YYKVStorageItem *)getItemInfoForKey:(NSString *)key { if (key.length == 0) return nil; YYKVStorageItem *item = [self _dbGetItemWithKey:key excludeInlineData:YES]; return item; } - (NSData *)getItemValueForKey:(NSString *)key { if (key.length == 0) return nil; NSData *value = nil; switch (_type) { case YYKVStorageTypeFile: { NSString *filename = [self _dbGetFilenameWithKey:key]; if (filename) { value = [self _fileReadWithName:filename]; if (!value) { [self _dbDeleteItemWithKey:key]; value = nil; } } } break; case YYKVStorageTypeSQLite: { value = [self _dbGetValueWithKey:key]; } break; case YYKVStorageTypeMixed: { NSString *filename = [self _dbGetFilenameWithKey:key]; if (filename) { value = [self _fileReadWithName:filename]; if (!value) { [self _dbDeleteItemWithKey:key]; value = nil; } } else { value = [self _dbGetValueWithKey:key]; } } break; } if (value) { [self _dbUpdateAccessTimeWithKey:key]; } return value; } - (NSArray *)getItemForKeys:(NSArray *)keys { if (keys.count == 0) return nil; NSMutableArray *items = [self _dbGetItemWithKeys:keys excludeInlineData:NO]; if (_type != YYKVStorageTypeSQLite) { for (NSInteger i = 0, max = items.count; i < max; i++) { YYKVStorageItem *item = items[i]; if (item.filename) { item.value = [self _fileReadWithName:item.filename]; if (!item.value) { if (item.key) [self _dbDeleteItemWithKey:item.key]; [items removeObjectAtIndex:i]; i--; max--; } } } } if (items.count > 0) { [self _dbUpdateAccessTimeWithKeys:keys]; } return items.count ? items : nil; } - (NSArray *)getItemInfoForKeys:(NSArray *)keys { if (keys.count == 0) return nil; return [self _dbGetItemWithKeys:keys excludeInlineData:YES]; } - (NSDictionary *)getItemValueForKeys:(NSArray *)keys { NSMutableArray *items = (NSMutableArray *)[self getItemForKeys:keys]; NSMutableDictionary *kv = [NSMutableDictionary new]; for (YYKVStorageItem *item in items) { if (item.key && item.value) { [kv setObject:item.value forKey:item.key]; } } return kv.count ? kv : nil; } - (BOOL)itemExistsForKey:(NSString *)key { if (key.length == 0) return NO; return [self _dbGetItemCountWithKey:key] > 0; } - (int)getItemsCount { return [self _dbGetTotalItemCount]; } - (int)getItemsSize { return [self _dbGetTotalItemSize]; } @end