diff --git a/Tests/FMDatabaseOnlineBackupTests.m b/Tests/FMDatabaseOnlineBackupTests.m new file mode 100644 index 00000000..c8820bac --- /dev/null +++ b/Tests/FMDatabaseOnlineBackupTests.m @@ -0,0 +1,94 @@ +// +// FMDatabaseOnlineBackupTests.m +// fmdb +// +// Created by Mark Pustjens on 23/09/15. +// (c) Angelbird Technologies GmbH +// +// + +#import +#import + +@interface FMDatabaseOnlineBackupTests : FMDBTempDBTests + +@end + +@implementation FMDatabaseOnlineBackupTests + ++ (void)populateDatabase:(FMDatabase *)db +{ + [db executeUpdate:@"create table test (a text, b text, c integer, d double, e double)"]; + + [db beginTransaction]; + int i = 0; + while (i++ < 2000) { + [db executeUpdate:@"insert into test (a, b, c, d, e) values (?, ?, ?, ?, ?)" , + @"hi'", // look! I put in a ', and I'm not escaping it! + [NSString stringWithFormat:@"number %d", i], + [NSNumber numberWithInt:i], + [NSDate date], + [NSNumber numberWithFloat:2.2f]]; + } + [db commit]; +} + +- (void)setUp { + [super setUp]; + // Put setup code here. This method is called before the invocation of each test method in the class. +} + +- (void)tearDown { + // Put teardown code here. This method is called after the invocation of each test method in the class. + [super tearDown]; +} + +- (void)testBackupDatabase +{ + NSString *backupPath = @"/tmp/tmp.db.bck"; + + // perform the backup + XCTAssertTrue ([self.db backupTo:backupPath withKey:nil andProgressBlock:^(int pagesRemaining, int pageCount) { + // No need to show progress. + }], @"Should have succeeded"); + + // check if the backup file exists + NSFileManager *fileManager = [NSFileManager defaultManager]; + XCTAssertTrue([fileManager fileExistsAtPath:backupPath], + @"Backup db file should exist"); + + // test if the bakcup is ok + FMDatabase *bck = [FMDatabase databaseWithPath:backupPath]; + XCTAssertTrue([bck open], @"Should pass"); + XCTAssert([bck executeQuery:@"select * from test"] != nil, @"Should pass"); + XCTAssertTrue([bck close], @"Should pass"); + + // delete backup + [fileManager removeItemAtPath:backupPath error:NULL]; +} + +- (void)testBackupDatabaseEncrypted +{ + NSString *backupPath = @"/tmp/tmp.edb.bck"; + + // perform the backup + XCTAssertTrue ([self.db backupTo:backupPath withKey:@"passw0rd" andProgressBlock:^(int pagesRemaining, int pageCount) { + // No need to show progress. + }], @"Should have succeeded"); + + // check if the backup file exists + NSFileManager *fileManager = [NSFileManager defaultManager]; + XCTAssertTrue([fileManager fileExistsAtPath:backupPath], + @"Backup db file should exist"); + + // test if the backup is ok + FMDatabase *bck = [FMDatabase databaseWithPath:backupPath]; + XCTAssertTrue([bck open], @"Should pass"); + XCTAssert([bck executeQuery:@"select * from test"] != nil, @"Should pass"); + XCTAssertTrue([bck close], @"Should pass"); + + // delete backup + [fileManager removeItemAtPath:backupPath error:NULL]; +} + +@end diff --git a/fmdb.xcodeproj/project.pbxproj b/fmdb.xcodeproj/project.pbxproj index 37772d80..a41f4274 100644 --- a/fmdb.xcodeproj/project.pbxproj +++ b/fmdb.xcodeproj/project.pbxproj @@ -15,6 +15,7 @@ 621721B61892BFE30006691F /* FMDatabasePool.m in Sources */ = {isa = PBXBuildFile; fileRef = CC9E4EB813B31188005F9210 /* FMDatabasePool.m */; }; 6290CBB7188FE836009790F8 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6290CBB6188FE836009790F8 /* Foundation.framework */; }; 67CB1E3019AD27D000A3CA7F /* FMDatabaseFTS3Tests.m in Sources */ = {isa = PBXBuildFile; fileRef = 67CB1E2F19AD27D000A3CA7F /* FMDatabaseFTS3Tests.m */; }; + 8081C06C1BB319E200CA7C73 /* FMDatabaseOnlineBackupTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 8081C06B1BB319E200CA7C73 /* FMDatabaseOnlineBackupTests.m */; }; 8314AF3318CD73D600EC0E25 /* FMDB.h in Headers */ = {isa = PBXBuildFile; fileRef = 8314AF3218CD73D600EC0E25 /* FMDB.h */; }; 8DD76F9C0486AA7600D96B5E /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 08FB779EFE84155DC02AAC07 /* Foundation.framework */; }; 8DD76F9F0486AA7600D96B5E /* fmdb.1 in CopyFiles */ = {isa = PBXBuildFile; fileRef = C6859EA3029092ED04C91782 /* fmdb.1 */; }; @@ -95,6 +96,7 @@ 6290CBB6188FE836009790F8 /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; }; 6290CBC6188FE837009790F8 /* UIKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UIKit.framework; path = Library/Frameworks/UIKit.framework; sourceTree = DEVELOPER_DIR; }; 67CB1E2F19AD27D000A3CA7F /* FMDatabaseFTS3Tests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FMDatabaseFTS3Tests.m; sourceTree = ""; }; + 8081C06B1BB319E200CA7C73 /* FMDatabaseOnlineBackupTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FMDatabaseOnlineBackupTests.m; sourceTree = ""; }; 8314AF3218CD73D600EC0E25 /* FMDB.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = FMDB.h; path = src/fmdb/FMDB.h; sourceTree = ""; }; 831DE6FD175B7C9C001F7317 /* README.markdown */ = {isa = PBXFileReference; lastKnownFileType = text; path = README.markdown; sourceTree = ""; }; 832F502419EC4C6B0087DCBF /* FMDatabaseVariadic.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = FMDatabaseVariadic.swift; path = "src/extra/Swift extensions/FMDatabaseVariadic.swift"; sourceTree = ""; }; @@ -308,6 +310,7 @@ BF940F5A18417D490001E077 /* FMDBTempDBTests.h */, BF940F5B18417D490001E077 /* FMDBTempDBTests.m */, 3354379B19E71096005661F3 /* FMResultSetTests.m */, + 8081C06B1BB319E200CA7C73 /* FMDatabaseOnlineBackupTests.m */, BF5D041B18416BB2008C5AA9 /* Supporting Files */, ); path = Tests; @@ -512,6 +515,7 @@ files = ( BFC152B118417F0D00605DF7 /* FMDatabaseAdditions.m in Sources */, CCA66A3019C0CB1900EFDAC1 /* FMTokenizers.m in Sources */, + 8081C06C1BB319E200CA7C73 /* FMDatabaseOnlineBackupTests.m in Sources */, BF940F5C18417D490001E077 /* FMDBTempDBTests.m in Sources */, BF940F5E18417DEA0001E077 /* FMDatabaseAdditionsTests.m in Sources */, 3354379C19E71096005661F3 /* FMResultSetTests.m in Sources */, diff --git a/src/fmdb/FMDatabase.h b/src/fmdb/FMDatabase.h index 1aec77d2..0246e357 100644 --- a/src/fmdb/FMDatabase.h +++ b/src/fmdb/FMDatabase.h @@ -859,6 +859,26 @@ typedef int(^FMDBExecuteStatementsCallbackBlock)(NSDictionary *resultsDictionary #endif +///---------------------------- +/// @name Online Backup +///---------------------------- + +/** Perform a live backup of the database to a new file + + @param path Path of the file to write the backup to + @param key The encryption key to use for the target database. + @param block Block to call with progress updates + + @return@return `YES` if successful, `NO` on error. + + @see [Using the SQLite Online Backup API()](http://sqlite.org/backup.html) + + */ + +- (BOOL)backupTo:(NSString*)aPath + withKey:(NSString*)key +andProgressBlock:(void (^)(int pagesRemaining, int pageCount))progressBlock; + ///---------------------------- /// @name SQLite library status ///---------------------------- diff --git a/src/fmdb/FMDatabase.m b/src/fmdb/FMDatabase.m index 9e1b4d46..6f13d50c 100644 --- a/src/fmdb/FMDatabase.m +++ b/src/fmdb/FMDatabase.m @@ -1330,6 +1330,82 @@ - (NSError*)inSavePoint:(void (^)(BOOL *rollback))block { #endif +#pragma mark Online backup + +- (BOOL)backupTo:(NSString*)aPath + withKey:(NSString*)key +andProgressBlock:(void (^)(int pagesRemaining, int pageCount))progressBlock +{ + NSParameterAssert(aPath); + NSParameterAssert(progressBlock); + + if (![self databaseExists]) { + return NO; + } + + if (_isExecutingStatement) { + [self warnInUse]; + return NO; + } + + _isExecutingStatement = YES; + + int err = 0; + sqlite3 *pFile = NULL; + sqlite3_backup *pBackup = NULL; + + if (_traceExecution) { + NSLog(@"%@ backupTo: %@", self, aPath); + } + + err = sqlite3_open([aPath fileSystemRepresentation], &pFile); + if (err != SQLITE_OK) { + goto backupTowithProgressBlockDone; + } +#ifdef SQLITE_HAS_CODEC + if (key) { + NSData *keyData = [NSData dataWithBytes:[key UTF8String] length:(NSUInteger)strlen([key UTF8String])]; + err = sqlite3_key(pFile, [keyData bytes], (int)[keyData length]); + if (err != SQLITE_OK) { + goto backupTowithProgressBlockDone; + } + } +#endif + + /* Open the backup object to accomplish the backup. */ + pBackup = sqlite3_backup_init(pFile, "main", _db, "main"); + if (!pBackup) { + goto backupTowithProgressBlockDone; + } + + do { + err = sqlite3_backup_step(pBackup, 10); //TODO optimize page count + progressBlock(sqlite3_backup_remaining(pBackup), sqlite3_backup_pagecount(pBackup)); + + //if (err == SQLITE_OK || err == SQLITE_BUSY || err == SQLITE_LOCKED) { + // sqlite3_sleep(5); + //} + } while (err == SQLITE_OK || err == SQLITE_BUSY || err == SQLITE_LOCKED); + + /* Release resources allocated by backup_init(). */ + sqlite3_backup_finish(pBackup); + + +backupTowithProgressBlockDone: + + err = sqlite3_errcode(pFile); + if (err != SQLITE_OK) { + NSString *msg = [NSString stringWithUTF8String:sqlite3_errmsg(pFile)]; + NSLog(@"error performing backup: %d \"%@\"", err, msg); + } + + _isExecutingStatement = NO; + + sqlite3_close (pFile); + + return (err == SQLITE_OK); +} + #pragma mark Cache statements - (BOOL)shouldCacheStatements {