Skip to content

Commit

Permalink
WIP: K/V backup and restore using v2
Browse files Browse the repository at this point in the history
while maintaining support for v0 and v1
  • Loading branch information
grote committed Sep 10, 2024
1 parent 1615790 commit 50b6e4d
Show file tree
Hide file tree
Showing 9 changed files with 349 additions and 352 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ class KoinInstrumentationTestApp : App() {

single { spyk(BackupNotificationManager(context)) }
single { spyk(FullBackup(get(), get(), get(), get())) }
single { spyk(KVBackup(get(), get(), get(), get(), get(), get())) }
single { spyk(KVBackup(get(), get(), get(), get())) }
single { spyk(InputFactory()) }

single { spyk(FullRestore(get(), get(), get(), get(), get(), get())) }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ internal interface LargeBackupTestBase : LargeTestBase {
var data = mutableMapOf<String, ByteArray>()

coEvery {
spyKVBackup.performBackup(any(), any(), any(), any(), any())
spyKVBackup.performBackup(any(), any(), any())
} answers {
packageName = firstArg<PackageInfo>().packageName
callOriginal()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
/*
* SPDX-FileCopyrightText: 2024 The Calyx Institute
* SPDX-License-Identifier: Apache-2.0
*/

package com.stevesoltys.seedvault.transport.backup

import android.app.backup.BackupDataInput
import android.app.backup.BackupTransport.FLAG_NON_INCREMENTAL
import android.content.pm.PackageInfo
import android.os.ParcelFileDescriptor
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest
import com.stevesoltys.seedvault.settings.SettingsManager
import io.mockk.CapturingSlot
import io.mockk.Runs
import io.mockk.coEvery
import io.mockk.every
import io.mockk.just
import io.mockk.mockk
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertArrayEquals
import org.junit.Assert.assertNull
import org.junit.Test
import org.junit.runner.RunWith
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import kotlin.random.Random
import kotlin.test.assertEquals

@RunWith(AndroidJUnit4::class)
@MediumTest
class KvBackupInstrumentationTest : KoinComponent {

private val settingsManager: SettingsManager by inject()
private val backupReceiver: BackupReceiver = mockk()
private val inputFactory: InputFactory = mockk()
private val dbManager: KvDbManager by inject()

private val backup = KVBackup(
settingsManager = settingsManager,
backupReceiver = backupReceiver,
inputFactory = inputFactory,
dbManager = dbManager,
)

private val data = mockk<ParcelFileDescriptor>()
private val dataInput = mockk<BackupDataInput>()
private val key = "foo.bar"
private val dataValue = Random.nextBytes(23)

@Test
fun `test non-incremental backup with existing DB`() {
val packageName = "com.example"
val backupData = BackupData(emptyList(), emptyMap())

// create existing db
dbManager.getDb(packageName).use { db ->
db.put("foo", "bar".toByteArray())
}

val packageInfo = PackageInfo().apply {
this.packageName = packageName
}

every { backupReceiver.assertFinalized() } just Runs
every { inputFactory.getBackupDataInput(data) } returns dataInput
every { dataInput.readNextHeader() } returnsMany listOf(true, false)
every { dataInput.key } returns key
every { dataInput.dataSize } returns dataValue.size
val slot = CapturingSlot<ByteArray>()
every { dataInput.readEntityData(capture(slot), 0, dataValue.size) } answers {
dataValue.copyInto(slot.captured)
dataValue.size
}
every { data.close() } just Runs

backup.performBackup(packageInfo, data, FLAG_NON_INCREMENTAL)

coEvery { backupReceiver.readFromStream(any()) } just Runs
coEvery { backupReceiver.finalize() } returns backupData

runBlocking {
assertEquals(backupData, backup.finishBackup())
}

dbManager.getDb(packageName).use { db ->
assertNull(db.get("foo")) // existing data foo is gone
assertArrayEquals(dataValue, db.get(key)) // new data got added
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ import android.os.ParcelFileDescriptor
import android.util.Log
import androidx.annotation.WorkerThread
import com.stevesoltys.seedvault.Clock
import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER
import com.stevesoltys.seedvault.backend.BackendManager
import com.stevesoltys.seedvault.backend.getMetadataOutputStream
import com.stevesoltys.seedvault.backend.isOutOfSpace
Expand Down Expand Up @@ -157,7 +156,7 @@ internal class BackupCoordinator(
fun getBackupQuota(packageName: String, isFullBackup: Boolean): Long {
// report back quota
Log.i(TAG, "Get backup quota for $packageName. Is full backup: $isFullBackup.")
val quota = if (isFullBackup) full.quota else kv.getQuota()
val quota = if (isFullBackup) full.quota else kv.quota
Log.i(TAG, "Reported quota of $quota bytes.")
return quota
}
Expand Down Expand Up @@ -217,7 +216,7 @@ internal class BackupCoordinator(
* [TRANSPORT_NOT_INITIALIZED] (if the backend dataset has become lost due to
* inactivity purge or some other reason and needs re-initializing)
*/
suspend fun performIncrementalBackup(
fun performIncrementalBackup(
packageInfo: PackageInfo,
data: ParcelFileDescriptor,
flags: Int,
Expand All @@ -232,9 +231,7 @@ internal class BackupCoordinator(
// This causes a backup error, but things should go back to normal afterwards.
return TRANSPORT_NOT_INITIALIZED
}
val token = settingsManager.getToken() ?: error("no token in performFullBackup")
val salt = metadataManager.salt
return kv.performBackup(packageInfo, data, flags, token, salt)
return kv.performBackup(packageInfo, data, flags)
}

// ------------------------------------------------------------------------------------
Expand Down Expand Up @@ -323,17 +320,8 @@ internal class BackupCoordinator(
*
* @return the same error codes as [performFullBackup].
*/
suspend fun clearBackupData(packageInfo: PackageInfo): Int {
val packageName = packageInfo.packageName
Log.i(TAG, "Clear Backup Data of $packageName.")
val token = settingsManager.getToken() ?: error("no token in clearBackupData")
val salt = metadataManager.salt
try {
kv.clearBackupData(packageInfo, token, salt)
} catch (e: IOException) {
Log.w(TAG, "Error clearing K/V backup data for $packageName", e)
return TRANSPORT_ERROR
}
fun clearBackupData(packageInfo: PackageInfo): Int {
Log.i(TAG, "Ignoring clear backup data of ${packageInfo.packageName}.")
// we don't clear backup data anymore, we have snapshots and those old ones stay valid
state.calledClearBackupData = true
return TRANSPORT_OK
Expand All @@ -348,33 +336,29 @@ internal class BackupCoordinator(
* @return the same error codes as [performIncrementalBackup] or [performFullBackup].
*/
suspend fun finishBackup(): Int = when {
kv.hasState() -> {
kv.hasState -> {
check(!full.hasState) {
"K/V backup has state, but full backup has dangling state as well"
}
// getCurrentPackage() not-null because we have state, call before finishing
val packageInfo = kv.getCurrentPackage()!!
val packageInfo = kv.currentPackageInfo!!
val packageName = packageInfo.packageName
val size = kv.getCurrentSize()
// tell K/V backup to finish
var result = kv.finishBackup()
if (result == TRANSPORT_OK) {
val isNormalBackup = packageName != MAGIC_PACKAGE_MANAGER
// call onPackageBackedUp for @pm@ only if we can do backups right now
if (isNormalBackup || backendManager.canDoBackupNow()) {
try {
metadataManager.onPackageBackedUp(packageInfo, BackupType.KV, size)
} catch (e: Exception) {
Log.e(TAG, "Error calling onPackageBackedUp for $packageName", e)
if (e.isOutOfSpace()) nm.onInsufficientSpaceError()
result = TRANSPORT_PACKAGE_REJECTED
}
}
try {
// tell K/V backup to finish
val backupData = kv.finishBackup()
snapshotCreator.onPackageBackedUp(packageInfo, BackupType.KV, backupData)
// TODO unify both calls
metadataManager.onPackageBackedUp(packageInfo, BackupType.KV, backupData.size)
TRANSPORT_OK
} catch (e: Exception) {
Log.e(TAG, "Error finishing K/V backup for $packageName", e)
if (e.isOutOfSpace()) nm.onInsufficientSpaceError()
onPackageBackupError(packageInfo, BackupType.KV)
TRANSPORT_PACKAGE_REJECTED
}
result
}
full.hasState -> {
check(!kv.hasState()) {
check(!kv.hasState) {
"Full backup has state, but K/V backup has dangling state as well"
}
// getCurrentPackage() not-null because we have state
Expand All @@ -390,6 +374,7 @@ internal class BackupCoordinator(
} catch (e: Exception) {
Log.e(TAG, "Error calling onPackageBackedUp for $packageName", e)
if (e.isOutOfSpace()) nm.onInsufficientSpaceError()
onPackageBackupError(packageInfo, BackupType.FULL)
TRANSPORT_PACKAGE_REJECTED
}
}
Expand All @@ -400,6 +385,7 @@ internal class BackupCoordinator(
else -> throw IllegalStateException("Unexpected state in finishBackup()")
}

// TODO is this only nice to have info, or do we need to do more?
private suspend fun onPackageBackupError(packageInfo: PackageInfo, type: BackupType) {
val packageName = packageInfo.packageName
try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,9 @@ val backupModule = module {
single<KvDbManager> { KvDbManagerImpl(androidContext()) }
single {
KVBackup(
backendManager = get(),
settingsManager = get(),
nm = get(),
backupReceiver = get(),
inputFactory = get(),
crypto = get(),
dbManager = get(),
)
}
Expand Down
Loading

0 comments on commit 50b6e4d

Please sign in to comment.