//
|
// 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
|