Skip to content

Commit

Permalink
Add proof-of-concept backup proto file export/import
Browse files Browse the repository at this point in the history
  • Loading branch information
harry-signal committed Oct 20, 2023
1 parent 784b888 commit ef7668f
Show file tree
Hide file tree
Showing 8 changed files with 574 additions and 1 deletion.
24 changes: 24 additions & 0 deletions Signal.xcodeproj/project.pbxproj
Expand Up @@ -848,6 +848,10 @@
6659A0352A7C6E0900066AB7 /* PersistPreKeyTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6659A0342A7C6E0900066AB7 /* PersistPreKeyTask.swift */; };
6659A0392A81933B00066AB7 /* ProvisioningPermissionsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6659A0382A81933B00066AB7 /* ProvisioningPermissionsViewController.swift */; };
6659CCB129CD4650000C24C0 /* RegistrationConfirmModeSwitchViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6659CCB029CD4650000C24C0 /* RegistrationConfirmModeSwitchViewController.swift */; };
665C0D5C2ADF538100539A37 /* CloudBackupManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 665C0D5B2ADF538100539A37 /* CloudBackupManager.swift */; };
665C0D5E2ADF53E200539A37 /* CloudBackupManagerImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 665C0D5D2ADF53E200539A37 /* CloudBackupManagerImpl.swift */; };
665C0D602ADF57D000539A37 /* CloudBackupManager+Shims.swift in Sources */ = {isa = PBXBuildFile; fileRef = 665C0D5F2ADF57D000539A37 /* CloudBackupManager+Shims.swift */; };
665C0D622AE0552900539A37 /* CloudBackupProtoFileStreams.swift in Sources */ = {isa = PBXBuildFile; fileRef = 665C0D612AE0552900539A37 /* CloudBackupProtoFileStreams.swift */; };
665C0D6C2AE0776700539A37 /* Backup.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = 665C0D6B2AE0776700539A37 /* Backup.pb.swift */; };
665C0D6E2AE07A8A00539A37 /* BackupProto.swift in Sources */ = {isa = PBXBuildFile; fileRef = 665C0D6D2AE07A8A00539A37 /* BackupProto.swift */; };
665EF86D290C385B00F490D2 /* OWSNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 665EF86C290C385B00F490D2 /* OWSNavigationController.swift */; };
Expand Down Expand Up @@ -3429,6 +3433,10 @@
6659A0342A7C6E0900066AB7 /* PersistPreKeyTask.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PersistPreKeyTask.swift; sourceTree = "<group>"; };
6659A0382A81933B00066AB7 /* ProvisioningPermissionsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProvisioningPermissionsViewController.swift; sourceTree = "<group>"; };
6659CCB029CD4650000C24C0 /* RegistrationConfirmModeSwitchViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RegistrationConfirmModeSwitchViewController.swift; sourceTree = "<group>"; };
665C0D5B2ADF538100539A37 /* CloudBackupManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloudBackupManager.swift; sourceTree = "<group>"; };
665C0D5D2ADF53E200539A37 /* CloudBackupManagerImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloudBackupManagerImpl.swift; sourceTree = "<group>"; };
665C0D5F2ADF57D000539A37 /* CloudBackupManager+Shims.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CloudBackupManager+Shims.swift"; sourceTree = "<group>"; };
665C0D612AE0552900539A37 /* CloudBackupProtoFileStreams.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloudBackupProtoFileStreams.swift; sourceTree = "<group>"; };
665C0D6B2AE0776700539A37 /* Backup.pb.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Backup.pb.swift; sourceTree = "<group>"; };
665C0D6D2AE07A8A00539A37 /* BackupProto.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BackupProto.swift; sourceTree = "<group>"; };
665EF86C290C385B00F490D2 /* OWSNavigationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OWSNavigationController.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -6876,6 +6884,17 @@
path = PreKeys;
sourceTree = "<group>";
};
665C0D5A2ADF537000539A37 /* CloudBackup */ = {
isa = PBXGroup;
children = (
665C0D5F2ADF57D000539A37 /* CloudBackupManager+Shims.swift */,
665C0D5B2ADF538100539A37 /* CloudBackupManager.swift */,
665C0D5D2ADF53E200539A37 /* CloudBackupManagerImpl.swift */,
665C0D612AE0552900539A37 /* CloudBackupProtoFileStreams.swift */,
);
path = CloudBackup;
sourceTree = "<group>";
};
666BAB0E2980B76B00867196 /* Dependencies */ = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -8819,6 +8838,7 @@
F9C5C9BA289453B100548EEE /* Account */,
F945FE482984795A00C835C7 /* Calls */,
D9C2D78529A80BE700D79715 /* ChangePhoneNumber */,
665C0D5A2ADF537000539A37 /* CloudBackup */,
F9C5C9CC289453B100548EEE /* Contacts */,
6600F36A298DAA4F00B1EDB7 /* DateProvider */,
666BAB0E2980B76B00867196 /* Dependencies */,
Expand Down Expand Up @@ -12359,6 +12379,10 @@
500AEE072A4DF48700371F05 /* ChatColorSettingStore.swift in Sources */,
664160D029A6D60A00F5BA85 /* ChatServiceAuth.swift in Sources */,
F9C5CCF2289453B300548EEE /* ChunkedInputStream.swift in Sources */,
665C0D602ADF57D000539A37 /* CloudBackupManager+Shims.swift in Sources */,
665C0D5C2ADF538100539A37 /* CloudBackupManager.swift in Sources */,
665C0D5E2ADF53E200539A37 /* CloudBackupManagerImpl.swift in Sources */,
665C0D622AE0552900539A37 /* CloudBackupProtoFileStreams.swift in Sources */,
F9C5CDFC289453B400548EEE /* Collection+OWS.swift in Sources */,
F9C5CCE5289453B300548EEE /* Contact+Swift.swift in Sources */,
F9C5CCB5289453B300548EEE /* Contact.m in Sources */,
Expand Down
77 changes: 76 additions & 1 deletion Signal/src/ViewControllers/DebugUI/DebugUIMisc.swift
Expand Up @@ -10,7 +10,7 @@ import SignalUI

#if USE_DEBUG_UI

class DebugUIMisc: DebugUIPage, Dependencies {
class DebugUIMisc: NSObject, DebugUIPage, Dependencies {

let name = "Misc."

Expand Down Expand Up @@ -179,6 +179,14 @@ class DebugUIMisc: DebugUIPage, Dependencies {

OWSTableItem(title: "Enable edit send education prompt", actionBlock: {
DebugUIMisc.enableEditMessagePromptMessage()
}),

OWSTableItem(title: "Create cloud backup proto", actionBlock: {
DebugUIMisc.createCloudBackupProto()
}),

OWSTableItem(title: "Import cloud backup proto", actionBlock: { [weak self] in
self?.importCloudBackupProto()
})
]
return OWSTableSection(title: name, items: items)
Expand Down Expand Up @@ -511,6 +519,73 @@ class DebugUIMisc: DebugUIPage, Dependencies {
)
}
}

private static func createCloudBackupProto() {
let vc = UIApplication.shared.frontmostViewController!
ModalActivityIndicatorViewController.present(fromViewController: vc, canCancel: false, backgroundBlock: { modal in
Task {
do {
let fileUrl = try await DependenciesBridge.shared.cloudBackupManager.createBackup()
await MainActor.run {
let activityVC = UIActivityViewController(
activityItems: [fileUrl],
applicationActivities: nil
)
let vc = UIApplication.shared.frontmostViewController!
activityVC.popoverPresentationController?.sourceView = vc.view
activityVC.completionWithItemsHandler = { _, _, _, _ in
modal.dismiss()
}
vc.present(activityVC, animated: true)
}
} catch {
// Do nothing
await modal.dismiss()
}
}
})
}

private func importCloudBackupProto() {
let vc = UIApplication.shared.frontmostViewController!
guard #available(iOS 14.0, *) else {
return
}
let documentPicker = UIDocumentPickerViewController(forOpeningContentTypes: [.item], asCopy: true)
documentPicker.delegate = self
documentPicker.allowsMultipleSelection = false
vc.present(documentPicker, animated: true)
}
}

extension DebugUIMisc: UIDocumentPickerDelegate {

func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) {
guard let fileUrl = urls.first else {
return
}
let vc = UIApplication.shared.frontmostViewController!
ModalActivityIndicatorViewController.present(fromViewController: vc, canCancel: false, backgroundBlock: { modal in
Task {
do {
try await DependenciesBridge.shared.cloudBackupManager.importBackup(fileUrl: fileUrl)
await MainActor.run {
modal.dismiss {
let vc = UIApplication.shared.frontmostViewController!
vc.presentToast(text: "Done!")
}
}
} catch {
await MainActor.run {
modal.dismiss {
let vc = UIApplication.shared.frontmostViewController!
vc.presentToast(text: "Failed!")
}
}
}
}
})
}
}

#endif
113 changes: 113 additions & 0 deletions SignalServiceKit/CloudBackup/CloudBackupManager+Shims.swift
@@ -0,0 +1,113 @@
//
// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//

import Foundation
import GRDB

public enum CloudBackup {}

extension CloudBackup {
public enum Shims {
public typealias SignalRecipientFetcher = _CloudBackup_SignalRecipientShim
public typealias TSInteractionFetcher = _CloudBackup_TSInteractionShim
public typealias TSThreadFetcher = _CloudBackup_TSThreadShim
}

public enum Wrappers {
public typealias SignalRecipientFetcher = _CloudBackup_SignalRecipientWrapper
public typealias TSInteractionFetcher = _CloudBackup_TSInteractionWrapper
public typealias TSThreadFetcher = _CloudBackup_TSThreadWrapper
}
}

// MARK: - SignalRecipient

public protocol _CloudBackup_SignalRecipientShim {

func enumerateAll(tx: DBReadTransaction, block: @escaping (SignalRecipient) -> Void)
}

public class _CloudBackup_SignalRecipientWrapper: _CloudBackup_SignalRecipientShim {

public init() {}

public func enumerateAll(tx: DBReadTransaction, block: @escaping (SignalRecipient) -> Void) {
SignalRecipient.anyEnumerate(
transaction: SDSDB.shimOnlyBridge(tx),
block: { recipient, _ in
block(recipient)
}
)
}
}

// MARK: - TSInteraction

public protocol _CloudBackup_TSInteractionShim {

func enumerateAllTextOnlyMessages(tx: DBReadTransaction, block: @escaping (TSMessage) -> Void)
}

public class _CloudBackup_TSInteractionWrapper: _CloudBackup_TSInteractionShim {

public init() {}

public func enumerateAllTextOnlyMessages(tx: DBReadTransaction, block: @escaping (TSMessage) -> Void) {
let emptyArraySerializedData = try! NSKeyedArchiver.archivedData(withRootObject: [String](), requiringSecureCoding: true)

let sql = """
SELECT *
FROM \(InteractionRecord.databaseTableName)
WHERE (
interaction.\(interactionColumn: .recordType) IS \(SDSRecordType.outgoingMessage.rawValue)
OR interaction.\(interactionColumn: .recordType) IS \(SDSRecordType.incomingMessage.rawValue)
)
AND (
\(interactionColumn: .attachmentIds) IS NULL
OR \(interactionColumn: .attachmentIds) == ?
)
"""
let arguments: StatementArguments = [emptyArraySerializedData]
let cursor = TSInteraction.grdbFetchCursor(
sql: sql,
arguments: arguments,
transaction: SDSDB.shimOnlyBridge(tx).unwrapGrdbRead
)

do {
while let interaction = try cursor.next() {
guard let message = interaction as? TSMessage else {
owsFailDebug("Interaction has unexpected type: \(type(of: interaction))")
continue
}

block(message)
}
} catch {
owsFailDebug("Failed to enumerate messages!")
}
}
}

// MARK: - TSThread

public protocol _CloudBackup_TSThreadShim {

func enumerateAll(tx: DBReadTransaction, block: @escaping (TSThread) -> Void)
}

public class _CloudBackup_TSThreadWrapper: _CloudBackup_TSThreadShim {

public init() {}

public func enumerateAll(tx: DBReadTransaction, block: @escaping (TSThread) -> Void) {
TSThread.anyEnumerate(
transaction: SDSDB.shimOnlyBridge(tx),
block: { thread, _ in
block(thread)
}
)
}
}
14 changes: 14 additions & 0 deletions SignalServiceKit/CloudBackup/CloudBackupManager.swift
@@ -0,0 +1,14 @@
//
// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//

import Foundation

public protocol CloudBackupManager {

/// Outputs file url the backup proto is located at.
func createBackup() async throws -> URL

func importBackup(fileUrl: URL) async throws
}
101 changes: 101 additions & 0 deletions SignalServiceKit/CloudBackup/CloudBackupManagerImpl.swift
@@ -0,0 +1,101 @@
//
// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//

import Foundation

public class NotImplementedError: Error {}

public class CloudBackupManagerImpl: CloudBackupManager {

private let dateProvider: DateProvider
private let db: DB
private let signalRecipientFetcher: CloudBackup.Shims.SignalRecipientFetcher
private let streamProvider: CloudBackupOutputStreamProvider
private let tsInteractionFetcher: CloudBackup.Shims.TSInteractionFetcher
private let tsThreadFetcher: CloudBackup.Shims.TSThreadFetcher

public init(
dateProvider: @escaping DateProvider,
db: DB,
signalRecipientFetcher: CloudBackup.Shims.SignalRecipientFetcher,
streamProvider: CloudBackupOutputStreamProvider,
tsInteractionFetcher: CloudBackup.Shims.TSInteractionFetcher,
tsThreadFetcher: CloudBackup.Shims.TSThreadFetcher
) {
self.dateProvider = dateProvider
self.db = db
self.signalRecipientFetcher = signalRecipientFetcher
self.streamProvider = streamProvider
self.tsInteractionFetcher = tsInteractionFetcher
self.tsThreadFetcher = tsThreadFetcher
}

public func createBackup() async throws -> URL {
guard FeatureFlags.cloudBackupFileAlpha else {
owsFailDebug("Should not be able to use backups!")
throw NotImplementedError()
}
return try await db.awaitableWrite { tx in
// The mother of all write transactions. Eventually we want to use
// a read tx, and use explicit locking to prevent other things from
// happening in the meantime (e.g. message processing) but for now
// hold the single write lock and call it a day.
return try self._createBackup(tx: tx)
}
}

public func importBackup(fileUrl: URL) async throws {
guard FeatureFlags.cloudBackupFileAlpha else {
owsFailDebug("Should not be able to use backups!")
throw NotImplementedError()
}
try await db.awaitableWrite { tx in
// This has to open one big write transaction; the alternative is
// to chunk them into separate writes. Nothing else should be happening
// in the app anyway.
try self._importBackup(fileUrl, tx: tx)
}
}

private func _createBackup(tx: DBWriteTransaction) throws -> URL {
let stream: CloudBackupOutputStream
switch streamProvider.openOutputFileStream() {
case .success(let streamResult):
stream = streamResult
case .failure(let error):
throw error
}

try writeHeader(stream: stream, tx: tx)

// TODO: write frames

return stream.closeFileStream()
}

private func writeHeader(stream: CloudBackupOutputStream, tx: DBWriteTransaction) throws {
let backupInfo = try BackupProtoBackupInfo.builder(
version: 1,
backupTime: dateProvider().ows_millisecondsSince1970
).build()
try stream.writeHeader(backupInfo)
}

private func _importBackup(_ fileUrl: URL, tx: DBWriteTransaction) throws {
let stream: CloudBackupInputStream
switch streamProvider.openInputFileStream(fileURL: fileUrl) {
case .success(let streamResult):
stream = streamResult
case .failure(let error):
throw error
}

_ = try stream.readHeader()

// TODO: read frames

return stream.closeFileStream()
}
}

0 comments on commit ef7668f

Please sign in to comment.