Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add proof-of-concept backup proto file export/import
- Loading branch information
1 parent
784b888
commit ef7668f
Showing
8 changed files
with
574 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
113 changes: 113 additions & 0 deletions
113
SignalServiceKit/CloudBackup/CloudBackupManager+Shims.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) | ||
} | ||
) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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
101
SignalServiceKit/CloudBackup/CloudBackupManagerImpl.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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() | ||
} | ||
} |
Oops, something went wrong.