|
@@ -19,6 +19,9 @@
|
|
|
#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";
|
|
@@ -26,7 +29,19 @@ 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,
|
|
@@ -41,6 +56,22 @@ static NSString *const kTrashDirectoryName = @"trash";
|
|
|
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
|
|
|
|
|
@@ -54,49 +85,40 @@ static NSString *const kTrashDirectoryName = @"trash";
|
|
|
|
|
|
sqlite3 *_db;
|
|
|
CFMutableDictionaryRef _dbStmtCache;
|
|
|
-
|
|
|
- BOOL _invalidated; ///< If YES, then the db should not open again, all read/write should be ignored.
|
|
|
- BOOL _dbIsClosing; ///< If YES, then the db is during closing.
|
|
|
+ NSTimeInterval _dbLastOpenErrorTime;
|
|
|
+ NSUInteger _dbOpenErrorCount;
|
|
|
}
|
|
|
|
|
|
|
|
|
#pragma mark - db
|
|
|
|
|
|
- (BOOL)_dbOpen {
|
|
|
- BOOL shouldOpen = YES;
|
|
|
- if (_invalidated) {
|
|
|
- shouldOpen = NO;
|
|
|
- } else if (_dbIsClosing) {
|
|
|
- shouldOpen = NO;
|
|
|
- } else if (_db){
|
|
|
- shouldOpen = NO;
|
|
|
- }
|
|
|
- if (!shouldOpen) return YES;
|
|
|
+ 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 {
|
|
|
- NSLog(@"%s line:%d sqlite open failed (%d).", __FUNCTION__, __LINE__, result);
|
|
|
+ _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 {
|
|
|
- BOOL needClose = YES;
|
|
|
- if (!_db) {
|
|
|
- needClose = NO;
|
|
|
- } else if (_invalidated) {
|
|
|
- needClose = NO;
|
|
|
- } else if (_dbIsClosing) {
|
|
|
- needClose = NO;
|
|
|
- } else {
|
|
|
- _dbIsClosing = YES;
|
|
|
- }
|
|
|
- if (!needClose) return YES;
|
|
|
+ if (!_db) return YES;
|
|
|
|
|
|
int result = 0;
|
|
|
BOOL retry = NO;
|
|
@@ -118,16 +140,25 @@ static NSString *const kTrashDirectoryName = @"trash";
|
|
|
}
|
|
|
}
|
|
|
} else if (result != SQLITE_OK) {
|
|
|
- NSLog(@"%s line:%d sqlite close failed (%d).", __FUNCTION__, __LINE__, result);
|
|
|
+ if (_errorLogsEnabled) {
|
|
|
+ NSLog(@"%s line:%d sqlite close failed (%d).", __FUNCTION__, __LINE__, result);
|
|
|
+ }
|
|
|
}
|
|
|
} while (retry);
|
|
|
_db = NULL;
|
|
|
- _dbIsClosing = NO;
|
|
|
return YES;
|
|
|
}
|
|
|
|
|
|
-- (BOOL)_dbIsReady {
|
|
|
- return (_db && !_dbIsClosing && !_invalidated);
|
|
|
+- (BOOL)_dbCheck {
|
|
|
+ if (!_db) {
|
|
|
+ if (_dbOpenErrorCount < kMaxErrorRetryCount &&
|
|
|
+ CACurrentMediaTime() - _dbLastOpenErrorTime > kMinRetryTimeInterval) {
|
|
|
+ return [self _dbOpen] && [self _dbInitialize];
|
|
|
+ } else {
|
|
|
+ return NO;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return YES;
|
|
|
}
|
|
|
|
|
|
- (BOOL)_dbInitialize {
|
|
@@ -136,14 +167,14 @@ static NSString *const kTrashDirectoryName = @"trash";
|
|
|
}
|
|
|
|
|
|
- (void)_dbCheckpoint {
|
|
|
- if (![self _dbIsReady]) return;
|
|
|
+ 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 _dbIsReady]) return NO;
|
|
|
+ if (![self _dbCheck]) return NO;
|
|
|
|
|
|
char *error = NULL;
|
|
|
int result = sqlite3_exec(_db, sql.UTF8String, NULL, NULL, &error);
|
|
@@ -156,7 +187,7 @@ static NSString *const kTrashDirectoryName = @"trash";
|
|
|
}
|
|
|
|
|
|
- (sqlite3_stmt *)_dbPrepareStmt:(NSString *)sql {
|
|
|
- if (![self _dbIsReady] || sql.length == 0 || !_dbStmtCache) return NULL;
|
|
|
+ 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);
|
|
@@ -230,7 +261,7 @@ static NSString *const kTrashDirectoryName = @"trash";
|
|
|
}
|
|
|
|
|
|
- (BOOL)_dbUpdateAccessTimeWithKeys:(NSArray *)keys {
|
|
|
- if (![self _dbIsReady]) return NO;
|
|
|
+ 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]];
|
|
|
|
|
@@ -266,7 +297,7 @@ static NSString *const kTrashDirectoryName = @"trash";
|
|
|
}
|
|
|
|
|
|
- (BOOL)_dbDeleteItemWithKeys:(NSArray *)keys {
|
|
|
- if (![self _dbIsReady]) return NO;
|
|
|
+ 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);
|
|
@@ -353,7 +384,7 @@ static NSString *const kTrashDirectoryName = @"trash";
|
|
|
}
|
|
|
|
|
|
- (NSMutableArray *)_dbGetItemWithKeys:(NSArray *)keys excludeInlineData:(BOOL)excludeInlineData {
|
|
|
- if (![self _dbIsReady]) return nil;
|
|
|
+ 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]];
|
|
@@ -427,7 +458,7 @@ static NSString *const kTrashDirectoryName = @"trash";
|
|
|
}
|
|
|
|
|
|
- (NSMutableArray *)_dbGetFilenameWithKeys:(NSArray *)keys {
|
|
|
- if (![self _dbIsReady]) return nil;
|
|
|
+ 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);
|
|
@@ -510,8 +541,8 @@ static NSString *const kTrashDirectoryName = @"trash";
|
|
|
return filenames;
|
|
|
}
|
|
|
|
|
|
-- (NSMutableArray *)_dbGetItemSizeInfoOrderByTimeDescWithLimit:(int)count {
|
|
|
- NSString *sql = @"select key, filename, size from manifest order by last_access_time desc limit ?1;";
|
|
|
+- (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);
|
|
@@ -580,26 +611,22 @@ static NSString *const kTrashDirectoryName = @"trash";
|
|
|
#pragma mark - file
|
|
|
|
|
|
- (BOOL)_fileWriteWithName:(NSString *)filename data:(NSData *)data {
|
|
|
- if (_invalidated) return NO;
|
|
|
NSString *path = [_dataPath stringByAppendingPathComponent:filename];
|
|
|
return [data writeToFile:path atomically:NO];
|
|
|
}
|
|
|
|
|
|
- (NSData *)_fileReadWithName:(NSString *)filename {
|
|
|
- if (_invalidated) return nil;
|
|
|
NSString *path = [_dataPath stringByAppendingPathComponent:filename];
|
|
|
NSData *data = [NSData dataWithContentsOfFile:path];
|
|
|
return data;
|
|
|
}
|
|
|
|
|
|
- (BOOL)_fileDeleteWithName:(NSString *)filename {
|
|
|
- if (_invalidated) return NO;
|
|
|
NSString *path = [_dataPath stringByAppendingPathComponent:filename];
|
|
|
return [[NSFileManager defaultManager] removeItemAtPath:path error:NULL];
|
|
|
}
|
|
|
|
|
|
- (BOOL)_fileMoveAllToTrash {
|
|
|
- if (_invalidated) return NO;
|
|
|
CFUUIDRef uuidRef = CFUUIDCreate(NULL);
|
|
|
CFStringRef uuid = CFUUIDCreateString(NULL, uuidRef);
|
|
|
CFRelease(uuidRef);
|
|
@@ -613,7 +640,6 @@ static NSString *const kTrashDirectoryName = @"trash";
|
|
|
}
|
|
|
|
|
|
- (void)_fileEmptyTrashInBackground {
|
|
|
- if (_invalidated) return;
|
|
|
NSString *trashPath = _trashPath;
|
|
|
dispatch_queue_t queue = _trashQueue;
|
|
|
dispatch_async(queue, ^{
|
|
@@ -641,10 +667,6 @@ static NSString *const kTrashDirectoryName = @"trash";
|
|
|
[self _fileEmptyTrashInBackground];
|
|
|
}
|
|
|
|
|
|
-- (void)_appWillBeTerminated {
|
|
|
- _invalidated = YES;
|
|
|
-}
|
|
|
-
|
|
|
#pragma mark - public
|
|
|
|
|
|
- (instancetype)init {
|
|
@@ -698,13 +720,15 @@ static NSString *const kTrashDirectoryName = @"trash";
|
|
|
return nil;
|
|
|
}
|
|
|
[self _fileEmptyTrashInBackground]; // empty the trash if failed at last time
|
|
|
- [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(_appWillBeTerminated) name:UIApplicationWillTerminateNotification object:nil];
|
|
|
return self;
|
|
|
}
|
|
|
|
|
|
- (void)dealloc {
|
|
|
- [[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationWillTerminateNotification object:nil];
|
|
|
+ UIBackgroundTaskIdentifier taskID = [_YYSharedApplication() beginBackgroundTaskWithExpirationHandler:^{}];
|
|
|
[self _dbClose];
|
|
|
+ if (taskID != UIBackgroundTaskInvalid) {
|
|
|
+ [_YYSharedApplication() endBackgroundTask:taskID];
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
- (BOOL)saveItem:(YYKVStorageItem *)item {
|
|
@@ -841,7 +865,7 @@ static NSString *const kTrashDirectoryName = @"trash";
|
|
|
BOOL suc = NO;
|
|
|
do {
|
|
|
int perCount = 16;
|
|
|
- items = [self _dbGetItemSizeInfoOrderByTimeDescWithLimit:perCount];
|
|
|
+ items = [self _dbGetItemSizeInfoOrderByTimeAscWithLimit:perCount];
|
|
|
for (YYKVStorageItem *item in items) {
|
|
|
if (total > maxSize) {
|
|
|
if (item.filename) {
|
|
@@ -871,7 +895,7 @@ static NSString *const kTrashDirectoryName = @"trash";
|
|
|
BOOL suc = NO;
|
|
|
do {
|
|
|
int perCount = 16;
|
|
|
- items = [self _dbGetItemSizeInfoOrderByTimeDescWithLimit:perCount];
|
|
|
+ items = [self _dbGetItemSizeInfoOrderByTimeAscWithLimit:perCount];
|
|
|
for (YYKVStorageItem *item in items) {
|
|
|
if (total > maxCount) {
|
|
|
if (item.filename) {
|
|
@@ -909,7 +933,7 @@ static NSString *const kTrashDirectoryName = @"trash";
|
|
|
NSArray *items = nil;
|
|
|
BOOL suc = NO;
|
|
|
do {
|
|
|
- items = [self _dbGetItemSizeInfoOrderByTimeDescWithLimit:perCount];
|
|
|
+ items = [self _dbGetItemSizeInfoOrderByTimeAscWithLimit:perCount];
|
|
|
for (YYKVStorageItem *item in items) {
|
|
|
if (left > 0) {
|
|
|
if (item.filename) {
|