New file |
| | |
| | | // |
| | | // YYKVStorage.m |
| | | // YYCache <https://github.com/ibireme/YYCache> |
| | | // |
| | | // 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 <UIKit/UIKit.h> |
| | | #import <time.h> |
| | | |
| | | #if __has_include(<sqlite3.h>) |
| | | #import <sqlite3.h> |
| | | #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); |
| | | */ |
| | | |
| | | /// Returns nil in App Extension. |
| | | static UIApplication *_YYSharedApplication() { |
| | | static BOOL isAppExtension = NO; |
| | | static dispatch_once_t onceToken; |
| | | dispatch_once(&onceToken, ^{ |
| | | Class cls = NSClassFromString(@"UIApplication"); |
| | | if(!cls || ![cls respondsToSelector:@selector(sharedApplication)]) isAppExtension = YES; |
| | | if ([[[NSBundle mainBundle] bundlePath] hasSuffix:@".appex"]) isAppExtension = YES; |
| | | }); |
| | | #pragma clang diagnostic push |
| | | #pragma clang diagnostic ignored "-Wundeclared-selector" |
| | | return isAppExtension ? nil : [UIApplication performSelector:@selector(sharedApplication)]; |
| | | #pragma clang diagnostic pop |
| | | } |
| | | |
| | | |
| | | @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 stringWithFormat:@"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 stringWithFormat:@"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 stringWithFormat:@"select key, filename, size, modification_time, last_access_time, extended_data from manifest where key in (%@);", [self _dbJoinedKeys:keys]]; |
| | | } else { |
| | | sql = [NSString stringWithFormat:@"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 stringWithFormat:@"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 = [_YYSharedApplication() beginBackgroundTaskWithExpirationHandler:^{}]; |
| | | [self _dbClose]; |
| | | if (taskID != UIBackgroundTaskInvalid) { |
| | | [_YYSharedApplication() 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 |