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]]; 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