This repository has been archived on 2024-07-22. You can view files and clone it, but cannot push or open issues or pull requests.
Files
Doric/Example/Pods/YYCache/YYCache/YYKVStorage.m
2019-12-04 13:29:26 +08:00

1070 lines
38 KiB
Objective-C

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