DatabaseManager.m 34 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726
  1. // _____ _
  2. // |_ _| |_ _ _ ___ ___ _ __ __ _
  3. // | | | ' \| '_/ -_) -_) ' \/ _` |_
  4. // |_| |_||_|_| \___\___|_|_|_\__,_(_)
  5. //
  6. // Threema iOS Client
  7. // Copyright (c) 2015-2020 Threema GmbH
  8. //
  9. // This program is free software: you can redistribute it and/or modify
  10. // it under the terms of the GNU Affero General Public License, version 3,
  11. // as published by the Free Software Foundation.
  12. //
  13. // This program is distributed in the hope that it will be useful,
  14. // but WITHOUT ANY WARRANTY; without even the implied warranty of
  15. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  16. // GNU Affero General Public License for more details.
  17. //
  18. // You should have received a copy of the GNU Affero General Public License
  19. // along with this program. If not, see <https://www.gnu.org/licenses/>.
  20. #import "DatabaseManager.h"
  21. #import "ErrorHandler.h"
  22. #import "DocumentManager.h"
  23. #import "AppGroup.h"
  24. #import "BundleUtil.h"
  25. #import "UserSettings.h"
  26. #import "ValidationLogger.h"
  27. #import "MDMSetup.h"
  28. #import "DatabaseContext.h"
  29. #import "ThreemaFramework/ThreemaFramework-Swift.h"
  30. #import "Utils.h"
  31. #define THREEMA_DB_MODEL @"ThreemaData"
  32. #define THREEMA_DB_FILE @"ThreemaData.sqlite"
  33. #define THREEMA_DB_IMPORT_FILE @"RepairedThreemaData.sqlite"
  34. #define THREEMA_DB_EXTERNALS @".ThreemaData_SUPPORT"
  35. #define THREEMA_DB_DIRTY_OBJECT_KEY @"DBDirtyObjectsKey"
  36. #define THREEMA_DB_DID_UPDATE_EXTERNAL_DATA_PROTECTION_KEY @"DBDidUpdateExternalDataProtectionNewKey"
  37. #ifdef DEBUG
  38. static const DDLogLevel ddLogLevel = DDLogLevelVerbose;
  39. #else
  40. static const DDLogLevel ddLogLevel = DDLogLevelWarning;
  41. #endif
  42. @implementation DatabaseManager
  43. #pragma mark - Core Data
  44. static DatabaseManager *dbManager;
  45. + (instancetype)dbManager {
  46. if (dbManager == nil) {
  47. static dispatch_once_t onceToken;
  48. dispatch_once(&onceToken, ^{
  49. dbManager = [[DatabaseManager alloc] init];
  50. });
  51. }
  52. return dbManager;
  53. }
  54. - (DatabaseContext *)getDatabaseContext:(BOOL)forBackgroundProcess
  55. {
  56. DatabaseContext *context;
  57. NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator];
  58. if (coordinator != nil) {
  59. context = [[DatabaseContext alloc] initWithPersistentCoordinator:coordinator forBackgroundProcess:forBackgroundProcess];
  60. }
  61. else {
  62. [NSException raise:@"Invalid persistent store coordinator" format:@"Could not create persistent store coordinator"];
  63. }
  64. return context;
  65. }
  66. - (BOOL)shouldUpdateProtection {
  67. BOOL didUpdateProtectionForExternalData = [[AppGroup userDefaults] boolForKey:THREEMA_DB_DID_UPDATE_EXTERNAL_DATA_PROTECTION_KEY];
  68. NSError *error;
  69. NSURL *storeURL = [DatabaseManager storeUrl];
  70. NSDictionary *dict = [[NSFileManager defaultManager] attributesOfItemAtPath:storeURL.path error:&error];
  71. if (dict[@"NSFileProtectionKey"] == NSFileProtectionCompleteUntilFirstUserAuthentication && didUpdateProtectionForExternalData) {
  72. // Update shared directories every time to avoid crash (IOS-1406)
  73. [self updateDirectoryProtectionAtURL:[DocumentManager databaseDirectory]];
  74. return NO;
  75. }
  76. return YES;
  77. }
  78. - (void)updateDirectoryProtectionAtURL:(NSURL *)baseURL {
  79. NSFileManager *fileManager = [[NSFileManager alloc] init];
  80. NSDirectoryEnumerator *directoryEnumerator = [fileManager enumeratorAtURL:baseURL includingPropertiesForKeys:@[NSURLNameKey, NSURLFileProtectionKey] options:0 errorHandler:nil];
  81. for (NSURL *fileURL in [directoryEnumerator.allObjects filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:@"RWIsDirectory == YES"]]) {
  82. NSString *fileProtection = nil;
  83. [fileURL getResourceValue:&fileProtection forKey:NSURLFileProtectionKey error:nil];
  84. if (fileProtection != NSURLFileProtectionComplete)
  85. continue;
  86. [fileManager setAttributes:@{NSFileProtectionKey: NSFileProtectionCompleteUntilFirstUserAuthentication} ofItemAtPath:fileURL.path error:nil];
  87. }
  88. }
  89. - (void)updateProtection {
  90. // In earlier versions, when we updated the database from NSFileProtectionComplete to NSFileProtectionCompleteUntilFirstUserAuthentication,
  91. // we did not also update the contents of the external data directory. This needs to be done now, because the Web client must be
  92. // able to access images etc. when the device is locked, and Core Data needs to save external data files for received media.
  93. // To be sure, we check our entire app and group containers and change any files or directories that are still set to
  94. // NSFileProtectionComplete to the more apppropriate NSFileProtectionCompleteUntilFirstUserAuthentication.
  95. // Note that directories may have a file protection class (which then applies to all new files created within them), but they do not have to.
  96. [self updateProtectionAtURL:[DocumentManager databaseDirectory]];
  97. [self updateProtectionAtURL:[NSURL fileURLWithPath:NSHomeDirectory()]];
  98. NSUserDefaults *defaults = [AppGroup userDefaults];
  99. [defaults setBool:YES forKey:THREEMA_DB_DID_UPDATE_EXTERNAL_DATA_PROTECTION_KEY];
  100. [defaults synchronize];
  101. }
  102. - (void)updateProtectionAtURL:(NSURL*)baseURL {
  103. NSFileManager *fileManager = [[NSFileManager alloc] init];
  104. NSDirectoryEnumerator *directoryEnumerator = [fileManager enumeratorAtURL:baseURL includingPropertiesForKeys:@[NSURLNameKey, NSURLFileProtectionKey] options:0 errorHandler:nil];
  105. for (NSURL *fileURL in directoryEnumerator) {
  106. NSString *fileProtection = nil;
  107. [fileURL getResourceValue:&fileProtection forKey:NSURLFileProtectionKey error:nil];
  108. if (fileProtection != NSURLFileProtectionComplete)
  109. continue;
  110. [fileManager setAttributes:@{NSFileProtectionKey: NSFileProtectionCompleteUntilFirstUserAuthentication} ofItemAtPath:fileURL.path error:nil];
  111. }
  112. }
  113. // Returns the managed object model for the application.
  114. // If the model doesn't already exist, it is created from the application's model.
  115. - (NSManagedObjectModel *)managedObjectModel
  116. {
  117. if (_managedObjectModel != nil) {
  118. return _managedObjectModel;
  119. }
  120. NSURL *modelURL = [BundleUtil URLForResource:THREEMA_DB_MODEL withExtension:@"momd"];
  121. _managedObjectModel = [[NSManagedObjectModel alloc] initWithContentsOfURL:modelURL];
  122. return _managedObjectModel;
  123. }
  124. + (BOOL)storeExists {
  125. NSURL *storeURL = [DatabaseManager storeUrl];
  126. return [[NSFileManager defaultManager] fileExistsAtPath:storeURL.path];
  127. }
  128. - (BOOL)storeRequiresMigration {
  129. NSURL *storeURL = [DatabaseManager storeUrl];
  130. if (![[NSFileManager defaultManager] fileExistsAtPath:storeURL.path]) {
  131. return NO; /* no store = no migration */
  132. }
  133. NSError *error;
  134. NSFileProtectionType protectionType = NSFileProtectionCompleteUntilFirstUserAuthentication;
  135. NSDictionary *options = [NSDictionary dictionaryWithObjectsAndKeys:
  136. [NSNumber numberWithBool:YES], NSMigratePersistentStoresAutomaticallyOption,
  137. [NSNumber numberWithBool:YES], NSInferMappingModelAutomaticallyOption,
  138. protectionType, NSPersistentStoreFileProtectionKey, nil];
  139. NSDictionary *sourceMetadata = [NSPersistentStoreCoordinator metadataForPersistentStoreOfType:NSSQLiteStoreType URL:storeURL options:options error:&error];
  140. return ![self.managedObjectModel isConfiguration:nil compatibleWithStoreMetadata:sourceMetadata];
  141. }
  142. - (BOOL)storeRequiresImport {
  143. NSURL *repairedThreemaDataUrl = [[DocumentManager applicationDocumentsDirectory] URLByAppendingPathComponent:THREEMA_DB_IMPORT_FILE];
  144. return [[NSFileManager defaultManager] fileExistsAtPath:repairedThreemaDataUrl.path];
  145. }
  146. /**
  147. Returns the persistent store coordinator for the application.
  148. If the coordinator doesn't already exist, it is created and the application's store added to it.
  149. */
  150. - (NSPersistentStoreCoordinator *)persistentStoreCoordinator
  151. {
  152. // Execute possible DB migration just once
  153. static dispatch_once_t onceToken;
  154. dispatch_once(&onceToken, ^{
  155. if (_persistentStoreCoordinator == nil) {
  156. #ifdef DEBUG
  157. if ([AppGroup getCurrentType] == AppGroupTypeApp) {
  158. [FileUtility logDirectoriesAndFilesWithPath:[DocumentManager databaseDirectory] logFileName:@"db-migration-before.log"];
  159. }
  160. #endif
  161. double startTime = CACurrentMediaTime();
  162. NSURL *storeURL = [DatabaseManager storeUrl];
  163. [self migrateDatabaseLocation];
  164. NSFileManager *fileManager = [NSFileManager defaultManager];
  165. NSURL *documentsUrl = [DocumentManager databaseDirectory];
  166. NSURL *tmpUrlToExternalStorage = [documentsUrl URLByAppendingPathComponent:@"tmpPathToReplacementData"];
  167. NSURL *urlToExternalStorage = [documentsUrl URLByAppendingPathComponent:@".ThreemaData_SUPPORT/_EXTERNAL_DATA"];
  168. NSString *coreDataModelVersion = [BundleUtil objectForInfoDictionaryKey:@"ThreemaCoreDataVersion"];
  169. NSURL *urlToBackupStorage = [NSURL URLWithString:[NSString stringWithFormat:@"%@.bak.%@", storeURL.absoluteString, coreDataModelVersion]];
  170. NSURL *tmpUrlToBackupStorage = [NSURL URLWithString:[NSString stringWithFormat:@"%@.bak.%@.%.0f", storeURL.absoluteString, coreDataModelVersion, [[NSDate date] timeIntervalSince1970]]];
  171. //Check if the new model is compatible with any previously stored model
  172. BOOL requiresMigration = [self storeRequiresMigration];
  173. if (requiresMigration) {
  174. // Migration is required - check if a store backup file (.bak) exists. If so, the last migration attempt has
  175. // failed, and before trying again, we copy the backup back to the store URL so Core Data can make another try.
  176. // Also, during migration, we move away the external data storage folder to keep Core Data from copying every
  177. // single external data item (media etc.), which is useless, and takes a long time and a lot of disk space.
  178. if ([fileManager fileExistsAtPath:[urlToBackupStorage path]]) {
  179. // Delete the broken, half-migrated store and copy the backup
  180. NSError *copyBackupError = nil;
  181. [fileManager removeItemAtURL:storeURL error:nil];
  182. [fileManager copyItemAtURL:urlToBackupStorage toURL:storeURL error:&copyBackupError];
  183. if (copyBackupError != nil) {
  184. _storeError = copyBackupError;
  185. }
  186. else {
  187. // Remove wal and shm temporary files to prevent problems with the SQLite store
  188. NSURL *walFile = [NSURL URLWithString:[NSString stringWithFormat:@"%@-wal", storeURL.absoluteString]];
  189. [fileManager removeItemAtURL:walFile error:nil];
  190. NSURL *shmFile = [NSURL URLWithString:[NSString stringWithFormat:@"%@-shm", storeURL.absoluteString]];
  191. [fileManager removeItemAtURL:shmFile error:nil];
  192. // Remove external storage folder; the original will be at tmpUrlToExternalStorage at this point
  193. [fileManager removeItemAtURL:urlToExternalStorage error:nil];
  194. }
  195. } else {
  196. // Before migration begins, copy the store to a backup file (.bak). We do this in two steps:
  197. // first we copy the store to a .bak2 file, and then we rename the .bak2 to .bak. This is
  198. // so that if the copy operation is interrupted (which is possible as it can take some time for
  199. // large stores), we don't end up using a broken .bak when we start again.
  200. NSError *copyBackupError = nil;
  201. [fileManager removeItemAtURL:tmpUrlToBackupStorage error:nil];
  202. [fileManager copyItemAtURL:storeURL toURL:tmpUrlToBackupStorage error:&copyBackupError];
  203. if (copyBackupError != nil) {
  204. _storeError = copyBackupError;
  205. }
  206. else {
  207. // Rename .bak2 to .bak
  208. [fileManager removeItemAtURL:urlToBackupStorage error:nil];
  209. [fileManager moveItemAtURL:tmpUrlToBackupStorage toURL:urlToBackupStorage error:nil];
  210. // Move away external storage directory during migration
  211. [fileManager removeItemAtURL:tmpUrlToExternalStorage error:nil];
  212. if ([fileManager fileExistsAtPath:[urlToExternalStorage path]]) {
  213. [fileManager moveItemAtURL:urlToExternalStorage toURL:tmpUrlToExternalStorage error:nil];
  214. }
  215. }
  216. }
  217. } else {
  218. // Migration is currently not required, but if a previous migration completed without
  219. // us having a chance to put the external data storage folder back in place, we will
  220. // end up with the media in tmpUrlToExternalStorage where it is inaccessible to Core Data.
  221. // Attempt to move the media back in such a case, if necessary
  222. if ([fileManager fileExistsAtPath:[tmpUrlToExternalStorage path]]) {
  223. if ([fileManager fileExistsAtPath:[urlToExternalStorage path]]) {
  224. // Ooops, the external storage directory already exists, so we should not delete it or
  225. // we will risk losing some (new) media. Instead, merge the contents of the two directories
  226. NSError *mergeError = nil;
  227. [self mergeContentsOfPath:[tmpUrlToExternalStorage path] intoPath:[urlToExternalStorage path] error:&mergeError];
  228. if (!mergeError) {
  229. [fileManager removeItemAtURL:tmpUrlToExternalStorage error:nil];
  230. }
  231. } else {
  232. [fileManager moveItemAtURL:tmpUrlToExternalStorage toURL:urlToExternalStorage error:nil];
  233. }
  234. [self removeMigrationLeftover];
  235. }
  236. }
  237. if (_storeError == nil) {
  238. NSError *error = nil;
  239. NSPersistentStoreCoordinator *persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:[self managedObjectModel]];
  240. NSFileProtectionType protectionType = NSFileProtectionCompleteUntilFirstUserAuthentication;
  241. NSDictionary *options = [NSDictionary dictionaryWithObjectsAndKeys:
  242. [NSNumber numberWithBool:YES], NSMigratePersistentStoresAutomaticallyOption,
  243. [NSNumber numberWithBool:YES], NSInferMappingModelAutomaticallyOption,
  244. protectionType, NSPersistentStoreFileProtectionKey, nil];
  245. if (![persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:storeURL options:options error:&error]) {
  246. if ([fileManager fileExistsAtPath:[urlToBackupStorage path]]) {
  247. [fileManager removeItemAtURL:storeURL error:nil];
  248. [fileManager copyItemAtURL:urlToBackupStorage toURL:storeURL error:nil];
  249. }
  250. DDLogError(@"Unresolved error %@, %@", error, [error userInfo]);
  251. _storeError = error;
  252. }
  253. if (requiresMigration && error == nil) {
  254. // Core Data migration is now complete. Replace the default external storage folder with the version pre upgrade,
  255. // and delete the store backup files.
  256. [fileManager removeItemAtURL:urlToExternalStorage error:nil];
  257. [fileManager moveItemAtURL:tmpUrlToExternalStorage toURL:urlToExternalStorage error:nil];
  258. [fileManager removeItemAtURL:urlToBackupStorage error:nil];
  259. [fileManager removeItemAtURL:tmpUrlToBackupStorage error:nil];
  260. [self removeMigrationLeftover];
  261. }
  262. double endTime = CACurrentMediaTime();
  263. DDLogInfo(@"DB setup time %f s", (endTime - startTime));
  264. #ifdef DEBUG
  265. [FileUtility logDirectoriesAndFilesWithPath:[DocumentManager databaseDirectory] logFileName:@"db-migration-after.log"];
  266. #endif
  267. _persistentStoreCoordinator = persistentStoreCoordinator;
  268. }
  269. }
  270. });
  271. return _persistentStoreCoordinator;
  272. }
  273. - (void)removeMigrationLeftover {
  274. // remove any leftover from previous failed migrations
  275. NSURL *documentsUrl = [DocumentManager databaseDirectory];
  276. NSFileManager *fileManager = [NSFileManager defaultManager];
  277. NSArray *files = [fileManager contentsOfDirectoryAtPath:documentsUrl.path error:nil];
  278. for (NSString *fileName in files) {
  279. if ([fileName hasPrefix:[NSString stringWithFormat:@"%@.v2.bak", THREEMA_DB_FILE]] || [fileName hasPrefix:[NSString stringWithFormat:@"%@.bak", THREEMA_DB_FILE]]) {
  280. NSURL *fileUrl = [documentsUrl URLByAppendingPathComponent:fileName];
  281. [fileManager removeItemAtURL:fileUrl error:nil];
  282. }
  283. }
  284. }
  285. - (unsigned long long)storeSize {
  286. unsigned long long storeSize = 0;
  287. NSString *documentsPath = [DocumentManager databaseDirectory].path;
  288. storeSize += [DocumentManager sizeOfObjectAtPath:[documentsPath stringByAppendingPathComponent:THREEMA_DB_FILE]];
  289. // NSString *pathToSupportDir = [documentsPath stringByAppendingPathComponent:THREEMA_DB_EXTERNALS];
  290. // storeSize += [Utils sizeOfObjectAtPath:pathToSupportDir];
  291. return storeSize;
  292. }
  293. - (BOOL)canMigrateDB {
  294. unsigned long long storeSize = [self storeSize];
  295. unsigned long long freeDiskSpace = [self freeDiskSpace];
  296. unsigned long long minFreeRequired = MAX(storeSize*3, 512*1024*1024);
  297. /* must have at least 3 * storeSize free, and in any case at least 512 MB */
  298. if (freeDiskSpace < minFreeRequired) {
  299. DDLogError(@"Not enough space for migration (store size %llu, %llu free)", storeSize, freeDiskSpace);
  300. dispatch_async(dispatch_get_main_queue(), ^{
  301. NSString *message = [NSString stringWithFormat:NSLocalizedString(@"database_migration_storage_warning_message", nil), minFreeRequired/1073741824.0f, freeDiskSpace/1073741824.0f];
  302. [ErrorHandler abortWithTitle:NSLocalizedString(@"database_migration_storage_warning_title", nil) message:message];
  303. });
  304. return NO;
  305. }
  306. return YES;
  307. }
  308. - (void)doMigrateDB {
  309. [self persistentStoreCoordinator];
  310. }
  311. - (void)copyImportedDatabase {
  312. double startTime = CACurrentMediaTime();
  313. NSURL *storeURL = [DatabaseManager storeUrl];
  314. [self migrateDatabaseLocation];
  315. NSFileManager *fileManager = [NSFileManager defaultManager];
  316. NSURL *documentsUrl = [DocumentManager applicationDocumentsDirectory];
  317. NSURL *urlToImportStorage = [NSURL URLWithString:[NSString stringWithFormat:@"%@%@", documentsUrl.absoluteString, THREEMA_DB_IMPORT_FILE]];
  318. if ([fileManager fileExistsAtPath:[urlToImportStorage path]]) {
  319. NSError *copyImportError = nil;
  320. [fileManager removeItemAtURL:storeURL error:nil];
  321. [fileManager copyItemAtURL:urlToImportStorage toURL:storeURL error:&copyImportError];
  322. if (copyImportError == nil) {
  323. // Remove wal and shm temporary files to prevent problems with the SQLite store
  324. NSURL *walFile = [NSURL URLWithString:[NSString stringWithFormat:@"%@-wal", storeURL.absoluteString]];
  325. [fileManager removeItemAtURL:walFile error:nil];
  326. NSURL *shmFile = [NSURL URLWithString:[NSString stringWithFormat:@"%@-shm", storeURL.absoluteString]];
  327. [fileManager removeItemAtURL:shmFile error:nil];
  328. [fileManager removeItemAtURL:urlToImportStorage error:nil];
  329. }
  330. double endTime = CACurrentMediaTime();
  331. DDLogInfo(@"DB setup time %f s", (endTime - startTime));
  332. }
  333. }
  334. - (void)eraseDB {
  335. NSArray *stores = [_persistentStoreCoordinator persistentStores];
  336. for (NSPersistentStore *store in stores) {
  337. [_persistentStoreCoordinator removePersistentStore:store error:nil];
  338. [[NSFileManager defaultManager] removeItemAtPath:store.URL.path error:nil];
  339. }
  340. _persistentStoreCoordinator = nil;
  341. }
  342. - (void)migrateDatabaseLocation {
  343. NSFileManager *fileManager = [NSFileManager defaultManager];
  344. // check if data is at application location, if yes move it to group directory
  345. NSURL *appUrl = [DocumentManager applicationDocumentsDirectory];
  346. NSURL *appFile = [appUrl URLByAppendingPathComponent:THREEMA_DB_FILE];
  347. if ([fileManager fileExistsAtPath:appFile.path]) {
  348. NSURL *targetURL = [DocumentManager databaseDirectory];
  349. [self moveDBFilesFrom:appUrl to:targetURL];
  350. }
  351. }
  352. - (void)moveDBFilesFrom:(NSURL *)sourceUrl to:(NSURL *)targetUrl {
  353. NSError *error;
  354. NSFileManager *fileManager = [NSFileManager defaultManager];
  355. [fileManager createDirectoryAtURL:targetUrl withIntermediateDirectories:YES attributes:nil error:&error];
  356. NSURL *sourceDBFile = [sourceUrl URLByAppendingPathComponent:THREEMA_DB_FILE];
  357. NSURL *targetDBFile = [targetUrl URLByAppendingPathComponent:THREEMA_DB_FILE];
  358. if ([fileManager fileExistsAtPath:sourceDBFile.path]) {
  359. [fileManager removeItemAtURL:targetDBFile error:&error];
  360. [fileManager moveItemAtURL:sourceDBFile toURL:targetDBFile error:&error];
  361. }
  362. NSString *walFile = [THREEMA_DB_FILE stringByAppendingString:@"-wal"];
  363. NSURL *sourceDBWalFile = [sourceUrl URLByAppendingPathComponent:walFile];
  364. NSURL *targetDBWalFile = [targetUrl URLByAppendingPathComponent:walFile];
  365. if ([fileManager fileExistsAtPath:sourceDBWalFile.path]) {
  366. [fileManager removeItemAtURL:targetDBWalFile error:&error];
  367. [fileManager moveItemAtURL:sourceDBWalFile toURL:targetDBWalFile error:&error];
  368. }
  369. // no need to move shm file, it is recreated by sqllite (https://www.sqlite.org/tempfiles.html)
  370. // to keep clean -> delete it
  371. NSString *shmFile = [THREEMA_DB_FILE stringByAppendingString:@"-shm"];
  372. NSURL *shmUrl = [sourceUrl URLByAppendingPathComponent:shmFile];
  373. if ([fileManager fileExistsAtPath:shmUrl.path]) {
  374. [fileManager removeItemAtURL:shmUrl error:nil];
  375. }
  376. NSURL *sourceExternals = [sourceUrl URLByAppendingPathComponent:THREEMA_DB_EXTERNALS];
  377. NSURL *targetExternals = [targetUrl URLByAppendingPathComponent:THREEMA_DB_EXTERNALS];
  378. if ([fileManager fileExistsAtPath:sourceExternals.path]) {
  379. if ([fileManager fileExistsAtPath:targetExternals.path]) {
  380. // there already are some external files at target -> keep it and move source files one by one
  381. NSURL *sourceExternalsSubDir = [sourceExternals URLByAppendingPathComponent:@"_EXTERNAL_DATA"];
  382. NSURL *targetExternalsSubDir = [targetExternals URLByAppendingPathComponent:@"_EXTERNAL_DATA"];
  383. if ([fileManager fileExistsAtPath:targetExternalsSubDir.path] == NO) {
  384. [fileManager createDirectoryAtURL:targetExternalsSubDir withIntermediateDirectories:YES attributes:0 error:nil];
  385. }
  386. NSDirectoryEnumerator *dirEnum = [fileManager enumeratorAtPath:sourceExternalsSubDir.path];
  387. NSString *file;
  388. while ((file = [dirEnum nextObject])) {
  389. NSURL *externalDataSource = [sourceExternalsSubDir URLByAppendingPathComponent:file];
  390. NSURL *externalDataTarget = [targetExternalsSubDir URLByAppendingPathComponent:file];
  391. [fileManager moveItemAtPath:externalDataSource.path toPath:externalDataTarget.path error:&error];
  392. }
  393. // remove old directory
  394. [fileManager removeItemAtURL:sourceExternalsSubDir error:nil];
  395. } else {
  396. // move whole directory
  397. [fileManager moveItemAtURL:sourceExternals toURL:targetExternals error:&error];
  398. }
  399. }
  400. }
  401. + (NSURL *)storeUrl {
  402. return [[DocumentManager databaseDirectory] URLByAppendingPathComponent:THREEMA_DB_FILE];
  403. }
  404. - (unsigned long long)freeDiskSpace {
  405. if (@available(iOS 11.0, *)) {
  406. NSURL *fileUrl = [NSURL fileURLWithPath:NSHomeDirectory()];
  407. NSError *error = nil;
  408. NSDictionary *dict = [fileUrl resourceValuesForKeys:@[NSURLVolumeAvailableCapacityForImportantUsageKey] error:&error];
  409. if (dict) {
  410. NSNumber *availableCapacity = dict[NSURLVolumeAvailableCapacityForImportantUsageKey];
  411. return availableCapacity.unsignedLongLongValue;
  412. } else {
  413. DDLogError(@"Cannot retrieve free disk space: %@", error);
  414. return 0;
  415. }
  416. } else {
  417. //unsigned long long totalSpace = 0;
  418. unsigned long long totalFreeSpace = 0;
  419. NSError *error = nil;
  420. NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
  421. NSDictionary *dictionary = [[NSFileManager defaultManager] attributesOfFileSystemForPath:[paths lastObject] error: &error];
  422. if (dictionary) {
  423. //NSNumber *fileSystemSizeInBytes = [dictionary objectForKey: NSFileSystemSize];
  424. NSNumber *freeFileSystemSizeInBytes = [dictionary objectForKey:NSFileSystemFreeSize];
  425. //totalSpace = [fileSystemSizeInBytes unsignedLongLongValue];
  426. totalFreeSpace = [freeFileSystemSizeInBytes unsignedLongLongValue];
  427. }
  428. return totalFreeSpace;
  429. }
  430. }
  431. - (void)disableBackupForDatabaseDirectory:(BOOL)disable
  432. {
  433. NSString *documentsPath = [DocumentManager databaseDirectory].path;
  434. NSString *applicationDocumentsPath = [DocumentManager applicationDocumentsDirectory].path;
  435. NSString *cachePath = [[[NSFileManager defaultManager] URLsForDirectory:NSCachesDirectory inDomains:NSUserDomainMask] lastObject].path;
  436. NSURL *urlToExternalStorage = [NSURL fileURLWithPath:[documentsPath stringByAppendingPathComponent:@".ThreemaData_SUPPORT"]];
  437. [self setResourceValue:disable forUrl:urlToExternalStorage];
  438. urlToExternalStorage = [NSURL fileURLWithPath:[documentsPath stringByAppendingPathComponent:THREEMA_DB_FILE]];
  439. [self setResourceValue:disable forUrl:urlToExternalStorage];
  440. urlToExternalStorage = [NSURL fileURLWithPath:[documentsPath stringByAppendingPathComponent:@"ThreemaData.sqlite-shm"]];
  441. [self setResourceValue:disable forUrl:urlToExternalStorage];
  442. urlToExternalStorage = [NSURL fileURLWithPath:[documentsPath stringByAppendingPathComponent:@"ThreemaData.sqlite-wal"]];
  443. [self setResourceValue:disable forUrl:urlToExternalStorage];
  444. urlToExternalStorage = [NSURL fileURLWithPath:[documentsPath stringByAppendingPathComponent:@"DoneMessages"]];
  445. [self setResourceValue:disable forUrl:urlToExternalStorage];
  446. urlToExternalStorage = [NSURL fileURLWithPath:[documentsPath stringByAppendingPathComponent:@"WebSessions"]];
  447. [self setResourceValue:disable forUrl:urlToExternalStorage];
  448. urlToExternalStorage = [NSURL fileURLWithPath:[documentsPath stringByAppendingPathComponent:@"PreviousContext"]];
  449. [self setResourceValue:disable forUrl:urlToExternalStorage];
  450. urlToExternalStorage = [NSURL fileURLWithPath:[applicationDocumentsPath stringByAppendingPathComponent:@"idbackup.txt"]];
  451. [self setResourceValue:disable forUrl:urlToExternalStorage];
  452. urlToExternalStorage = [NSURL fileURLWithPath:[cachePath stringByAppendingPathComponent:@"ch.threema.work.iapp/Cache.db"]];
  453. [self setResourceValue:disable forUrl:urlToExternalStorage];
  454. urlToExternalStorage = [NSURL fileURLWithPath:[cachePath stringByAppendingPathComponent:@"ch.threema.work.iapp/Cache.db-shm"]];
  455. [self setResourceValue:disable forUrl:urlToExternalStorage];
  456. urlToExternalStorage = [NSURL fileURLWithPath:[cachePath stringByAppendingPathComponent:@"ch.threema.work.iapp/Cache.db-wal"]];
  457. [self setResourceValue:disable forUrl:urlToExternalStorage];
  458. }
  459. - (void)setResourceValue:(BOOL)disable forUrl:(NSURL *)url {
  460. NSError *error = nil;
  461. if ([[NSFileManager defaultManager] fileExistsAtPath:[url path]]) {
  462. // assert([[NSFileManager defaultManager] fileExistsAtPath:[url path]]);
  463. BOOL success = [url setResourceValue:[NSNumber numberWithBool:disable] forKey: NSURLIsExcludedFromBackupKey error: &error];
  464. if(!success){
  465. DDLogError(@"Error excluding %@ from backup %@", [url lastPathComponent], error);
  466. }
  467. }
  468. }
  469. #pragma mark - dirty object handling (e.g. when switching from share extension back to app)
  470. - (void)refreshDirtyObjectIDs:(NSDictionary *)changes intoContext:(NSManagedObjectContext *)context {
  471. [NSManagedObjectContext mergeChangesFromRemoteContextSave:changes intoContexts:@[context]];
  472. }
  473. - (void)refreshDirtyObjects {
  474. DatabaseContext *dbContext = [self getDatabaseContext:NO];
  475. NSUserDefaults *defaults = [AppGroup userDefaults];
  476. NSArray *objects = [defaults arrayForKey:THREEMA_DB_DIRTY_OBJECT_KEY];
  477. if (objects == nil) {
  478. return;
  479. }
  480. [defaults removeObjectForKey:THREEMA_DB_DIRTY_OBJECT_KEY];
  481. [defaults synchronize];
  482. NSTimeInterval stalenessInterval = dbContext.main.stalenessInterval;
  483. [dbContext main].stalenessInterval = 0.0;
  484. NSMutableSet *notifyObjectIds = [NSMutableSet setWithCapacity:[objects count]];
  485. // first refresh objects in context
  486. for (NSString *urlString in objects) {
  487. NSURL *url = [NSURL URLWithString:urlString];
  488. NSManagedObjectID *objectID = [_persistentStoreCoordinator managedObjectIDForURIRepresentation:url];
  489. if (objectID) {
  490. NSManagedObject *object = [[dbContext main] objectWithID:objectID];
  491. [[dbContext main] refreshObject:object mergeChanges:YES];
  492. [notifyObjectIds addObject:objectID];
  493. }
  494. }
  495. [dbContext main].stalenessInterval = stalenessInterval;
  496. // notfiy object changes
  497. for (NSManagedObjectID *objectID in notifyObjectIds) {
  498. DDLogInfo(@"Notify refresh of dirty object: %@", objectID);
  499. [self notifyObjectRefresh:objectID];
  500. }
  501. }
  502. - (void)notifyObjectRefresh:(NSManagedObjectID *)objectID {
  503. NSDictionary *info = [NSDictionary dictionaryWithObjectsAndKeys:
  504. objectID, kKeyObjectID,
  505. nil];
  506. [[NSNotificationCenter defaultCenter] postNotificationName:kNotificationDBRefreshedDirtyObject object:self userInfo:info];
  507. }
  508. - (void)addDirtyObject:(NSManagedObject *)object {
  509. if (object == nil || object.objectID == nil) {
  510. return;
  511. }
  512. NSMutableSet *newObjects;
  513. NSUserDefaults *defaults = [AppGroup userDefaults];
  514. NSArray *objects = [defaults arrayForKey:THREEMA_DB_DIRTY_OBJECT_KEY];
  515. if (objects) {
  516. newObjects = [NSMutableSet setWithArray:objects];
  517. } else {
  518. newObjects = [NSMutableSet set];
  519. }
  520. NSURL *url = object.objectID.URIRepresentation;
  521. [newObjects addObject:url.absoluteString];
  522. NSArray *newObjectsArray = [newObjects allObjects];
  523. [defaults setObject:newObjectsArray forKey:THREEMA_DB_DIRTY_OBJECT_KEY];
  524. [defaults synchronize];
  525. [AppGroup notifyAppGroupSyncNeeded];
  526. }
  527. - (void)mergeContentsOfPath:(NSString *)srcDir intoPath:(NSString *)dstDir error:(NSError**)err {
  528. NSFileManager *fm = [NSFileManager defaultManager];
  529. NSDirectoryEnumerator *srcDirEnum = [fm enumeratorAtPath:srcDir];
  530. NSString *subPath;
  531. while ((subPath = [srcDirEnum nextObject])) {
  532. NSString *srcPath = [srcDir stringByAppendingPathComponent:subPath];
  533. NSString *dstPath = [dstDir stringByAppendingPathComponent:subPath];
  534. [fm moveItemAtPath:srcPath toPath:dstPath error:err];
  535. if (err && *err) {
  536. NSLog(@"ERROR: %@", *err);
  537. return;
  538. }
  539. }
  540. }
  541. - (BOOL)copyOldVersionOfDatabase {
  542. NSURL *oldVersionUrl = [[DocumentManager applicationDocumentsDirectory] URLByAppendingPathComponent:@"ThreemaDataOldVersion"];
  543. NSFileManager *fm = [NSFileManager defaultManager];
  544. if ([fm fileExistsAtPath:oldVersionUrl.path]) {
  545. // delete current DB
  546. NSURL *databaseUrl = [DatabaseManager storeUrl];
  547. [DocumentManager removeItemIfExists:databaseUrl];
  548. NSString *shmFile = [THREEMA_DB_FILE stringByAppendingString:@"-shm"];
  549. [DocumentManager removeItemIfExists:[[DocumentManager databaseDirectory] URLByAppendingPathComponent:shmFile]];
  550. NSString *walFile = [THREEMA_DB_FILE stringByAppendingString:@"-wal"];
  551. [DocumentManager removeItemIfExists:[[DocumentManager databaseDirectory] URLByAppendingPathComponent:walFile]];
  552. NSURL *externalsUrl = [[DocumentManager databaseDirectory] URLByAppendingPathComponent:THREEMA_DB_EXTERNALS];
  553. [DocumentManager removeItemIfExists:externalsUrl];
  554. // move older version of DB
  555. NSURL *sourceDatabaseUrl = [[[DocumentManager applicationDocumentsDirectory] URLByAppendingPathComponent:@"ThreemaDataOldVersion"] URLByAppendingPathComponent:THREEMA_DB_FILE];
  556. [DocumentManager moveItemIfExists:sourceDatabaseUrl destination:databaseUrl];
  557. NSURL *sourceExternalsUrl = [[[DocumentManager applicationDocumentsDirectory] URLByAppendingPathComponent:@"ThreemaDataOldVersion"] URLByAppendingPathComponent:THREEMA_DB_EXTERNALS];
  558. [DocumentManager moveItemIfExists:sourceExternalsUrl destination:externalsUrl];
  559. // delete older version files
  560. [fm removeItemAtURL:oldVersionUrl error:nil];
  561. NSURL *pendingMessages = [[DocumentManager documentsDirectory] URLByAppendingPathComponent:@"PendingMessages"];
  562. [DocumentManager removeItemIfExists:pendingMessages];
  563. return YES;
  564. }
  565. return NO;
  566. }
  567. @end
  568. @implementation NSURL (RWIsDirectory)
  569. - (BOOL)RWIsDirectory
  570. {
  571. NSNumber * isDir;
  572. [self getResourceValue:&isDir forKey:NSURLIsDirectoryKey error:NULL];
  573. return [isDir boolValue];
  574. }
  575. @end