Prepare restore backup loading for v2

This commit is contained in:
Torsten Grote 2024-09-09 12:23:40 -03:00
parent 8ce79f4195
commit 83708d9403
No known key found for this signature in database
GPG key ID: 3E5F77D92CF891FF
20 changed files with 274 additions and 168 deletions

View file

@ -10,7 +10,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest
import androidx.test.platform.app.InstrumentationRegistry
import com.stevesoltys.seedvault.backend.LegacyStoragePlugin
import com.stevesoltys.seedvault.backend.getAvailableBackups
import com.stevesoltys.seedvault.backend.getAvailableBackupFileHandles
import com.stevesoltys.seedvault.backend.saf.DocumentsProviderLegacyPlugin
import com.stevesoltys.seedvault.backend.saf.DocumentsStorage
import com.stevesoltys.seedvault.settings.SettingsManager
@ -92,7 +92,7 @@ class PluginTest : KoinComponent {
@Test
fun testInitializationAndRestoreSets() = runBlocking(Dispatchers.IO) {
// no backups available initially
assertEquals(0, backend.getAvailableBackups()?.toList()?.size)
assertEquals(0, backend.getAvailableBackupFileHandles().toList().size)
// prepare returned tokens requested when initializing device
every { mockedSettingsManager.getToken() } returnsMany listOf(token, token + 1, token + 1)
@ -102,17 +102,17 @@ class PluginTest : KoinComponent {
.writeAndClose(getRandomByteArray())
// one backup available now
assertEquals(1, backend.getAvailableBackups()?.toList()?.size)
assertEquals(1, backend.getAvailableBackupFileHandles().toList().size)
// initializing again (with another restore set) does add a restore set
backend.save(LegacyAppBackupFile.Metadata(token + 1))
.writeAndClose(getRandomByteArray())
assertEquals(2, backend.getAvailableBackups()?.toList()?.size)
assertEquals(2, backend.getAvailableBackupFileHandles().toList().size)
// initializing again (without new restore set) doesn't change number of restore sets
backend.save(LegacyAppBackupFile.Metadata(token + 1))
.writeAndClose(getRandomByteArray())
assertEquals(2, backend.getAvailableBackups()?.toList()?.size)
assertEquals(2, backend.getAvailableBackupFileHandles().toList().size)
}
@Test
@ -124,27 +124,26 @@ class PluginTest : KoinComponent {
backend.save(LegacyAppBackupFile.Metadata(token)).writeAndClose(metadata)
// get available backups, expect only one with our token and no error
var availableBackups = backend.getAvailableBackups()?.toList()
check(availableBackups != null)
var availableBackups = backend.getAvailableBackupFileHandles().toList()
assertEquals(1, availableBackups.size)
assertEquals(token, availableBackups[0].token)
var backupHandle = availableBackups[0] as LegacyAppBackupFile.Metadata
assertEquals(token, backupHandle.token)
// read metadata matches what was written earlier
assertReadEquals(metadata, availableBackups[0].inputStreamRetriever())
assertReadEquals(metadata, backend.load(backupHandle))
// initializing again (without changing storage) keeps restore set with same token
backend.save(LegacyAppBackupFile.Metadata(token)).writeAndClose(metadata)
availableBackups = backend.getAvailableBackups()?.toList()
check(availableBackups != null)
availableBackups = backend.getAvailableBackupFileHandles().toList()
assertEquals(1, availableBackups.size)
assertEquals(token, availableBackups[0].token)
backupHandle = availableBackups[0] as LegacyAppBackupFile.Metadata
assertEquals(token, backupHandle.token)
// metadata hasn't changed
assertReadEquals(metadata, availableBackups[0].inputStreamRetriever())
assertReadEquals(metadata, backend.load(backupHandle))
}
@Test
@Suppress("Deprecation")
fun v0testApkWriteRead() = runBlocking {
// initialize storage with given token
initStorage(token)

View file

@ -5,37 +5,29 @@
package com.stevesoltys.seedvault.backend
import android.util.Log
import at.bitfire.dav4jvm.exception.HttpException
import org.calyxos.seedvault.core.backends.AppBackupFileType
import org.calyxos.seedvault.core.backends.Backend
import org.calyxos.seedvault.core.backends.FileHandle
import org.calyxos.seedvault.core.backends.LegacyAppBackupFile
import java.io.IOException
import java.io.InputStream
import java.io.OutputStream
suspend fun Backend.getMetadataOutputStream(token: Long): OutputStream {
return save(LegacyAppBackupFile.Metadata(token))
}
suspend fun Backend.getAvailableBackups(): Sequence<EncryptedMetadata>? {
return try {
// get all restore set tokens in root folder that have a metadata file
val handles = ArrayList<LegacyAppBackupFile.Metadata>()
list(null, LegacyAppBackupFile.Metadata::class) { fileInfo ->
val handle = fileInfo.fileHandle as LegacyAppBackupFile.Metadata
handles.add(handle)
suspend fun Backend.getAvailableBackupFileHandles(): List<FileHandle> {
// v1 get all restore set tokens in root folder that have a metadata file
// v2 get all snapshots in all repository folders
return ArrayList<FileHandle>().apply {
list(
null,
AppBackupFileType.Snapshot::class,
LegacyAppBackupFile.Metadata::class,
) { fileInfo ->
add(fileInfo.fileHandle)
}
val handleIterator = handles.iterator()
return generateSequence {
if (!handleIterator.hasNext()) return@generateSequence null // end sequence
val handle = handleIterator.next()
EncryptedMetadata(handle.token) {
load(handle)
}
}
} catch (e: Exception) {
Log.e("SafBackend", "Error getting available backups: ", e)
null
}
}
@ -49,5 +41,3 @@ fun Exception.isOutOfSpace(): Boolean {
else -> false
}
}
class EncryptedMetadata(val token: Long, val inputStreamRetriever: suspend () -> InputStream)

View file

@ -15,7 +15,7 @@ import android.util.Log
import androidx.annotation.WorkerThread
import com.stevesoltys.seedvault.R
import com.stevesoltys.seedvault.backend.BackendManager
import com.stevesoltys.seedvault.backend.getAvailableBackups
import com.stevesoltys.seedvault.backend.getAvailableBackupFileHandles
import com.stevesoltys.seedvault.isMassStorage
import com.stevesoltys.seedvault.settings.FlashDrive
import com.stevesoltys.seedvault.settings.SettingsManager
@ -59,9 +59,8 @@ internal class SafHandler(
@WorkerThread
@Throws(IOException::class)
suspend fun hasAppBackup(safProperties: SafProperties): Boolean {
val appPlugin = backendFactory.createSafBackend(safProperties)
val backups = appPlugin.getAvailableBackups()
return backups != null && backups.iterator().hasNext()
val backend = backendFactory.createSafBackend(safProperties)
return backend.getAvailableBackupFileHandles().isNotEmpty()
}
fun save(safProperties: SafProperties) {

View file

@ -10,7 +10,7 @@ import android.util.Log
import androidx.annotation.WorkerThread
import com.stevesoltys.seedvault.R
import com.stevesoltys.seedvault.backend.BackendManager
import com.stevesoltys.seedvault.backend.getAvailableBackups
import com.stevesoltys.seedvault.backend.getAvailableBackupFileHandles
import com.stevesoltys.seedvault.settings.SettingsManager
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
@ -81,8 +81,7 @@ internal class WebDavHandler(
@WorkerThread
@Throws(IOException::class)
suspend fun hasAppBackup(backend: Backend): Boolean {
val backups = backend.getAvailableBackups()
return backups != null && backups.iterator().hasNext()
return backend.getAvailableBackupFileHandles().isNotEmpty()
}
fun save(properties: WebDavProperties) {

View file

@ -30,6 +30,23 @@ data class BackupMetadata(
internal var d2dBackup: Boolean = false,
internal val packageMetadataMap: PackageMetadataMap = PackageMetadataMap(),
) {
companion object {
fun fromSnapshot(s: Snapshot) = BackupMetadata(
version = s.version.toByte(),
token = s.token,
salt = "",
time = s.token,
androidVersion = s.sdkInt,
androidIncremental = s.androidIncremental,
deviceName = s.name,
d2dBackup = s.d2D,
packageMetadataMap = s.appsMap.mapValues { (_, app) ->
PackageMetadata.fromSnapshot(app)
} as PackageMetadataMap
)
}
val size: Long
get() = packageMetadataMap.values.sumOf { m ->
(m.size ?: 0L) + (m.splits?.sumOf { it.size ?: 0L } ?: 0L)

View file

@ -23,12 +23,13 @@ import com.stevesoltys.seedvault.BackupMonitor
import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER
import com.stevesoltys.seedvault.NO_DATA_END_SENTINEL
import com.stevesoltys.seedvault.R
import com.stevesoltys.seedvault.backend.BackendManager
import com.stevesoltys.seedvault.metadata.PackageMetadataMap
import com.stevesoltys.seedvault.metadata.PackageState
import com.stevesoltys.seedvault.backend.BackendManager
import com.stevesoltys.seedvault.restore.install.isInstalled
import com.stevesoltys.seedvault.settings.SettingsManager
import com.stevesoltys.seedvault.transport.TRANSPORT_ID
import com.stevesoltys.seedvault.transport.restore.RestorableBackup
import com.stevesoltys.seedvault.transport.restore.RestoreCoordinator
import com.stevesoltys.seedvault.ui.AppBackupState
import com.stevesoltys.seedvault.ui.AppBackupState.FAILED
@ -263,20 +264,19 @@ internal class AppDataRestoreManager(
/**
* Restore the next chunk of packages.
*
* We need to restore in chunks, otherwise [BackupTransport.startRestore] in the
* framework's [PerformUnifiedRestoreTask] may fail due to an oversize Binder
* transaction, causing the entire restoration to fail.
* We need to restore packages in chunks, otherwise [BackupTransport.startRestore] in the
* framework's [PerformUnifiedRestoreTask] may fail due to an oversize Binder transaction,
* causing the entire restoration to fail due to too many package names.
*/
private fun restoreNextPackages() {
// Make sure metadata for selected backup is cached before starting each chunk.
val backupMetadata = restorableBackup.backupMetadata
restoreCoordinator.beforeStartRestore(backupMetadata)
restoreCoordinator.beforeStartRestore(restorableBackup)
val nextChunkIndex = (packageIndex + PACKAGES_PER_CHUNK).coerceAtMost(packages.size)
val packageChunk = packages.subList(packageIndex, nextChunkIndex).toTypedArray()
packageIndex += packageChunk.size
val token = backupMetadata.token
val token = restorableBackup.token
val result = session.restorePackages(token, this, packageChunk, monitor)
@Suppress("UNRESOLVED_REFERENCE") // BackupManager.SUCCESS

View file

@ -15,6 +15,7 @@ import com.stevesoltys.seedvault.R
import com.stevesoltys.seedvault.backend.BackendManager
import com.stevesoltys.seedvault.metadata.PackageMetadata
import com.stevesoltys.seedvault.metadata.PackageMetadataMap
import com.stevesoltys.seedvault.transport.restore.RestorableBackup
import com.stevesoltys.seedvault.ui.PACKAGE_NAME_SYSTEM
import com.stevesoltys.seedvault.ui.systemData
import com.stevesoltys.seedvault.worker.IconManager
@ -68,21 +69,23 @@ internal class AppSelectionManager(
val name = context.getString(data.nameRes)
SelectableAppItem(packageName, metadata.copy(name = name), true)
}
val systemItem = SelectableAppItem(
packageName = PACKAGE_NAME_SYSTEM,
metadata = PackageMetadata(
time = restorableBackup.packageMetadataMap.values.maxOf {
if (it.system) it.time else -1
},
size = restorableBackup.packageMetadataMap.values.sumOf {
if (it.system) it.size ?: 0L else 0L
},
system = true,
name = context.getString(R.string.backup_system_apps),
),
selected = isSetupWizard,
)
items.add(0, systemItem)
if (restorableBackup.packageMetadataMap.isNotEmpty()) {
val systemItem = SelectableAppItem(
packageName = PACKAGE_NAME_SYSTEM,
metadata = PackageMetadata(
time = restorableBackup.packageMetadataMap.values.maxOf {
if (it.system) it.time else -1
},
size = restorableBackup.packageMetadataMap.values.sumOf {
if (it.system) it.size ?: 0L else 0L
},
system = true,
name = context.getString(R.string.backup_system_apps),
),
selected = isSetupWizard,
)
items.add(0, systemItem)
}
items.addAll(0, systemDataItems)
selectedApps.value =
SelectedAppsState(apps = items, allSelected = isSetupWizard, iconsLoaded = false)

View file

@ -19,6 +19,7 @@ import androidx.recyclerview.widget.RecyclerView.Adapter
import androidx.recyclerview.widget.RecyclerView.ViewHolder
import com.stevesoltys.seedvault.R
import com.stevesoltys.seedvault.restore.RestoreSetAdapter.RestoreSetViewHolder
import com.stevesoltys.seedvault.transport.restore.RestorableBackup
internal class RestoreSetAdapter(
private val listener: RestorableBackupClickListener,

View file

@ -17,6 +17,7 @@ import android.widget.TextView
import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.RecyclerView
import com.stevesoltys.seedvault.R
import com.stevesoltys.seedvault.transport.restore.RestorableBackup
import org.koin.androidx.viewmodel.ext.android.sharedViewModel
class RestoreSetFragment : Fragment() {

View file

@ -9,7 +9,6 @@ import android.app.Application
import android.app.backup.IBackupManager
import android.content.Intent
import android.graphics.drawable.Drawable
import android.util.Log
import androidx.annotation.UiThread
import androidx.appcompat.content.res.AppCompatResources.getDrawable
import androidx.lifecycle.LiveData
@ -17,8 +16,8 @@ import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.asLiveData
import androidx.lifecycle.viewModelScope
import com.stevesoltys.seedvault.R
import com.stevesoltys.seedvault.crypto.KeyManager
import com.stevesoltys.seedvault.backend.BackendManager
import com.stevesoltys.seedvault.crypto.KeyManager
import com.stevesoltys.seedvault.restore.DisplayFragment.RESTORE_APPS
import com.stevesoltys.seedvault.restore.DisplayFragment.RESTORE_BACKUP
import com.stevesoltys.seedvault.restore.DisplayFragment.RESTORE_FILES
@ -30,6 +29,9 @@ import com.stevesoltys.seedvault.restore.install.InstallIntentCreator
import com.stevesoltys.seedvault.restore.install.InstallResult
import com.stevesoltys.seedvault.settings.SettingsManager
import com.stevesoltys.seedvault.storage.StorageRestoreService
import com.stevesoltys.seedvault.transport.restore.RestorableBackup
import com.stevesoltys.seedvault.transport.restore.RestorableBackupResult.ErrorResult
import com.stevesoltys.seedvault.transport.restore.RestorableBackupResult.SuccessResult
import com.stevesoltys.seedvault.transport.restore.RestoreCoordinator
import com.stevesoltys.seedvault.ui.LiveEvent
import com.stevesoltys.seedvault.ui.MutableLiveEvent
@ -106,20 +108,11 @@ internal class RestoreViewModel(
private var storedSnapshot: StoredSnapshot? = null
internal fun loadRestoreSets() = viewModelScope.launch(ioDispatcher) {
val backups = restoreCoordinator.getAvailableMetadata()?.mapNotNull { (token, metadata) ->
when (metadata.time) {
0L -> {
Log.d(TAG, "Ignoring RestoreSet with no last backup time: $token.")
null
}
else -> RestorableBackup(metadata)
}
}
val result = when {
backups == null -> RestoreSetResult(app.getString(R.string.restore_set_error))
backups.isEmpty() -> RestoreSetResult(app.getString(R.string.restore_set_empty_result))
else -> RestoreSetResult(backups)
val result = when (val backups = restoreCoordinator.getAvailableBackups()) {
is ErrorResult -> RestoreSetResult(
app.getString(R.string.restore_set_error) + "\n\n${backups.e}"
)
is SuccessResult -> RestoreSetResult(backups.backups)
}
mRestoreSetResults.postValue(result)
}

View file

@ -21,7 +21,6 @@ import com.stevesoltys.seedvault.crypto.Crypto
import com.stevesoltys.seedvault.encodeBase64
import com.stevesoltys.seedvault.metadata.ApkSplit
import com.stevesoltys.seedvault.metadata.PackageMetadata
import com.stevesoltys.seedvault.restore.RestorableBackup
import com.stevesoltys.seedvault.restore.RestoreService
import com.stevesoltys.seedvault.restore.install.ApkInstallState.FAILED
import com.stevesoltys.seedvault.restore.install.ApkInstallState.FAILED_SYSTEM_APP

View file

@ -1,20 +1,31 @@
/*
* SPDX-FileCopyrightText: 2020 The Calyx Institute
* SPDX-FileCopyrightText: 2024 The Calyx Institute
* SPDX-License-Identifier: Apache-2.0
*/
package com.stevesoltys.seedvault.restore
package com.stevesoltys.seedvault.transport.restore
import com.stevesoltys.seedvault.metadata.BackupMetadata
import com.stevesoltys.seedvault.metadata.PackageMetadataMap
import com.stevesoltys.seedvault.proto.Snapshot
sealed class RestorableBackupResult {
data class ErrorResult(val e: Exception?) : RestorableBackupResult()
data class SuccessResult(val backups: List<RestorableBackup>) : RestorableBackupResult()
}
data class RestorableBackup(
val backupMetadata: BackupMetadata,
val repoId: String? = null,
val snapshot: Snapshot? = null,
) {
constructor(repoId: String, snapshot: Snapshot) : this(
backupMetadata = BackupMetadata.fromSnapshot(snapshot),
repoId = repoId,
snapshot = snapshot,
)
val name: String
get() = backupMetadata.deviceName
@ -30,8 +41,9 @@ data class RestorableBackup(
val time: Long
get() = backupMetadata.time
val size: Long?
get() = backupMetadata.size
val size: Long
get() = snapshot?.blobsMap?.values?.sumOf { it.uncompressedLength.toLong() }
?: backupMetadata.size
val deviceName: String
get() = backupMetadata.deviceName

View file

@ -18,20 +18,22 @@ import android.os.ParcelFileDescriptor
import android.util.Log
import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER
import com.stevesoltys.seedvault.R
import com.stevesoltys.seedvault.backend.BackendManager
import com.stevesoltys.seedvault.backend.getAvailableBackupFileHandles
import com.stevesoltys.seedvault.crypto.Crypto
import com.stevesoltys.seedvault.header.UnsupportedVersionException
import com.stevesoltys.seedvault.metadata.BackupMetadata
import com.stevesoltys.seedvault.metadata.BackupType
import com.stevesoltys.seedvault.metadata.DecryptionFailedException
import com.stevesoltys.seedvault.metadata.MetadataManager
import com.stevesoltys.seedvault.metadata.MetadataReader
import com.stevesoltys.seedvault.backend.BackendManager
import com.stevesoltys.seedvault.backend.getAvailableBackups
import com.stevesoltys.seedvault.proto.Snapshot
import com.stevesoltys.seedvault.settings.SettingsManager
import com.stevesoltys.seedvault.transport.D2D_TRANSPORT_FLAGS
import com.stevesoltys.seedvault.transport.DEFAULT_TRANSPORT_FLAGS
import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager
import org.calyxos.seedvault.core.backends.AppBackupFileType
import org.calyxos.seedvault.core.backends.Backend
import org.calyxos.seedvault.core.backends.LegacyAppBackupFile
import java.io.IOException
/**
@ -49,7 +51,7 @@ private data class RestoreCoordinatorState(
* Optional [PackageInfo] for single package restore, to reduce data needed to read for @pm@
*/
val autoRestorePackageInfo: PackageInfo?,
val backupMetadata: BackupMetadata,
val backup: RestorableBackup,
) {
var currentPackage: String? = null
}
@ -63,6 +65,7 @@ internal class RestoreCoordinator(
private val metadataManager: MetadataManager,
private val notificationManager: BackupNotificationManager,
private val backendManager: BackendManager,
private val loader: Loader,
private val kv: KVRestore,
private val full: FullRestore,
private val metadataReader: MetadataReader,
@ -70,34 +73,58 @@ internal class RestoreCoordinator(
private val backend: Backend get() = backendManager.backend
private var state: RestoreCoordinatorState? = null
private var backupMetadata: BackupMetadata? = null
private var restorableBackup: RestorableBackup? = null
private val failedPackages = ArrayList<String>()
suspend fun getAvailableMetadata(): Map<Long, BackupMetadata>? {
val availableBackups = backend.getAvailableBackups() ?: return null
val metadataMap = HashMap<Long, BackupMetadata>()
for (encryptedMetadata in availableBackups) {
suspend fun getAvailableBackups(): RestorableBackupResult {
val fileHandles = try {
backend.getAvailableBackupFileHandles()
} catch (e: Exception) {
Log.e(TAG, "Error getting available backups.", e)
return RestorableBackupResult.ErrorResult(e)
}
val backups = ArrayList<RestorableBackup>()
var lastException: Exception? = null
for (handle in fileHandles) {
try {
val metadata = encryptedMetadata.inputStreamRetriever().use { inputStream ->
metadataReader.readMetadata(inputStream, encryptedMetadata.token)
val backup = when (handle) {
is AppBackupFileType.Snapshot -> {
val snapshot = loader.loadFile(handle).use { inputStream ->
Snapshot.parseFrom(inputStream)
}
RestorableBackup(
repoId = handle.repoId,
snapshot = snapshot,
)
}
is LegacyAppBackupFile.Metadata -> {
val metadata = backend.load(handle).use { inputStream ->
metadataReader.readMetadata(inputStream, handle.token)
}
RestorableBackup(backupMetadata = metadata)
}
else -> error("Unexpected file handle: $handle")
}
metadataMap[encryptedMetadata.token] = metadata
backups.add(backup)
} catch (e: IOException) {
Log.e(TAG, "Error while getting restore set ${encryptedMetadata.token}", e)
Log.e(TAG, "Error while getting restore set $handle", e)
lastException = e
continue
} catch (e: SecurityException) {
Log.e(TAG, "Error while getting restore set ${encryptedMetadata.token}", e)
return null
Log.e(TAG, "Error while getting restore set $handle", e)
return RestorableBackupResult.ErrorResult(e)
} catch (e: DecryptionFailedException) {
Log.e(TAG, "Error while decrypting restore set ${encryptedMetadata.token}", e)
Log.e(TAG, "Error while decrypting restore set $handle", e)
lastException = e
continue
} catch (e: UnsupportedVersionException) {
Log.w(TAG, "Backup with unsupported version read", e)
lastException = e
continue
}
}
Log.i(TAG, "Got available metadata for tokens: ${metadataMap.keys}")
return metadataMap
if (backups.isEmpty()) return RestorableBackupResult.ErrorResult(lastException)
return RestorableBackupResult.SuccessResult(backups)
}
/**
@ -107,22 +134,21 @@ internal class RestoreCoordinator(
* or null if an error occurred (the attempt should be rescheduled).
**/
suspend fun getAvailableRestoreSets(): Array<RestoreSet>? {
return getAvailableMetadata()?.map { (_, metadata) ->
val transportFlags = if (metadata.d2dBackup) {
val result = getAvailableBackups() as? RestorableBackupResult.SuccessResult ?: return null
val backups = result.backups
return backups.map { backup ->
val transportFlags = if (backup.d2dBackup) {
D2D_TRANSPORT_FLAGS
} else {
DEFAULT_TRANSPORT_FLAGS
}
val deviceName = if (metadata.d2dBackup) {
val deviceName = if (backup.d2dBackup) {
D2D_DEVICE_NAME
} else {
metadata.deviceName
backup.deviceName
}
RestoreSet(metadata.deviceName, deviceName, metadata.token, transportFlags)
}?.toTypedArray()
RestoreSet(backup.deviceName, deviceName, backup.token, transportFlags)
}.toTypedArray()
}
/**
@ -141,10 +167,10 @@ internal class RestoreCoordinator(
/**
* Call this before starting the restore as an optimization to prevent re-fetching metadata.
*/
fun beforeStartRestore(backupMetadata: BackupMetadata) {
this.backupMetadata = backupMetadata
fun beforeStartRestore(restorableBackup: RestorableBackup) {
this.restorableBackup = restorableBackup
if (backupMetadata.d2dBackup) {
if (restorableBackup.d2dBackup) {
settingsManager.setD2dBackupsEnabled(true)
}
}
@ -188,13 +214,15 @@ internal class RestoreCoordinator(
packages[1]
} else null
val metadata = if (backupMetadata?.token == token) {
backupMetadata!! // if token matches, backupMetadata is non-null
val backup = if (restorableBackup?.token == token) {
restorableBackup!! // if token matches, backupMetadata is non-null
} else {
getAvailableMetadata()?.get(token) ?: return TRANSPORT_ERROR
val backup = getAvailableBackups() as? RestorableBackupResult.SuccessResult
?: return TRANSPORT_ERROR
backup.backups.find { it.token == token } ?: return TRANSPORT_ERROR
}
state = RestoreCoordinatorState(token, packages.iterator(), pmPackageInfo, metadata)
backupMetadata = null
state = RestoreCoordinatorState(token, packages.iterator(), pmPackageInfo, backup)
restorableBackup = null
failedPackages.clear()
return TRANSPORT_OK
}
@ -231,13 +259,13 @@ internal class RestoreCoordinator(
if (!state.packages.hasNext()) return NO_MORE_PACKAGES
val packageInfo = state.packages.next()
val version = state.backupMetadata.version
val version = state.backup.version
if (version == 0.toByte()) return nextRestorePackageV0(state, packageInfo)
val packageName = packageInfo.packageName
val type = when (state.backupMetadata.packageMetadataMap[packageName]?.backupType) {
val type = when (state.backup.packageMetadataMap[packageName]?.backupType) {
BackupType.KV -> {
val name = crypto.getNameForPackage(state.backupMetadata.salt, packageName)
val name = crypto.getNameForPackage(state.backup.salt, packageName)
kv.initializeState(
version = version,
token = state.token,
@ -250,7 +278,7 @@ internal class RestoreCoordinator(
}
BackupType.FULL -> {
val name = crypto.getNameForPackage(state.backupMetadata.salt, packageName)
val name = crypto.getNameForPackage(state.backup.salt, packageName)
full.initializeState(version, state.token, name, packageInfo)
state.currentPackage = packageName
TYPE_FULL_STREAM
@ -258,7 +286,7 @@ internal class RestoreCoordinator(
null -> {
Log.i(TAG, "No backup type found for $packageName. Skipping...")
state.backupMetadata.packageMetadataMap[packageName]?.backupType?.let { s ->
state.backup.packageMetadataMap[packageName]?.backupType?.let { s ->
Log.w(TAG, "State was ${s.name}")
}
failedPackages.add(packageName)

View file

@ -14,6 +14,17 @@ val restoreModule = module {
single { KVRestore(get(), get(), get(), get(), get(), get()) }
single { FullRestore(get(), get(), get(), get(), get()) }
single {
RestoreCoordinator(androidContext(), get(), get(), get(), get(), get(), get(), get(), get())
RestoreCoordinator(
context = androidContext(),
crypto = get(),
settingsManager = get(),
metadataManager = get(),
notificationManager = get(),
backendManager = get(),
loader = get(),
kv = get(),
full = get(),
metadataReader = get(),
)
}
}

View file

@ -19,7 +19,6 @@ import org.calyxos.seedvault.core.backends.Backend
import org.calyxos.seedvault.core.backends.Constants.DIRECTORY_ROOT
import org.calyxos.seedvault.core.backends.saf.SafProperties
import org.calyxos.seedvault.core.backends.webdav.WebDavProperties
import java.io.IOException
private val TAG = RestoreStorageViewModel::class.java.simpleName
@ -37,9 +36,11 @@ internal class RestoreStorageViewModel(
viewModelScope.launch(Dispatchers.IO) {
val hasBackup = try {
safHandler.hasAppBackup(safProperties)
} catch (e: IOException) {
} catch (e: Exception) {
Log.e(TAG, "Error reading URI: ${safProperties.uri}", e)
false
val errorMsg = app.getString(R.string.restore_set_error) + "\n\n$e"
mLocationChecked.postEvent(LocationResult(errorMsg))
return@launch
}
if (hasBackup) {
safHandler.save(safProperties)
@ -60,9 +61,11 @@ internal class RestoreStorageViewModel(
viewModelScope.launch(Dispatchers.IO) {
val hasBackup = try {
webdavHandler.hasAppBackup(backend)
} catch (e: IOException) {
} catch (e: Exception) {
Log.e(TAG, "Error reading: ${properties.config.url}", e)
false
val errorMsg = app.getString(R.string.restore_set_error) + "\n\n$e"
mLocationChecked.postEvent(LocationResult(errorMsg))
return@launch
}
if (hasBackup) {
webdavHandler.save(properties)

View file

@ -15,6 +15,7 @@ import com.stevesoltys.seedvault.metadata.BackupMetadata
import com.stevesoltys.seedvault.metadata.PackageMetadata
import com.stevesoltys.seedvault.metadata.PackageMetadataMap
import com.stevesoltys.seedvault.transport.TransportTest
import com.stevesoltys.seedvault.transport.restore.RestorableBackup
import com.stevesoltys.seedvault.ui.PACKAGE_NAME_CONTACTS
import com.stevesoltys.seedvault.ui.PACKAGE_NAME_SETTINGS
import com.stevesoltys.seedvault.ui.PACKAGE_NAME_SYSTEM
@ -27,7 +28,6 @@ import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.runTest
import org.calyxos.seedvault.core.backends.Backend
import org.calyxos.seedvault.core.backends.LegacyAppBackupFile
import org.junit.Test
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertFalse
@ -239,7 +239,7 @@ internal class AppSelectionManagerTest : TransportTest() {
val backend: Backend = mockk()
every { backendManager.backend } returns backend
coEvery {
backend.load(LegacyAppBackupFile.IconsFile(backupMetadata.token))
iconManager.downloadIcons(repoId, snapshot)
} throws IOException()
appSelectionManager.selectedAppsFlow.test {

View file

@ -29,7 +29,7 @@ import com.stevesoltys.seedvault.metadata.PackageMetadataMap
import com.stevesoltys.seedvault.proto.SnapshotKt.blob
import com.stevesoltys.seedvault.proto.SnapshotKt.split
import com.stevesoltys.seedvault.proto.copy
import com.stevesoltys.seedvault.restore.RestorableBackup
import com.stevesoltys.seedvault.transport.restore.RestorableBackup
import com.stevesoltys.seedvault.restore.install.ApkInstallState.FAILED
import com.stevesoltys.seedvault.restore.install.ApkInstallState.FAILED_SYSTEM_APP
import com.stevesoltys.seedvault.restore.install.ApkInstallState.IN_PROGRESS

View file

@ -30,7 +30,9 @@ import com.stevesoltys.seedvault.transport.backup.PackageService
import com.stevesoltys.seedvault.transport.backup.TestKvDbManager
import com.stevesoltys.seedvault.transport.restore.FullRestore
import com.stevesoltys.seedvault.transport.restore.KVRestore
import com.stevesoltys.seedvault.transport.restore.Loader
import com.stevesoltys.seedvault.transport.restore.OutputFactory
import com.stevesoltys.seedvault.transport.restore.RestorableBackup
import com.stevesoltys.seedvault.transport.restore.RestoreCoordinator
import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager
import com.stevesoltys.seedvault.worker.ApkBackup
@ -68,6 +70,7 @@ internal class CoordinatorIntegrationTest : TransportTest() {
@Suppress("Deprecation")
private val legacyPlugin = mockk<LegacyStoragePlugin>()
private val backend = mockk<Backend>()
private val loader = mockk<Loader>()
private val kvBackup = KVBackup(
backendManager = backendManager,
settingsManager = settingsManager,
@ -114,11 +117,13 @@ internal class CoordinatorIntegrationTest : TransportTest() {
metadataManager,
notificationManager,
backendManager,
loader,
kvRestore,
fullRestore,
metadataReader
)
private val restorableBackup = RestorableBackup(metadata)
private val backupDataInput = mockk<BackupDataInput>()
private val fileDescriptor = mockk<ParcelFileDescriptor>(relaxed = true)
private val appData = ByteArray(42).apply { Random.nextBytes(this) }
@ -185,7 +190,7 @@ internal class CoordinatorIntegrationTest : TransportTest() {
assertEquals(TRANSPORT_OK, backup.finishBackup())
// start restore
restore.beforeStartRestore(metadata)
restore.beforeStartRestore(restorableBackup)
assertEquals(TRANSPORT_OK, restore.startRestore(token, arrayOf(packageInfo)))
// find data for K/V backup
@ -262,7 +267,7 @@ internal class CoordinatorIntegrationTest : TransportTest() {
assertEquals(TRANSPORT_OK, backup.finishBackup())
// start restore
restore.beforeStartRestore(metadata)
restore.beforeStartRestore(restorableBackup)
assertEquals(TRANSPORT_OK, restore.startRestore(token, arrayOf(packageInfo)))
// find data for K/V backup
@ -328,7 +333,7 @@ internal class CoordinatorIntegrationTest : TransportTest() {
assertEquals(TRANSPORT_OK, backup.finishBackup())
// start restore
restore.beforeStartRestore(metadata)
restore.beforeStartRestore(restorableBackup)
assertEquals(TRANSPORT_OK, restore.startRestore(token, arrayOf(packageInfo)))
// finds data for full backup

View file

@ -14,8 +14,6 @@ import android.app.backup.RestoreDescription.TYPE_KEY_VALUE
import android.content.pm.PackageInfo
import android.os.ParcelFileDescriptor
import com.stevesoltys.seedvault.backend.BackendManager
import com.stevesoltys.seedvault.backend.EncryptedMetadata
import com.stevesoltys.seedvault.backend.getAvailableBackups
import com.stevesoltys.seedvault.coAssertThrows
import com.stevesoltys.seedvault.getRandomString
import com.stevesoltys.seedvault.header.VERSION
@ -32,7 +30,10 @@ import io.mockk.mockk
import io.mockk.mockkStatic
import io.mockk.verify
import kotlinx.coroutines.runBlocking
import org.calyxos.seedvault.core.backends.AppBackupFileType
import org.calyxos.seedvault.core.backends.Backend
import org.calyxos.seedvault.core.backends.FileInfo
import org.calyxos.seedvault.core.backends.LegacyAppBackupFile
import org.calyxos.seedvault.core.backends.saf.SafProperties
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertNotNull
@ -48,6 +49,7 @@ internal class RestoreCoordinatorTest : TransportTest() {
private val notificationManager: BackupNotificationManager = mockk()
private val backendManager: BackendManager = mockk()
private val backend = mockk<Backend>()
private val loader = mockk<Loader>()
private val kv = mockk<KVRestore>()
private val full = mockk<FullRestore>()
private val metadataReader = mockk<MetadataReader>()
@ -59,11 +61,13 @@ internal class RestoreCoordinatorTest : TransportTest() {
metadataManager = metadataManager,
notificationManager = notificationManager,
backendManager = backendManager,
loader = loader,
kv = kv,
full = full,
metadataReader = metadataReader,
)
private val restorableBackup = RestorableBackup(metadata)
private val inputStream = mockk<InputStream>()
private val safStorage: SafProperties = mockk()
private val packageInfo2 = PackageInfo().apply { packageName = "org.example2" }
@ -85,12 +89,22 @@ internal class RestoreCoordinatorTest : TransportTest() {
@Test
fun `getAvailableRestoreSets() builds set from plugin response`() = runBlocking {
val encryptedMetadata = EncryptedMetadata(token) { inputStream }
val info1 = FileInfo(LegacyAppBackupFile.Metadata(token), 1)
val info2 = FileInfo(LegacyAppBackupFile.Metadata(token + 1), 2)
coEvery { backend.getAvailableBackups() } returns sequenceOf(
encryptedMetadata,
EncryptedMetadata(token + 1) { inputStream }
)
coEvery {
backend.list(
topLevelFolder = null,
AppBackupFileType.Snapshot::class, LegacyAppBackupFile.Metadata::class,
callback = captureLambda<(FileInfo) -> Unit>()
)
} answers {
val callback = lambda<(FileInfo) -> Unit>().captured
callback(info1)
callback(info2)
}
coEvery { backend.load(info1.fileHandle) } returns inputStream
coEvery { backend.load(info2.fileHandle) } returns inputStream
every { metadataReader.readMetadata(inputStream, token) } returns metadata
every { metadataReader.readMetadata(inputStream, token + 1) } returns metadata
every { inputStream.close() } just Runs
@ -119,16 +133,28 @@ internal class RestoreCoordinatorTest : TransportTest() {
@Test
fun `startRestore() returns OK`() = runBlocking {
restore.beforeStartRestore(metadata)
restore.beforeStartRestore(restorableBackup)
assertEquals(TRANSPORT_OK, restore.startRestore(token, packageInfoArray))
}
@Test
fun `startRestore() fetches metadata if missing`() = runBlocking {
coEvery { backend.getAvailableBackups() } returns sequenceOf(
EncryptedMetadata(token) { inputStream },
EncryptedMetadata(token + 1) { inputStream }
)
val info1 = FileInfo(LegacyAppBackupFile.Metadata(token), 1)
val info2 = FileInfo(LegacyAppBackupFile.Metadata(token + 1), 2)
coEvery {
backend.list(
topLevelFolder = null,
AppBackupFileType.Snapshot::class, LegacyAppBackupFile.Metadata::class,
callback = captureLambda<(FileInfo) -> Unit>()
)
} answers {
val callback = lambda<(FileInfo) -> Unit>().captured
callback(info1)
callback(info2)
}
coEvery { backend.load(info1.fileHandle) } returns inputStream
coEvery { backend.load(info2.fileHandle) } returns inputStream
every { metadataReader.readMetadata(inputStream, token) } returns metadata
every { metadataReader.readMetadata(inputStream, token + 1) } returns metadata
every { inputStream.close() } just Runs
@ -138,18 +164,28 @@ internal class RestoreCoordinatorTest : TransportTest() {
@Test
fun `startRestore() errors if metadata is not matching token`() = runBlocking {
coEvery { backend.getAvailableBackups() } returns sequenceOf(
EncryptedMetadata(token + 42) { inputStream }
)
every { metadataReader.readMetadata(inputStream, token + 42) } returns metadata
val otherToken = token + 42
val info = FileInfo(LegacyAppBackupFile.Metadata(otherToken), 23)
coEvery {
backend.list(
topLevelFolder = null,
AppBackupFileType.Snapshot::class, LegacyAppBackupFile.Metadata::class,
callback = captureLambda<(FileInfo) -> Unit>()
)
} answers {
val callback = lambda<(FileInfo) -> Unit>().captured
callback(info)
}
coEvery { backend.load(info.fileHandle) } returns inputStream
every { metadataReader.readMetadata(inputStream, otherToken) } returns metadata
every { inputStream.close() } just Runs
assertEquals(TRANSPORT_ERROR, restore.startRestore(token, packageInfoArray))
assertEquals(TRANSPORT_ERROR, restore.startRestore(otherToken, packageInfoArray))
}
@Test
fun `startRestore() can not be called twice`() = runBlocking {
restore.beforeStartRestore(metadata)
restore.beforeStartRestore(restorableBackup)
assertEquals(TRANSPORT_OK, restore.startRestore(token, packageInfoArray))
assertThrows(IllegalStateException::class.javaObjectType) {
runBlocking {
@ -161,13 +197,13 @@ internal class RestoreCoordinatorTest : TransportTest() {
@Test
fun `startRestore() can be be called again after restore finished`() = runBlocking {
restore.beforeStartRestore(metadata)
restore.beforeStartRestore(restorableBackup)
assertEquals(TRANSPORT_OK, restore.startRestore(token, packageInfoArray))
every { full.hasState() } returns false
restore.finishRestore()
restore.beforeStartRestore(metadata)
restore.beforeStartRestore(restorableBackup)
assertEquals(TRANSPORT_OK, restore.startRestore(token, packageInfoArray))
}
@ -201,7 +237,7 @@ internal class RestoreCoordinatorTest : TransportTest() {
every { backendManager.backendProperties } returns safStorage
every { safStorage.isUnavailableUsb(context) } returns false
restore.beforeStartRestore(metadata)
restore.beforeStartRestore(restorableBackup)
assertEquals(TRANSPORT_OK, restore.startRestore(token, pmPackageInfoArray))
verify(exactly = 0) {
@ -237,7 +273,7 @@ internal class RestoreCoordinatorTest : TransportTest() {
@Test
fun `nextRestorePackage() returns KV description`() = runBlocking {
restore.beforeStartRestore(metadata)
restore.beforeStartRestore(restorableBackup)
restore.startRestore(token, packageInfoArray)
every { crypto.getNameForPackage(metadata.salt, packageName) } returns name
@ -250,7 +286,8 @@ internal class RestoreCoordinatorTest : TransportTest() {
@Test
@Suppress("Deprecation")
fun `v0 nextRestorePackage() returns KV description and takes precedence`() = runBlocking {
restore.beforeStartRestore(metadata.copy(version = 0x00))
val backup = restorableBackup.copy(backupMetadata = metadata.copy(version = 0x00))
restore.beforeStartRestore(backup)
restore.startRestore(token, packageInfoArray)
coEvery { kv.hasDataForPackage(token, packageInfo) } returns true
@ -263,7 +300,8 @@ internal class RestoreCoordinatorTest : TransportTest() {
@Test
@Suppress("deprecation")
fun `v0 nextRestorePackage() returns full description if no KV data found`() = runBlocking {
restore.beforeStartRestore(metadata.copy(version = 0x00))
val backup = restorableBackup.copy(backupMetadata = metadata.copy(version = 0x00))
restore.beforeStartRestore(backup)
restore.startRestore(token, packageInfoArray)
coEvery { kv.hasDataForPackage(token, packageInfo) } returns false
@ -278,7 +316,7 @@ internal class RestoreCoordinatorTest : TransportTest() {
fun `nextRestorePackage() tries next package if one has no backup type()`() = runBlocking {
metadata.packageMetadataMap[packageName] =
metadata.packageMetadataMap[packageName]!!.copy(backupType = null)
restore.beforeStartRestore(metadata)
restore.beforeStartRestore(restorableBackup)
restore.startRestore(token, packageInfoArray2)
every { crypto.getNameForPackage(metadata.salt, packageInfo2.packageName) } returns name2
@ -292,7 +330,7 @@ internal class RestoreCoordinatorTest : TransportTest() {
@Test
fun `nextRestorePackage() returns all packages from startRestore()`() = runBlocking {
restore.beforeStartRestore(metadata)
restore.beforeStartRestore(restorableBackup)
restore.startRestore(token, packageInfoArray2)
every { crypto.getNameForPackage(metadata.salt, packageName) } returns name
@ -314,7 +352,8 @@ internal class RestoreCoordinatorTest : TransportTest() {
@Test
@Suppress("deprecation")
fun `v0 nextRestorePackage() returns all packages from startRestore()`() = runBlocking {
restore.beforeStartRestore(metadata.copy(version = 0x00))
val backup = restorableBackup.copy(backupMetadata = metadata.copy(version = 0x00))
restore.beforeStartRestore(backup)
restore.startRestore(token, packageInfoArray2)
coEvery { kv.hasDataForPackage(token, packageInfo) } returns true
@ -336,7 +375,8 @@ internal class RestoreCoordinatorTest : TransportTest() {
@Test
@Suppress("Deprecation")
fun `v0 when kv#hasDataForPackage() throws, it tries next package`() = runBlocking {
restore.beforeStartRestore(metadata.copy(version = 0x00))
val backup = restorableBackup.copy(backupMetadata = metadata.copy(version = 0x00))
restore.beforeStartRestore(backup)
restore.startRestore(token, packageInfoArray)
coEvery { kv.hasDataForPackage(token, packageInfo) } throws IOException()
@ -347,7 +387,8 @@ internal class RestoreCoordinatorTest : TransportTest() {
@Test
@Suppress("deprecation")
fun `v0 when full#hasDataForPackage() throws, it tries next package`() = runBlocking {
restore.beforeStartRestore(metadata.copy(version = 0x00))
val backup = restorableBackup.copy(backupMetadata = metadata.copy(version = 0x00))
restore.beforeStartRestore(backup)
restore.startRestore(token, packageInfoArray)
coEvery { kv.hasDataForPackage(token, packageInfo) } returns false

View file

@ -55,6 +55,7 @@ internal class RestoreV0IntegrationTest : TransportTest() {
private val metadataReader = MetadataReaderImpl(cryptoImpl)
private val notificationManager = mockk<BackupNotificationManager>()
private val backendManager: BackendManager = mockk()
private val loader = mockk<Loader>()
@Suppress("Deprecation")
private val legacyPlugin = mockk<LegacyStoragePlugin>()
@ -76,10 +77,14 @@ internal class RestoreV0IntegrationTest : TransportTest() {
metadataManager = metadataManager,
notificationManager = notificationManager,
backendManager = backendManager,
loader = loader,
kv = kvRestore,
full = fullRestore,
metadataReader = metadataReader,
).apply { beforeStartRestore(metadata.copy(version = 0x00)) }
).apply {
val backup = RestorableBackup(metadata.copy(version = 0x00))
beforeStartRestore(backup)
}
private val fileDescriptor = mockk<ParcelFileDescriptor>(relaxed = true)
private val appData = ("562AB665C3543120FC794D7CDA3AC18E5959235A4D" +