From 6eb1ce682a9e55777ec1c4b92f8a50c6b8224ad7 Mon Sep 17 00:00:00 2001 From: Michael Kirk Date: Fri, 16 Feb 2018 23:27:38 -0800 Subject: [PATCH 1/2] Debug file browser // FREEBIE --- Signal.xcodeproj/project.pbxproj | 4 + Signal/src/Signal-Bridging-Header.h | 1 + .../DebugUI/DebugUIFileBrowser.swift | 378 ++++++++++++++++++ .../DebugUI/DebugUITableViewController.m | 17 + 4 files changed, 400 insertions(+) create mode 100644 Signal/src/ViewControllers/DebugUI/DebugUIFileBrowser.swift diff --git a/Signal.xcodeproj/project.pbxproj b/Signal.xcodeproj/project.pbxproj index ef74f7690..bc960aaa7 100644 --- a/Signal.xcodeproj/project.pbxproj +++ b/Signal.xcodeproj/project.pbxproj @@ -294,6 +294,7 @@ 45A663C51F92EC760027B59E /* GroupTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45A663C41F92EC760027B59E /* GroupTableViewCell.swift */; }; 45A6DAD61EBBF85500893231 /* ReminderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45A6DAD51EBBF85500893231 /* ReminderView.swift */; }; 45AE48511E0732D6004D96C2 /* TurnServerInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45AE48501E0732D6004D96C2 /* TurnServerInfo.swift */; }; + 45B27B862037FFB400A539DF /* DebugUIFileBrowser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45B27B852037FFB400A539DF /* DebugUIFileBrowser.swift */; }; 45B9EE9C200E91FB005D2F2D /* MediaDetailViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 45B9EE9B200E91FB005D2F2D /* MediaDetailViewController.m */; }; 45BB93381E688E14001E3939 /* UIDevice+featureSupport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45BB93371E688E14001E3939 /* UIDevice+featureSupport.swift */; }; 45BC829D1FD9C4B400011CF3 /* ShareViewDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45BC829C1FD9C4B400011CF3 /* ShareViewDelegate.swift */; }; @@ -837,6 +838,7 @@ 45A6DAD51EBBF85500893231 /* ReminderView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReminderView.swift; sourceTree = ""; }; 45AE48501E0732D6004D96C2 /* TurnServerInfo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TurnServerInfo.swift; sourceTree = ""; }; 45B201741DAECBFD00C461E0 /* Signal-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Signal-Bridging-Header.h"; sourceTree = ""; }; + 45B27B852037FFB400A539DF /* DebugUIFileBrowser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebugUIFileBrowser.swift; sourceTree = ""; }; 45B9EE9A200E91FB005D2F2D /* MediaDetailViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MediaDetailViewController.h; sourceTree = ""; }; 45B9EE9B200E91FB005D2F2D /* MediaDetailViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MediaDetailViewController.m; sourceTree = ""; }; 45BB93371E688E14001E3939 /* UIDevice+featureSupport.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIDevice+featureSupport.swift"; sourceTree = ""; }; @@ -1474,6 +1476,7 @@ 343A65941FC47D5E000477A1 /* DebugUISyncMessages.m */, 34D8C0251ED3673300188D7C /* DebugUITableViewController.h */, 34D8C0261ED3673300188D7C /* DebugUITableViewController.m */, + 45B27B852037FFB400A539DF /* DebugUIFileBrowser.swift */, ); path = DebugUI; sourceTree = ""; @@ -2896,6 +2899,7 @@ 34D1F0AC1F867BFC0066283D /* OWSExpirationTimerView.m in Sources */, 76EB063A18170B33006006FC /* FunctionalUtil.m in Sources */, 34F308A21ECB469700BB7697 /* OWSBezierPathView.m in Sources */, + 45B27B862037FFB400A539DF /* DebugUIFileBrowser.swift in Sources */, 34CE88E71F2FB9A10098030F /* ProfileViewController.m in Sources */, 34B0796D1FCF46B100E248C2 /* MainAppContext.m in Sources */, 34E3EF101EFC2684007F6822 /* DebugUIPage.m in Sources */, diff --git a/Signal/src/Signal-Bridging-Header.h b/Signal/src/Signal-Bridging-Header.h index 6457ca4c9..f2158e0f9 100644 --- a/Signal/src/Signal-Bridging-Header.h +++ b/Signal/src/Signal-Bridging-Header.h @@ -12,6 +12,7 @@ #import "DebugUIPage.h" #import "FingerprintViewController.h" #import "HomeViewController.h" +#import "DebugUITableViewController.h" #import "MediaDetailViewController.h" #import "NSString+OWS.h" #import "NotificationsManager.h" diff --git a/Signal/src/ViewControllers/DebugUI/DebugUIFileBrowser.swift b/Signal/src/ViewControllers/DebugUI/DebugUIFileBrowser.swift new file mode 100644 index 000000000..0b4fe6803 --- /dev/null +++ b/Signal/src/ViewControllers/DebugUI/DebugUIFileBrowser.swift @@ -0,0 +1,378 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +class DebugUIFileBrowser: OWSTableViewController { + + // MARK: Dependencies + var fileManager: FileManager { + return FileManager.default + } + + // MARK: Overrides + let fileURL: URL + + init(fileURL: URL) { + self.fileURL = fileURL + + super.init(nibName: nil, bundle: nil) + + self.contents = buildContents() + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + let titleLabel = UILabel() + titleLabel.text = "\(fileURL)" + titleLabel.sizeToFit() + titleLabel.textColor = UIColor.white + titleLabel.lineBreakMode = .byTruncatingHead + self.navigationItem.titleView = titleLabel + } + + fileprivate func updateContents() { + self.contents = buildContents() + self.tableView.reloadData() + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + // In case files were added / removed in child view controller + updateContents() + } + + func buildContents() -> OWSTableContents { + let contents = OWSTableContents() + + let isDirectoryPtr: UnsafeMutablePointer = UnsafeMutablePointer.allocate(capacity: 1) + guard fileManager.fileExists(atPath: fileURL.path, isDirectory: isDirectoryPtr) else { + contents.title = "File not found: \(fileURL)" + return contents + } + + let isDirectory: Bool = isDirectoryPtr.pointee.boolValue + + if isDirectory { + var fileItems: [OWSTableItem] = [] + let resourceKeys: [URLResourceKey] = [.isDirectoryKey] + + let directoryContents: [URL] = { + do { + return try fileManager.contentsOfDirectory(at: fileURL, + includingPropertiesForKeys: resourceKeys) + } catch { + owsFail("\(self.logTag) contentsOfDirectory(\(fileURL) failed with error: \(error)") + return [] + } + }() + + fileItems = directoryContents.map { fileInDirectory in + let fileIcon: String = { + do { + guard let isDirectory = try fileInDirectory.resourceValues(forKeys: Set(resourceKeys)).isDirectory else { + owsFail("\(logTag) unable to check isDirectory for file: \(fileInDirectory)") + return "" + } + + return isDirectory ? "📁 " : "" + } catch { + owsFail("\(logTag) failed to check isDirectory for file: \(fileInDirectory) with error: \(error)") + return "" + } + }() + + let labelText = "\(fileIcon)\(fileInDirectory.lastPathComponent)" + + return OWSTableItem.disclosureItem(withText: labelText) { [weak self] in + let subBrowser = DebugUIFileBrowser(fileURL: fileInDirectory) + self?.navigationController?.pushViewController(subBrowser, animated: true) + } + } + + let filesSection = OWSTableSection(title: "Dir with \(fileItems.count) files", items: fileItems) + contents.addSection(filesSection) + } // end `if isDirectory` + + let attributeItems: [OWSTableItem] = { + do { + let attributes: [FileAttributeKey: Any] = try fileManager.attributesOfItem(atPath: fileURL.path) + return attributes.map { (fileAttribute: FileAttributeKey, value: Any) in + let title = fileAttribute.rawValue.replacingOccurrences(of: "NSFile", with: "") + return OWSTableItem(title: "\(title): \(value)") { + OWSAlerts.showAlert(withTitle: title, message: "\(value)") + } + } + } catch { + owsFail("\(logTag) failed getting attributes for file at path: \(fileURL)") + return [] + } + }() + let attributesSection = OWSTableSection(title: "Attributes", items: attributeItems) + contents.addSection(attributesSection) + + var managementItems = [ + OWSTableItem.disclosureItem(withText: "✎ Rename") { [weak self] in + guard let strongSelf = self else { + return + } + + let alert = UIAlertController(title: "Rename File", + message: "Will be created in \(strongSelf.fileURL.lastPathComponent)", + preferredStyle: .alert) + + alert.addAction(OWSAlerts.cancelAction) + alert.addAction(UIAlertAction(title:"Rename \(strongSelf.fileURL.lastPathComponent)", style:.default) { _ in + guard let textField = alert.textFields?.first else { + owsFail("missing text field") + return + } + + guard let inputString = textField.text, inputString.count >= 4 else { + OWSAlerts.showAlert(withTitle: "new file name missing or less than 4 chars") + return + } + + let newURL = strongSelf.fileURL.deletingLastPathComponent().appendingPathComponent(inputString) + + do { + try strongSelf.fileManager.moveItem(at: strongSelf.fileURL, to: newURL) + + Logger.debug("\(strongSelf) moved \(strongSelf.fileURL) -> \(newURL)") + strongSelf.navigationController?.popViewController(animated: true) + } catch { + owsFail("\(strongSelf) failed to move \(strongSelf.fileURL) -> \(newURL) with error: \(error)") + } + }) + + alert.addTextField { textField in + textField.placeholder = "New Name" + textField.text = strongSelf.fileURL.lastPathComponent + } + + strongSelf.present(alert, animated: true) + }, + + OWSTableItem.disclosureItem(withText: "➡ Move") { [weak self] in + guard let strongSelf = self else { + return + } + + let fileURL: URL = strongSelf.fileURL + let filename: String = fileURL.lastPathComponent + let oldDirectory: URL = fileURL.deletingLastPathComponent() + + let alert = UIAlertController(title: "Moving File: \(filename)", + message: "Currently in: \(oldDirectory)", + preferredStyle: .alert) + + alert.addAction(OWSAlerts.cancelAction) + alert.addAction(UIAlertAction(title:"Moving \(filename)", style:.default) { _ in + guard let textField = alert.textFields?.first else { + owsFail("missing text field") + return + } + + guard let inputString = textField.text, inputString.count >= 4 else { + OWSAlerts.showAlert(withTitle: "new file dir missing or less than 4 chars") + return + } + + let newURL = URL(fileURLWithPath: inputString).appendingPathComponent(filename) + + do { + try strongSelf.fileManager.moveItem(at: fileURL, to: newURL) + + Logger.debug("\(strongSelf) moved \(fileURL) -> \(newURL)") + strongSelf.navigationController?.popViewController(animated: true) + } catch { + owsFail("\(strongSelf) failed to move \(fileURL) -> \(newURL) with error: \(error)") + } + }) + + alert.addTextField { textField in + textField.placeholder = "New Directory" + textField.text = oldDirectory.path + } + + strongSelf.present(alert, animated: true) + }, + + OWSTableItem.disclosureItem(withText: "❌ Delete") { [weak self] in + guard let strongSelf = self else { + return + } + + OWSAlerts.showConfirmationAlert(withTitle: "Delete \(strongSelf.fileURL.path)?") { _ in + Logger.debug("\(strongSelf.logTag) deleting file at \(strongSelf.fileURL.path)") + do { + try strongSelf.fileManager.removeItem(atPath: strongSelf.fileURL.path) + strongSelf.navigationController?.popViewController(animated: true) + } catch { + owsFail("\(strongSelf.logTag) failed to remove item: \(strongSelf.fileURL) with error: \(error)") + } + } + }, + + OWSTableItem.disclosureItem(withText: "📋 Copy Path to Clipboard") { [weak self] in + guard let strongSelf = self else { + return + } + + UIPasteboard.general.string = strongSelf.fileURL.path + + let alert = UIAlertController(title: "Path Copied to Clipboard!", + message: "\(strongSelf.fileURL.path)", + preferredStyle: .alert) + alert.addAction(UIAlertAction(title: "Copy Filename Instead", style: .default) { _ in + UIPasteboard.general.string = strongSelf.fileURL.lastPathComponent + }) + + alert.addAction(UIAlertAction(title: "Dismiss", style: .default)) + + strongSelf.present(alert, animated: true) + }, + + OWSTableItem.disclosureItem(withText: "🔒 Set File Protection") { [weak self] in + guard let strongSelf = self else { + return + } + + let fileURL = strongSelf.fileURL + + let currentFileProtection: FileProtectionType? = { + do { + let attributes = try strongSelf.fileManager.attributesOfItem(atPath: fileURL.path) + return attributes[FileAttributeKey.protectionKey] as? FileProtectionType + } catch { + owsFail("\(strongSelf.logTag) failed to get current file protection for file: \(fileURL)") + return nil + } + }() + + let actionSheet = UIAlertController(title: "Set file protection level", + message: "Currently: \(currentFileProtection?.rawValue ?? "Unknown")", + preferredStyle: .actionSheet) + + let protections: [FileProtectionType] = [.none, .complete, .completeUnlessOpen, .completeUntilFirstUserAuthentication] + protections.forEach { (protection: FileProtectionType) in + actionSheet.addAction(UIAlertAction(title: "\(protection.rawValue.replacingOccurrences(of:"NSFile", with: ""))", style: .default) { (_: UIAlertAction) in + Logger.debug("\(strongSelf.logTag) chose protection: \(protection) for file: \(fileURL)") + let fileAttributes: [FileAttributeKey: Any] = [.protectionKey: protection] + do { + try strongSelf.fileManager.setAttributes(fileAttributes, ofItemAtPath: strongSelf.fileURL.path) + Logger.debug("\(strongSelf.logTag) updated file protection at path:\(fileURL.path) to: \(protection.rawValue)") + strongSelf.updateContents() + } catch { + owsFail("\(strongSelf.logTag) failed to update file protection at path:\(fileURL.path) with error: \(error)") + } + }) + } + actionSheet.addAction(OWSAlerts.cancelAction) + + strongSelf.present(actionSheet, animated: true) + } + ] + + if isDirectory { + let createFileItem = OWSTableItem.disclosureItem(withText: "📝 Create File in this Dir") { [weak self] in + guard let strongSelf = self else { + return + } + + let alert = UIAlertController(title: "Name of file", + message: "Will be created in \(strongSelf.fileURL.lastPathComponent)", + preferredStyle: .alert) + + alert.addAction(OWSAlerts.cancelAction) + alert.addAction(UIAlertAction(title:"Create", style:.default) { _ in + guard let textField = alert.textFields?.first else { + owsFail("missing text field") + return + } + + guard let inputString = textField.text, inputString.count >= 4 else { + OWSAlerts.showAlert(withTitle: "file name missing or less than 4 chars") + return + } + + let newPath = strongSelf.fileURL.appendingPathComponent(inputString).path + + Logger.debug("\(strongSelf.logTag) creating file at \(newPath)") + strongSelf.fileManager.createFile(atPath: newPath, contents: nil) + + strongSelf.updateContents() + }) + + alert.addTextField { textField in + textField.placeholder = "File Name" + } + + strongSelf.present(alert, animated: true) + } + + managementItems.append(createFileItem) + + let createDirItem = OWSTableItem.disclosureItem(withText: "📁 Create Dir in this Dir") { [weak self] in + guard let strongSelf = self else { + return + } + + let alert = UIAlertController(title: "Name of Dir", + message: "Will be created in \(strongSelf.fileURL.lastPathComponent)", + preferredStyle: .alert) + + alert.addAction(OWSAlerts.cancelAction) + alert.addAction(UIAlertAction(title:"Create", style:.default) { _ in + guard let textField = alert.textFields?.first else { + owsFail("missing text field") + return + } + + guard let inputString = textField.text, inputString.count >= 4 else { + OWSAlerts.showAlert(withTitle: "dir name missing or less than 4 chars") + return + } + + let newPath = strongSelf.fileURL.appendingPathComponent(inputString).path + + Logger.debug("\(strongSelf.logTag) creating dir at \(newPath)") + do { + try strongSelf.fileManager.createDirectory(atPath: newPath, withIntermediateDirectories: false) + strongSelf.updateContents() + } catch { + owsFail("\(strongSelf.logTag) Failed to create dir: \(newPath) with error: \(error)") + } + }) + + alert.addTextField { textField in + textField.placeholder = "Dir Name" + } + + strongSelf.present(alert, animated: true) + } + managementItems.append(createDirItem) + + } else { // if not directory + + let shareItem = OWSTableItem.disclosureItem(withText: "📩 Share") { [weak self] in + guard let strongSelf = self else { + return + } + + AttachmentSharing.showShareUI(for: strongSelf.fileURL) + } + managementItems.append(shareItem) + } + + let fileType = isDirectory ? "Dir" : "File" + let filesSection = OWSTableSection(title: "\(fileType): \(fileURL.lastPathComponent)", items: managementItems) + contents.addSection(filesSection) + + contents.title = "\(fileType): \(fileURL)" + return contents + } +} diff --git a/Signal/src/ViewControllers/DebugUI/DebugUITableViewController.m b/Signal/src/ViewControllers/DebugUI/DebugUITableViewController.m index 007a443e0..2502f455b 100644 --- a/Signal/src/ViewControllers/DebugUI/DebugUITableViewController.m +++ b/Signal/src/ViewControllers/DebugUI/DebugUITableViewController.m @@ -73,6 +73,7 @@ NS_ASSUME_NONNULL_BEGIN contents.title = @"Debug: Conversation"; NSMutableArray *subsectionItems = [NSMutableArray new]; + [subsectionItems addObject:[self itemForSubsection:[DebugUIMessages new] viewController:viewController thread:thread]]; [subsectionItems @@ -92,6 +93,22 @@ NS_ASSUME_NONNULL_BEGIN addObject:[self itemForSubsection:[DebugUIStress new] viewController:viewController thread:thread]]; [subsectionItems addObject:[self itemForSubsection:[DebugUISyncMessages new] viewController:viewController thread:thread]]; + OWSTableItem *sharedDataFileBrowserItem = [OWSTableItem + disclosureItemWithText:@"📁 Shared Container" + actionBlock:^{ + NSURL *baseURL = [NSURL URLWithString:[OWSFileSystem appSharedDataDirectoryPath]]; + DebugUIFileBrowser *fileBrowser = [[DebugUIFileBrowser alloc] initWithFileURL:baseURL]; + [viewController.navigationController pushViewController:fileBrowser animated:YES]; + }]; + [subsectionItems addObject:sharedDataFileBrowserItem]; + OWSTableItem *documentsFileBrowserItem = [OWSTableItem + disclosureItemWithText:@"📁 Document Dir" + actionBlock:^{ + NSURL *baseURL = [NSURL URLWithString:[OWSFileSystem appDocumentDirectoryPath]]; + DebugUIFileBrowser *fileBrowser = [[DebugUIFileBrowser alloc] initWithFileURL:baseURL]; + [viewController.navigationController pushViewController:fileBrowser animated:YES]; + }]; + [subsectionItems addObject:documentsFileBrowserItem]; [subsectionItems addObject:[self itemForSubsection:[DebugUIMisc new] viewController:viewController thread:thread]]; [contents addSection:[OWSTableSection sectionWithTitle:@"Sections" items:subsectionItems]]; From 033505afd71172dc54334cfae0a45cad365abaff Mon Sep 17 00:00:00 2001 From: Michael Kirk Date: Sun, 18 Feb 2018 15:40:25 -0500 Subject: [PATCH 2/2] Remove slow file protection updates from launch path To avoid blocking launch, file protection is now updated async for most moved files. Out of paranoia, the database files are also update redundantly on a sync code path. It's still critical that we update permissions recursively for two reasons: 1. Updating a containing directories FileProtection does not affect existing files in that directory. 2. Because we've changed the containers default file protection level (from unspecified to NSFileProtectionComplete), some existing files will have there file protection updated upon launching Signal 2.20. It's not clear to me which files this affects, and I haven't found any relevant documentation, but from observation, it seems to affect any top-level files in the container. Regardless, we're now doing the right thing: after launching 2.20, ensure all file permissions are what we expect. Also removed no-op file protection on legacy db files. They've already been moved by the time this method runs in AppSetup. // FREEBIE --- .../src/Storage/TSStorageManager.m | 19 +++++++++++++------ SignalServiceKit/src/Util/OWSFileSystem.m | 7 ++++++- 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/SignalServiceKit/src/Storage/TSStorageManager.m b/SignalServiceKit/src/Storage/TSStorageManager.m index 47d2775ed..01fdceced 100644 --- a/SignalServiceKit/src/Storage/TSStorageManager.m +++ b/SignalServiceKit/src/Storage/TSStorageManager.m @@ -155,12 +155,6 @@ void runAsyncRegistrationsForStorage(OWSStorage *storage) DDLogInfo(@"%@ \t SHM file size: %@", self.logTag, [OWSFileSystem fileSizeOfPath:self.sharedDataDatabaseFilePath_SHM]); DDLogInfo(@"%@ \t WAL file size: %@", self.logTag, [OWSFileSystem fileSizeOfPath:self.sharedDataDatabaseFilePath_WAL]); - // The old database location was in the Document directory, - // so protect the database files individually. - [OWSFileSystem protectFileOrFolderAtPath:self.legacyDatabaseFilePath]; - [OWSFileSystem protectFileOrFolderAtPath:self.legacyDatabaseFilePath_SHM]; - [OWSFileSystem protectFileOrFolderAtPath:self.legacyDatabaseFilePath_WAL]; - // Protect the entire new database directory. [OWSFileSystem protectFileOrFolderAtPath:self.sharedDataDatabaseDirPath]; } @@ -228,6 +222,19 @@ void runAsyncRegistrationsForStorage(OWSStorage *storage) + (void)migrateToSharedData { + // We protect the db files here, which is somewhat redundant with what will happen in + // `moveAppFilePath:` which also ensures file protection. + // However that method dispatches async, since it can take a while with large attachment directories. + // + // Since we only have three files here it'll be quick to do it sync, and we want to make + // sure it happens as part of the migration. + // + // FileProtection attributes move with the file, so we do it on the legacy files before moving + // them. + [OWSFileSystem protectFileOrFolderAtPath:self.legacyDatabaseFilePath]; + [OWSFileSystem protectFileOrFolderAtPath:self.legacyDatabaseFilePath_SHM]; + [OWSFileSystem protectFileOrFolderAtPath:self.legacyDatabaseFilePath_WAL]; + [OWSFileSystem moveAppFilePath:self.legacyDatabaseFilePath sharedDataFilePath:self.sharedDataDatabaseFilePath exceptionName:TSStorageManagerExceptionName_CouldNotMoveDatabaseFile]; diff --git a/SignalServiceKit/src/Util/OWSFileSystem.m b/SignalServiceKit/src/Util/OWSFileSystem.m index 7f7604c55..06efb1f54 100644 --- a/SignalServiceKit/src/Util/OWSFileSystem.m +++ b/SignalServiceKit/src/Util/OWSFileSystem.m @@ -31,6 +31,7 @@ NS_ASSUME_NONNULL_BEGIN success = success && [self protectFileOrFolderAtPath:filePath]; } + DDLogInfo(@"%@ protected contents at path: %@", self.logTag, path); return success; } @@ -147,7 +148,11 @@ NS_ASSUME_NONNULL_BEGIN fabs([startDate timeIntervalSinceNow])); // Ensure all files moved have the proper data protection class. - [self protectRecursiveContentsAtPath:newFilePath]; + // 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]; + }); } + (BOOL)ensureDirectoryExists:(NSString *)dirPath