Add upload support for the main backup file in backupV2.

This commit is contained in:
Greyson Parrelli
2023-12-22 15:53:03 -05:00
committed by Clark Chen
parent f93a9a0f22
commit f4bcfca323
5 changed files with 180 additions and 20 deletions

View File

@@ -26,6 +26,7 @@ import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.recipients.RecipientId
import org.whispersystems.signalservice.api.NetworkResult
import org.whispersystems.signalservice.api.archive.ArchiveGetBackupInfoResponse
import org.whispersystems.signalservice.api.archive.ArchiveServiceCredential
import org.whispersystems.signalservice.api.push.ServiceId.ACI
import org.whispersystems.signalservice.api.push.ServiceId.PNI
@@ -165,14 +166,13 @@ object BackupRepository {
}
/**
* A simple test method that just hits various network endpoints. Only useful for the playground.
* Returns an object with details about the remote backup state.
*/
fun testNetworkInteractions() {
fun getRemoteBackupState(): NetworkResult<ArchiveGetBackupInfoResponse> {
val api = ApplicationDependencies.getSignalServiceAccountManager().archiveApi
val backupKey = SignalStore.svr().getOrCreateMasterKey().deriveBackupKey()
// Just running some sample requests
api
return api
.triggerBackupIdReservation(backupKey)
.then { getAuthCredential() }
.then { credential ->
@@ -182,13 +182,41 @@ object BackupRepository {
}
.then { credential ->
api.getBackupInfo(backupKey, credential)
.also { Log.i(TAG, "BackupInfoResult: $it") }
}
}
/**
* A simple test method that just hits various network endpoints. Only useful for the playground.
*
* @return True if successful, otherwise false.
*/
fun uploadBackupFile(backupStream: InputStream, backupStreamLength: Long): Boolean {
val api = ApplicationDependencies.getSignalServiceAccountManager().archiveApi
val backupKey = SignalStore.svr().getOrCreateMasterKey().deriveBackupKey()
return api
.triggerBackupIdReservation(backupKey)
.then { getAuthCredential() }
.then { credential ->
api.setPublicKey(backupKey, credential)
.also { Log.i(TAG, "PublicKeyResult: $it") }
.map { credential }
}
.then { credential ->
api.getMessageBackupUploadForm(backupKey, credential)
.also { Log.i(TAG, "UploadFormResult: $it") }
}.also { Log.i(TAG, "OverallResponse: $it") }
}
.then { form ->
api.getBackupResumableUploadUrl(form)
.also { Log.i(TAG, "ResumableUploadUrlResult: $it") }
.map { form to it }
}
.then { formAndUploadUrl ->
val (form, resumableUploadUrl) = formAndUploadUrl
api.uploadBackupFile(form, resumableUploadUrl, backupStream, backupStreamLength)
.also { Log.i(TAG, "UploadBackupFileResult: $it") }
}
.also { Log.i(TAG, "OverallResult: $it") } is NetworkResult.Success
}
/**

View File

@@ -33,9 +33,13 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.fragment.app.viewModels
import org.signal.core.ui.Buttons
import org.signal.core.ui.Dividers
import org.signal.core.ui.theme.SignalTheme
import org.signal.core.util.bytes
import org.signal.core.util.getLength
import org.signal.core.util.roundedString
import org.thoughtcrime.securesms.components.settings.app.internal.backup.InternalBackupPlaygroundViewModel.BackupState
import org.thoughtcrime.securesms.components.settings.app.internal.backup.InternalBackupPlaygroundViewModel.BackupUploadState
import org.thoughtcrime.securesms.components.settings.app.internal.backup.InternalBackupPlaygroundViewModel.ScreenState
import org.thoughtcrime.securesms.compose.ComposeFragment
@@ -98,7 +102,8 @@ class InternalBackupPlaygroundFragment : ComposeFragment() {
exportFileLauncher.launch(intent)
},
onTestNetworkClicked = { viewModel.testNetworkInteractions() }
onUploadToRemoteClicked = { viewModel.uploadBackupToRemote() },
onCheckRemoteBackupStateClicked = { viewModel.checkRemoteBackupState() }
)
}
@@ -115,7 +120,8 @@ fun Screen(
onImportFileClicked: () -> Unit = {},
onPlaintextClicked: () -> Unit = {},
onSaveToDiskClicked: () -> Unit = {},
onTestNetworkClicked: () -> Unit = {}
onUploadToRemoteClicked: () -> Unit = {},
onCheckRemoteBackupStateClicked: () -> Unit = {}
) {
Surface {
Column(
@@ -144,12 +150,9 @@ fun Screen(
) {
Text("Export")
}
Buttons.LargePrimary(
onClick = onTestNetworkClicked,
enabled = state.backupState == BackupState.EXPORT_DONE
) {
Text("Test network")
}
Dividers.Default()
Buttons.LargeTonal(
onClick = onImportMemoryClicked,
enabled = state.backupState == BackupState.EXPORT_DONE
@@ -172,7 +175,7 @@ fun Screen(
StateLabel("Export in progress...")
}
BackupState.EXPORT_DONE -> {
StateLabel("Export complete. Sitting in memory. You can click 'Import' to import that data, or you can save it to a file.")
StateLabel("Export complete. Sitting in memory. You can click 'Import' to import that data, save it to a file, or upload it to remote.")
Spacer(modifier = Modifier.height(8.dp))
@@ -184,6 +187,57 @@ fun Screen(
StateLabel("Import in progress...")
}
}
Dividers.Default()
Buttons.LargeTonal(
onClick = onCheckRemoteBackupStateClicked
) {
Text("Check remote backup state")
}
Spacer(modifier = Modifier.height(8.dp))
when (state.remoteBackupState) {
is InternalBackupPlaygroundViewModel.RemoteBackupState.Available -> {
StateLabel("Exists/allocated. Space used by media: ${state.remoteBackupState.response.usedSpace ?: 0} bytes (${state.remoteBackupState.response.usedSpace?.bytes?.inMebiBytes?.roundedString(3) ?: 0} MiB)")
}
InternalBackupPlaygroundViewModel.RemoteBackupState.GeneralError -> {
StateLabel("Hit an unknown error. Check the logs.")
}
InternalBackupPlaygroundViewModel.RemoteBackupState.NotFound -> {
StateLabel("Not found.")
}
InternalBackupPlaygroundViewModel.RemoteBackupState.Unknown -> {
StateLabel("Hit the button above to check the state.")
}
}
Spacer(modifier = Modifier.height(8.dp))
Buttons.LargePrimary(
onClick = onUploadToRemoteClicked,
enabled = state.backupState == BackupState.EXPORT_DONE
) {
Text("Upload to remote")
}
Spacer(modifier = Modifier.height(8.dp))
when (state.uploadState) {
BackupUploadState.NONE -> {
StateLabel("")
}
BackupUploadState.UPLOAD_IN_PROGRESS -> {
StateLabel("Upload in progress...")
}
BackupUploadState.UPLOAD_DONE -> {
StateLabel("Upload complete.")
}
BackupUploadState.UPLOAD_FAILED -> {
StateLabel("Upload failed.")
}
}
}
}
}
@@ -240,3 +294,14 @@ fun PreviewScreenImportInProgress() {
}
}
}
@Preview(name = "Light Theme", group = "screen", uiMode = Configuration.UI_MODE_NIGHT_NO)
@Preview(name = "Dark Theme", group = "screen", uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
fun PreviewScreenUploadInProgress() {
SignalTheme {
Surface {
Screen(state = ScreenState(uploadState = BackupUploadState.UPLOAD_IN_PROGRESS, plaintext = false))
}
}
}

View File

@@ -17,6 +17,8 @@ import io.reactivex.rxjava3.schedulers.Schedulers
import org.signal.libsignal.zkgroup.profiles.ProfileKey
import org.thoughtcrime.securesms.backup.v2.BackupRepository
import org.thoughtcrime.securesms.recipients.Recipient
import org.whispersystems.signalservice.api.NetworkResult
import org.whispersystems.signalservice.api.archive.ArchiveGetBackupInfoResponse
import java.io.ByteArrayInputStream
import java.io.InputStream
@@ -26,7 +28,7 @@ class InternalBackupPlaygroundViewModel : ViewModel() {
val disposables = CompositeDisposable()
private val _state: MutableState<ScreenState> = mutableStateOf(ScreenState(backupState = BackupState.NONE, plaintext = false))
private val _state: MutableState<ScreenState> = mutableStateOf(ScreenState(backupState = BackupState.NONE, uploadState = BackupUploadState.NONE, plaintext = false))
val state: State<ScreenState> = _state
fun export() {
@@ -80,11 +82,36 @@ class InternalBackupPlaygroundViewModel : ViewModel() {
_state.value = _state.value.copy(plaintext = !_state.value.plaintext)
}
fun testNetworkInteractions() {
fun uploadBackupToRemote() {
_state.value = _state.value.copy(uploadState = BackupUploadState.UPLOAD_IN_PROGRESS)
disposables += Single
.fromCallable { BackupRepository.testNetworkInteractions() }
.fromCallable { BackupRepository.uploadBackupFile(backupData!!.inputStream(), backupData!!.size.toLong()) }
.subscribeOn(Schedulers.io())
.subscribe()
.subscribe { success ->
_state.value = _state.value.copy(uploadState = if (success) BackupUploadState.UPLOAD_DONE else BackupUploadState.UPLOAD_FAILED)
}
}
fun checkRemoteBackupState() {
_state.value = _state.value.copy(remoteBackupState = RemoteBackupState.Unknown)
disposables += Single
.fromCallable { BackupRepository.getRemoteBackupState() }
.subscribeOn(Schedulers.io())
.subscribe { result ->
when {
result is NetworkResult.Success -> {
_state.value = _state.value.copy(remoteBackupState = RemoteBackupState.Available(result.result))
}
result is NetworkResult.StatusCodeError && result.code == 404 -> {
_state.value = _state.value.copy(remoteBackupState = RemoteBackupState.NotFound)
}
else -> {
_state.value = _state.value.copy(remoteBackupState = RemoteBackupState.GeneralError)
}
}
}
}
override fun onCleared() {
@@ -92,11 +119,24 @@ class InternalBackupPlaygroundViewModel : ViewModel() {
}
data class ScreenState(
val backupState: BackupState,
val backupState: BackupState = BackupState.NONE,
val uploadState: BackupUploadState = BackupUploadState.NONE,
val remoteBackupState: RemoteBackupState = RemoteBackupState.Unknown,
val plaintext: Boolean
)
enum class BackupState(val inProgress: Boolean = false) {
NONE, EXPORT_IN_PROGRESS(true), EXPORT_DONE, IMPORT_IN_PROGRESS(true)
}
enum class BackupUploadState(val inProgress: Boolean = false) {
NONE, UPLOAD_IN_PROGRESS(true), UPLOAD_DONE, UPLOAD_FAILED
}
sealed class RemoteBackupState {
object Unknown : RemoteBackupState()
object NotFound : RemoteBackupState()
object GeneralError : RemoteBackupState()
data class Available(val response: ArchiveGetBackupInfoResponse) : RemoteBackupState()
}
}

View File

@@ -16,6 +16,7 @@ import org.whispersystems.signalservice.api.NetworkResult
import org.whispersystems.signalservice.api.backup.BackupKey
import org.whispersystems.signalservice.api.push.ServiceId.ACI
import org.whispersystems.signalservice.internal.push.PushServiceSocket
import java.io.InputStream
/**
* Class to interact with various archive-related endpoints.
@@ -101,6 +102,24 @@ class ArchiveApi(
}
}
/**
* Retrieves a resumable upload URL you can use to upload your main message backup file to cloud storage.
*/
fun getBackupResumableUploadUrl(archiveFormResponse: ArchiveMessageBackupUploadFormResponse): NetworkResult<String> {
return NetworkResult.fromFetch {
pushServiceSocket.getResumableUploadUrl(archiveFormResponse)
}
}
/**
* Uploads your main backup file to cloud storage.
*/
fun uploadBackupFile(archiveFormResponse: ArchiveMessageBackupUploadFormResponse, resumableUploadUrl: String, data: InputStream, dataLength: Long): NetworkResult<Unit> {
return NetworkResult.fromFetch {
pushServiceSocket.uploadBackupFile(archiveFormResponse, resumableUploadUrl, data, dataLength)
}
}
private fun getZkCredential(backupKey: BackupKey, serviceCredential: ArchiveServiceCredential): BackupAuthCredential {
val backupAuthResponse = BackupAuthCredentialResponse(serviceCredential.credential)
val backupRequestContext = BackupAuthCredentialRequestContext.create(backupKey.value, aci.rawUuid)

View File

@@ -1637,6 +1637,10 @@ public class PushServiceSocket {
}
}
public String getResumableUploadUrl(ArchiveMessageBackupUploadFormResponse uploadFormResponse) throws IOException {
return getResumableUploadUrl(uploadFormResponse.getCdn(), uploadFormResponse.getSignedUploadLocation(), uploadFormResponse.getHeaders());
}
private String getResumableUploadUrl(int cdn, String signedUrl, Map<String, String> headers) throws IOException {
ConnectionHolder connectionHolder = getRandom(cdnClientsMap.get(cdn), random);
OkHttpClient okHttpClient = connectionHolder.getClient()
@@ -1739,6 +1743,10 @@ public class PushServiceSocket {
}
}
public void uploadBackupFile(ArchiveMessageBackupUploadFormResponse uploadFormResponse, String resumableUploadUrl, InputStream data, long dataLength) throws IOException {
uploadToCdn3(resumableUploadUrl, data, "application/octet-stream", dataLength, false, new NoCipherOutputStreamFactory(), null, null, uploadFormResponse.getHeaders());
}
private AttachmentDigest uploadToCdn3(String resumableUrl,
InputStream data,
String contentType,