// // Copyright (c) 2019 Open Whisper Systems. All rights reserved. // #import "OWSFileSystem.h" #import "OWSError.h" #import "TSConstants.h" #import "AppContext.h" #import NS_ASSUME_NONNULL_BEGIN @implementation OWSFileSystem + (BOOL)protectRecursiveContentsAtPath:(NSString *)path { BOOL isDirectory; if (![NSFileManager.defaultManager fileExistsAtPath:path isDirectory:&isDirectory]) { return NO; } if (!isDirectory) { return [self protectFileOrFolderAtPath:path]; } NSString *dirPath = path; BOOL success = YES; NSDirectoryEnumerator *directoryEnumerator = [[NSFileManager defaultManager] enumeratorAtPath:dirPath]; for (NSString *relativePath in directoryEnumerator) { NSString *filePath = [dirPath stringByAppendingPathComponent:relativePath]; OWSLogDebug(@"path: %@ had attributes: %@", filePath, directoryEnumerator.fileAttributes); success = success && [self protectFileOrFolderAtPath:filePath]; } OWSLogInfo(@"protected contents at path: %@", path); return success; } + (BOOL)protectFileOrFolderAtPath:(NSString *)path { return [self protectFileOrFolderAtPath:path fileProtectionType:NSFileProtectionCompleteUntilFirstUserAuthentication]; } + (BOOL)protectFileOrFolderAtPath:(NSString *)path fileProtectionType:(NSFileProtectionType)fileProtectionType { OWSLogVerbose(@"protecting file at path: %@", path); if (![NSFileManager.defaultManager fileExistsAtPath:path]) { return NO; } NSError *error; NSDictionary *fileProtection = @{ NSFileProtectionKey : fileProtectionType }; [[NSFileManager defaultManager] setAttributes:fileProtection ofItemAtPath:path error:&error]; NSDictionary *resourcesAttrs = @{ NSURLIsExcludedFromBackupKey : @YES }; NSURL *ressourceURL = [NSURL fileURLWithPath:path]; BOOL success = [ressourceURL setResourceValues:resourcesAttrs error:&error]; if (error || !success) { OWSFailDebug(@"Could not protect file or folder: %@", error); return NO; } return YES; } + (void)logAttributesOfItemAtPathRecursively:(NSString *)path { BOOL isDirectory; BOOL exists = [[NSFileManager defaultManager] fileExistsAtPath:path isDirectory:&isDirectory]; if (!exists) { OWSFailDebug(@"error retrieving file attributes for missing file"); return; } if (isDirectory) { NSDirectoryEnumerator *directoryEnumerator = [[NSFileManager defaultManager] enumeratorAtPath:(NSString *)path]; for (NSString *path in directoryEnumerator) { OWSLogDebug(@"path: %@ has attributes: %@", path, directoryEnumerator.fileAttributes); } } else { NSError *error; NSDictionary *_Nullable attributes = [[NSFileManager defaultManager] attributesOfItemAtPath:path error:&error]; if (error) { OWSFailDebug(@"error retrieving file attributes: %@", error); } else { OWSLogDebug(@"path: %@ has attributes: %@", path, attributes); } } } + (NSString *)appLibraryDirectoryPath { NSFileManager *fileManager = [NSFileManager defaultManager]; NSURL *documentDirectoryURL = [[fileManager URLsForDirectory:NSLibraryDirectory inDomains:NSUserDomainMask] lastObject]; return [documentDirectoryURL path]; } + (NSString *)appDocumentDirectoryPath { return CurrentAppContext().appDocumentDirectoryPath; } + (NSString *)appSharedDataDirectoryPath { return CurrentAppContext().appSharedDataDirectoryPath; } + (NSString *)cachesDirectoryPath { NSArray *paths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES); OWSAssertDebug(paths.count >= 1); return paths[0]; } + (nullable NSError *)renameFilePathUsingRandomExtension:(NSString *)oldFilePath { NSFileManager *fileManager = [NSFileManager defaultManager]; if (![fileManager fileExistsAtPath:oldFilePath]) { return nil; } NSString *newFilePath = [[oldFilePath stringByAppendingString:@"."] stringByAppendingString:[NSUUID UUID].UUIDString]; OWSLogInfo(@"Moving file or directory from: %@ to: %@", oldFilePath, newFilePath); NSError *_Nullable error; BOOL success = [fileManager moveItemAtPath:oldFilePath toPath:newFilePath error:&error]; if (!success || error) { OWSLogDebug(@"Could not move file or directory from: %@ to: %@, error: %@", oldFilePath, newFilePath, error); OWSFailDebug(@"Could not move file or directory with error: %@", error); return error; } return nil; } + (nullable NSError *)moveAppFilePath:(NSString *)oldFilePath sharedDataFilePath:(NSString *)newFilePath { NSFileManager *fileManager = [NSFileManager defaultManager]; if (![fileManager fileExistsAtPath:oldFilePath]) { return nil; } OWSLogInfo(@"Moving file or directory from: %@ to: %@", oldFilePath, newFilePath); if ([fileManager fileExistsAtPath:newFilePath]) { // If a file/directory already exists at the destination, // try to move it "aside" by renaming it with an extension. NSError *_Nullable error = [self renameFilePathUsingRandomExtension:newFilePath]; if (error) { return error; } } if ([fileManager fileExistsAtPath:newFilePath]) { OWSLogDebug( @"Can't move file or directory from: %@ to: %@; destination already exists.", oldFilePath, newFilePath); OWSFailDebug(@"Can't move file or directory; destination already exists."); return OWSErrorWithCodeDescription( OWSErrorCodeMoveFileToSharedDataContainerError, @"Can't move file; destination already exists."); } NSDate *startDate = [NSDate new]; NSError *_Nullable error; BOOL success = [fileManager moveItemAtPath:oldFilePath toPath:newFilePath error:&error]; if (!success || error) { OWSLogDebug(@"Could not move file or directory from: %@ to: %@, error: %@", oldFilePath, newFilePath, error); OWSFailDebug(@"Could not move file or directory with error: %@", error); return error; } OWSLogInfo(@"Moved file or directory in: %f", fabs([startDate timeIntervalSinceNow])); OWSLogDebug(@"Moved file or directory from: %@ to: %@ in: %f", oldFilePath, newFilePath, fabs([startDate timeIntervalSinceNow])); // Ensure all files moved have the proper data protection class. // On large directories this can take a while, so we dispatch async // since we're in the launch path. dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ [self protectRecursiveContentsAtPath:newFilePath]; }); return nil; } + (BOOL)ensureDirectoryExists:(NSString *)dirPath { return [self ensureDirectoryExists:dirPath fileProtectionType:NSFileProtectionCompleteUntilFirstUserAuthentication]; } + (BOOL)ensureDirectoryExists:(NSString *)dirPath fileProtectionType:(NSFileProtectionType)fileProtectionType { BOOL isDirectory; BOOL exists = [[NSFileManager defaultManager] fileExistsAtPath:dirPath isDirectory:&isDirectory]; if (exists) { OWSAssertDebug(isDirectory); return [self protectFileOrFolderAtPath:dirPath fileProtectionType:fileProtectionType]; } else { OWSLogInfo(@"Creating directory at: %@", dirPath); NSError *error = nil; [[NSFileManager defaultManager] createDirectoryAtPath:dirPath withIntermediateDirectories:YES attributes:nil error:&error]; if (error) { OWSFailDebug(@"Failed to create directory: %@, error: %@", dirPath, error); return NO; } return [self protectFileOrFolderAtPath:dirPath fileProtectionType:fileProtectionType]; } } + (BOOL)ensureFileExists:(NSString *)filePath { BOOL exists = [[NSFileManager defaultManager] fileExistsAtPath:filePath]; if (exists) { return [self protectFileOrFolderAtPath:filePath]; } else { BOOL success = [[NSFileManager defaultManager] createFileAtPath:filePath contents:nil attributes:nil]; if (!success) { OWSFailDebug(@"Failed to create file."); return NO; } return [self protectFileOrFolderAtPath:filePath]; } } + (BOOL)deleteFile:(NSString *)filePath { NSError *error; BOOL success = [[NSFileManager defaultManager] removeItemAtPath:filePath error:&error]; if (!success || error) { OWSLogError(@"Failed to delete file: %@", error.description); return NO; } return YES; } + (BOOL)deleteFileIfExists:(NSString *)filePath { if (![[NSFileManager defaultManager] fileExistsAtPath:filePath]) { return YES; } return [self deleteFile:filePath]; } + (NSArray *_Nullable)allFilesInDirectoryRecursive:(NSString *)dirPath error:(NSError **)error { OWSAssertDebug(dirPath.length > 0); *error = nil; NSArray *filenames = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:dirPath error:error]; if (*error) { OWSFailDebug(@"could not find files in directory: %@", *error); return nil; } NSMutableArray *filePaths = [NSMutableArray new]; for (NSString *filename in filenames) { NSString *filePath = [dirPath stringByAppendingPathComponent:filename]; BOOL isDirectory; [[NSFileManager defaultManager] fileExistsAtPath:filePath isDirectory:&isDirectory]; if (isDirectory) { [filePaths addObjectsFromArray:[self allFilesInDirectoryRecursive:filePath error:error]]; if (*error) { return nil; } } else { [filePaths addObject:filePath]; } } return filePaths; } + (NSString *)temporaryFilePath { return [self temporaryFilePathWithFileExtension:nil]; } + (NSString *)temporaryFilePathWithFileExtension:(NSString *_Nullable)fileExtension { NSString *temporaryDirectory = OWSTemporaryDirectory(); NSString *tempFileName = NSUUID.UUID.UUIDString; if (fileExtension.length > 0) { tempFileName = [[tempFileName stringByAppendingString:@"."] stringByAppendingString:fileExtension]; } NSString *tempFilePath = [temporaryDirectory stringByAppendingPathComponent:tempFileName]; return tempFilePath; } + (nullable NSString *)writeDataToTemporaryFile:(NSData *)data fileExtension:(NSString *_Nullable)fileExtension { OWSAssertDebug(data); NSString *tempFilePath = [self temporaryFilePathWithFileExtension:fileExtension]; NSError *error; BOOL success = [data writeToFile:tempFilePath options:NSDataWritingAtomic error:&error]; if (!success || error) { OWSFailDebug(@"could not write to temporary file: %@", error); return nil; } [self protectFileOrFolderAtPath:tempFilePath]; return tempFilePath; } + (nullable NSNumber *)fileSizeOfPath:(NSString *)filePath { NSFileManager *fileManager = [NSFileManager defaultManager]; NSError *_Nullable error; unsigned long long fileSize = [[fileManager attributesOfItemAtPath:filePath error:&error][NSFileSize] unsignedLongLongValue]; if (error) { OWSLogError(@"Couldn't fetch file size[%@]: %@", filePath, error); return nil; } else { return @(fileSize); } } @end #pragma mark - NSString *OWSTemporaryDirectory(void) { static NSString *dirPath; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ NSString *dirName = [NSString stringWithFormat:@"ows_temp_%@", NSUUID.UUID.UUIDString]; dirPath = [NSTemporaryDirectory() stringByAppendingPathComponent:dirName]; BOOL success = [OWSFileSystem ensureDirectoryExists:dirPath fileProtectionType:NSFileProtectionComplete]; OWSCAssert(success); }); return dirPath; } NSString *OWSTemporaryDirectoryAccessibleAfterFirstAuth(void) { NSString *dirPath = NSTemporaryDirectory(); BOOL success = [OWSFileSystem ensureDirectoryExists:dirPath fileProtectionType:NSFileProtectionCompleteUntilFirstUserAuthentication]; OWSCAssert(success); return dirPath; } void ClearOldTemporaryDirectoriesSync(void) { // Ignore the "current" temp directory. NSString *currentTempDirName = OWSTemporaryDirectory().lastPathComponent; NSDate *thresholdDate = CurrentAppContext().appLaunchTime; NSString *dirPath = NSTemporaryDirectory(); NSError *error; NSArray *fileNames = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:dirPath error:&error]; if (error) { OWSCFailDebug(@"contentsOfDirectoryAtPath error: %@", error); return; } for (NSString *fileName in fileNames) { if (!CurrentAppContext().isAppForegroundAndActive) { // Abort if app not active. return; } if ([fileName isEqualToString:currentTempDirName]) { continue; } NSString *filePath = [dirPath stringByAppendingPathComponent:fileName]; // Delete files with either: // // a) "ows_temp" name prefix. // b) modified time before app launch time. if (![fileName hasPrefix:@"ows_temp"]) { NSError *error; NSDictionary *attributes = [[NSFileManager defaultManager] attributesOfItemAtPath:filePath error:&error]; if (!attributes || error) { // This is fine; the file may have been deleted since we found it. OWSLogError(@"Could not get attributes of file or directory at: %@", filePath); continue; } // Don't delete files which were created in the last N minutes. NSDate *creationDate = attributes.fileModificationDate; if ([creationDate isAfterDate:thresholdDate]) { OWSLogInfo(@"Skipping file due to age: %f", fabs([creationDate timeIntervalSinceNow])); continue; } } OWSLogVerbose(@"Removing temp file or directory: %@", filePath); if (![OWSFileSystem deleteFile:filePath]) { // This can happen if the app launches before the phone is unlocked. // Clean up will occur when app becomes active. OWSLogWarn(@"Could not delete old temp directory: %@", filePath); } } } // NOTE: We need to call this method on launch _and_ every time the app becomes active, // since file protection may prevent it from succeeding in the background. void ClearOldTemporaryDirectories(void) { // We use the lowest priority queue for this, and wait N seconds // to avoid interfering with app startup. dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3.f * NSEC_PER_SEC)), dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{ ClearOldTemporaryDirectoriesSync(); }); } NS_ASSUME_NONNULL_END