mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-05-08 09:18:39 +01: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.keyvalue.SignalStore
|
||||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||||
import org.whispersystems.signalservice.api.NetworkResult
|
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.archive.ArchiveServiceCredential
|
||||||
import org.whispersystems.signalservice.api.push.ServiceId.ACI
|
import org.whispersystems.signalservice.api.push.ServiceId.ACI
|
||||||
import org.whispersystems.signalservice.api.push.ServiceId.PNI
|
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 api = ApplicationDependencies.getSignalServiceAccountManager().archiveApi
|
||||||
val backupKey = SignalStore.svr().getOrCreateMasterKey().deriveBackupKey()
|
val backupKey = SignalStore.svr().getOrCreateMasterKey().deriveBackupKey()
|
||||||
|
|
||||||
// Just running some sample requests
|
return api
|
||||||
api
|
|
||||||
.triggerBackupIdReservation(backupKey)
|
.triggerBackupIdReservation(backupKey)
|
||||||
.then { getAuthCredential() }
|
.then { getAuthCredential() }
|
||||||
.then { credential ->
|
.then { credential ->
|
||||||
@@ -182,13 +182,41 @@ object BackupRepository {
|
|||||||
}
|
}
|
||||||
.then { credential ->
|
.then { credential ->
|
||||||
api.getBackupInfo(backupKey, 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 }
|
.map { credential }
|
||||||
}
|
}
|
||||||
.then { credential ->
|
.then { credential ->
|
||||||
api.getMessageBackupUploadForm(backupKey, credential)
|
api.getMessageBackupUploadForm(backupKey, credential)
|
||||||
.also { Log.i(TAG, "UploadFormResult: $it") }
|
.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
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
+74
-9
@@ -33,9 +33,13 @@ import androidx.compose.ui.tooling.preview.Preview
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.fragment.app.viewModels
|
import androidx.fragment.app.viewModels
|
||||||
import org.signal.core.ui.Buttons
|
import org.signal.core.ui.Buttons
|
||||||
|
import org.signal.core.ui.Dividers
|
||||||
import org.signal.core.ui.theme.SignalTheme
|
import org.signal.core.ui.theme.SignalTheme
|
||||||
|
import org.signal.core.util.bytes
|
||||||
import org.signal.core.util.getLength
|
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.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.components.settings.app.internal.backup.InternalBackupPlaygroundViewModel.ScreenState
|
||||||
import org.thoughtcrime.securesms.compose.ComposeFragment
|
import org.thoughtcrime.securesms.compose.ComposeFragment
|
||||||
|
|
||||||
@@ -98,7 +102,8 @@ class InternalBackupPlaygroundFragment : ComposeFragment() {
|
|||||||
|
|
||||||
exportFileLauncher.launch(intent)
|
exportFileLauncher.launch(intent)
|
||||||
},
|
},
|
||||||
onTestNetworkClicked = { viewModel.testNetworkInteractions() }
|
onUploadToRemoteClicked = { viewModel.uploadBackupToRemote() },
|
||||||
|
onCheckRemoteBackupStateClicked = { viewModel.checkRemoteBackupState() }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -115,7 +120,8 @@ fun Screen(
|
|||||||
onImportFileClicked: () -> Unit = {},
|
onImportFileClicked: () -> Unit = {},
|
||||||
onPlaintextClicked: () -> Unit = {},
|
onPlaintextClicked: () -> Unit = {},
|
||||||
onSaveToDiskClicked: () -> Unit = {},
|
onSaveToDiskClicked: () -> Unit = {},
|
||||||
onTestNetworkClicked: () -> Unit = {}
|
onUploadToRemoteClicked: () -> Unit = {},
|
||||||
|
onCheckRemoteBackupStateClicked: () -> Unit = {}
|
||||||
) {
|
) {
|
||||||
Surface {
|
Surface {
|
||||||
Column(
|
Column(
|
||||||
@@ -144,12 +150,9 @@ fun Screen(
|
|||||||
) {
|
) {
|
||||||
Text("Export")
|
Text("Export")
|
||||||
}
|
}
|
||||||
Buttons.LargePrimary(
|
|
||||||
onClick = onTestNetworkClicked,
|
Dividers.Default()
|
||||||
enabled = state.backupState == BackupState.EXPORT_DONE
|
|
||||||
) {
|
|
||||||
Text("Test network")
|
|
||||||
}
|
|
||||||
Buttons.LargeTonal(
|
Buttons.LargeTonal(
|
||||||
onClick = onImportMemoryClicked,
|
onClick = onImportMemoryClicked,
|
||||||
enabled = state.backupState == BackupState.EXPORT_DONE
|
enabled = state.backupState == BackupState.EXPORT_DONE
|
||||||
@@ -172,7 +175,7 @@ fun Screen(
|
|||||||
StateLabel("Export in progress...")
|
StateLabel("Export in progress...")
|
||||||
}
|
}
|
||||||
BackupState.EXPORT_DONE -> {
|
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))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
|
||||||
@@ -184,6 +187,57 @@ fun Screen(
|
|||||||
StateLabel("Import in progress...")
|
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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+45
-5
@@ -17,6 +17,8 @@ import io.reactivex.rxjava3.schedulers.Schedulers
|
|||||||
import org.signal.libsignal.zkgroup.profiles.ProfileKey
|
import org.signal.libsignal.zkgroup.profiles.ProfileKey
|
||||||
import org.thoughtcrime.securesms.backup.v2.BackupRepository
|
import org.thoughtcrime.securesms.backup.v2.BackupRepository
|
||||||
import org.thoughtcrime.securesms.recipients.Recipient
|
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.ByteArrayInputStream
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
|
|
||||||
@@ -26,7 +28,7 @@ class InternalBackupPlaygroundViewModel : ViewModel() {
|
|||||||
|
|
||||||
val disposables = CompositeDisposable()
|
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
|
val state: State<ScreenState> = _state
|
||||||
|
|
||||||
fun export() {
|
fun export() {
|
||||||
@@ -80,11 +82,36 @@ class InternalBackupPlaygroundViewModel : ViewModel() {
|
|||||||
_state.value = _state.value.copy(plaintext = !_state.value.plaintext)
|
_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
|
disposables += Single
|
||||||
.fromCallable { BackupRepository.testNetworkInteractions() }
|
.fromCallable { BackupRepository.uploadBackupFile(backupData!!.inputStream(), backupData!!.size.toLong()) }
|
||||||
.subscribeOn(Schedulers.io())
|
.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() {
|
override fun onCleared() {
|
||||||
@@ -92,11 +119,24 @@ class InternalBackupPlaygroundViewModel : ViewModel() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
data class ScreenState(
|
data class ScreenState(
|
||||||
val backupState: BackupState,
|
val backupState: BackupState = BackupState.NONE,
|
||||||
|
val uploadState: BackupUploadState = BackupUploadState.NONE,
|
||||||
|
val remoteBackupState: RemoteBackupState = RemoteBackupState.Unknown,
|
||||||
val plaintext: Boolean
|
val plaintext: Boolean
|
||||||
)
|
)
|
||||||
|
|
||||||
enum class BackupState(val inProgress: Boolean = false) {
|
enum class BackupState(val inProgress: Boolean = false) {
|
||||||
NONE, EXPORT_IN_PROGRESS(true), EXPORT_DONE, IMPORT_IN_PROGRESS(true)
|
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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+19
@@ -16,6 +16,7 @@ import org.whispersystems.signalservice.api.NetworkResult
|
|||||||
import org.whispersystems.signalservice.api.backup.BackupKey
|
import org.whispersystems.signalservice.api.backup.BackupKey
|
||||||
import org.whispersystems.signalservice.api.push.ServiceId.ACI
|
import org.whispersystems.signalservice.api.push.ServiceId.ACI
|
||||||
import org.whispersystems.signalservice.internal.push.PushServiceSocket
|
import org.whispersystems.signalservice.internal.push.PushServiceSocket
|
||||||
|
import java.io.InputStream
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class to interact with various archive-related endpoints.
|
* 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 {
|
private fun getZkCredential(backupKey: BackupKey, serviceCredential: ArchiveServiceCredential): BackupAuthCredential {
|
||||||
val backupAuthResponse = BackupAuthCredentialResponse(serviceCredential.credential)
|
val backupAuthResponse = BackupAuthCredentialResponse(serviceCredential.credential)
|
||||||
val backupRequestContext = BackupAuthCredentialRequestContext.create(backupKey.value, aci.rawUuid)
|
val backupRequestContext = BackupAuthCredentialRequestContext.create(backupKey.value, aci.rawUuid)
|
||||||
|
|||||||
+8
@@ -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 {
|
private String getResumableUploadUrl(int cdn, String signedUrl, Map<String, String> headers) throws IOException {
|
||||||
ConnectionHolder connectionHolder = getRandom(cdnClientsMap.get(cdn), random);
|
ConnectionHolder connectionHolder = getRandom(cdnClientsMap.get(cdn), random);
|
||||||
OkHttpClient okHttpClient = connectionHolder.getClient()
|
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,
|
private AttachmentDigest uploadToCdn3(String resumableUrl,
|
||||||
InputStream data,
|
InputStream data,
|
||||||
String contentType,
|
String contentType,
|
||||||
|
|||||||
Reference in New Issue
Block a user