Skip to content

Commit

Permalink
Prepare restore backup loading for v2
Browse files Browse the repository at this point in the history
  • Loading branch information
grote committed Sep 9, 2024
1 parent aea0082 commit ca7cea1
Show file tree
Hide file tree
Showing 21 changed files with 268 additions and 160 deletions.
14 changes: 7 additions & 7 deletions app/src/androidTest/java/com/stevesoltys/seedvault/PluginTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -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
Expand All @@ -124,7 +124,7 @@ 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()
var availableBackups = backend.getAvailableBackupFileHandles()?.toList()
check(availableBackups != null)
assertEquals(1, availableBackups.size)
assertEquals(token, availableBackups[0].token)
Expand All @@ -134,7 +134,7 @@ class PluginTest : KoinComponent {

// initializing again (without changing storage) keeps restore set with same token
backend.save(LegacyAppBackupFile.Metadata(token)).writeAndClose(metadata)
availableBackups = backend.getAvailableBackups()?.toList()
availableBackups = backend.getAvailableBackupFileHandles()?.toList()
check(availableBackups != null)
assertEquals(1, availableBackups.size)
assertEquals(token, availableBackups[0].token)
Expand Down
34 changes: 12 additions & 22 deletions app/src/main/java/com/stevesoltys/seedvault/backend/BackendExt.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}

Expand All @@ -49,5 +41,3 @@ fun Exception.isOutOfSpace(): Boolean {
else -> false
}
}

class EncryptedMetadata(val token: Long, val inputStreamRetriever: suspend () -> InputStream)
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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) {
Expand Down
17 changes: 17 additions & 0 deletions app/src/main/java/com/stevesoltys/seedvault/metadata/Metadata.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,15 @@ 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
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
Expand All @@ -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
Expand Down Expand Up @@ -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)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ 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.transport.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
Expand Down
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -30,7 +41,7 @@ data class RestorableBackup(
val time: Long
get() = backupMetadata.time

val size: Long?
val size: Long
get() = backupMetadata.size

val deviceName: String
Expand Down
Loading

0 comments on commit ca7cea1

Please sign in to comment.