mirror of
https://github.com/signalapp/Signal-Android.git
synced 2025-12-24 04:58:45 +00:00
Add upload support for the main backup file in backupV2.
This commit is contained in:
committed by
Clark Chen
parent
f93a9a0f22
commit
f4bcfca323
@@ -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
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user