Make APK backup self-healing

This commit is contained in:
Torsten Grote 2024-10-30 09:58:33 -03:00
parent 15e8850e5e
commit 4f5199ce27
No known key found for this signature in database
GPG key ID: 3E5F77D92CF891FF
5 changed files with 80 additions and 14 deletions

View file

@ -62,6 +62,15 @@ class BlobCache(
*/ */
operator fun get(chunkId: String): Blob? = blobMap[chunkId] operator fun get(chunkId: String): Blob? = blobMap[chunkId]
/**
* Should only be called after [populateCache] has returned.
*
* @return true if all [chunkIds] are in cache, or false if one or more is missing.
*/
fun containsAll(chunkIds: List<String>): Boolean = chunkIds.all { chunkId ->
blobMap.containsKey(chunkId)
}
/** /**
* Should get called for all new blobs as soon as they've been saved to the backend. * Should get called for all new blobs as soon as they've been saved to the backend.
*/ */

View file

@ -19,6 +19,7 @@ import com.stevesoltys.seedvault.proto.Snapshot.Blob
import com.stevesoltys.seedvault.proto.SnapshotKt.split import com.stevesoltys.seedvault.proto.SnapshotKt.split
import com.stevesoltys.seedvault.repo.AppBackupManager import com.stevesoltys.seedvault.repo.AppBackupManager
import com.stevesoltys.seedvault.repo.BackupReceiver import com.stevesoltys.seedvault.repo.BackupReceiver
import com.stevesoltys.seedvault.repo.BlobCache
import com.stevesoltys.seedvault.repo.forProto import com.stevesoltys.seedvault.repo.forProto
import com.stevesoltys.seedvault.repo.hexFromProto import com.stevesoltys.seedvault.repo.hexFromProto
import com.stevesoltys.seedvault.settings.SettingsManager import com.stevesoltys.seedvault.settings.SettingsManager
@ -37,6 +38,7 @@ internal class ApkBackup(
private val backupReceiver: BackupReceiver, private val backupReceiver: BackupReceiver,
private val appBackupManager: AppBackupManager, private val appBackupManager: AppBackupManager,
private val settingsManager: SettingsManager, private val settingsManager: SettingsManager,
private val blobCache: BlobCache,
) { ) {
private val snapshotCreator private val snapshotCreator
@ -101,22 +103,26 @@ internal class ApkBackup(
if (!needsBackup && oldApk != null) { if (!needsBackup && oldApk != null) {
// We could also check if there are new feature module splits to back up, // We could also check if there are new feature module splits to back up,
// but we rely on the app themselves to re-download those, if needed after restore. // but we rely on the app themselves to re-download those, if needed after restore.
Log.d(
TAG, "Package $packageName with version $version" +
" already has a backup ($backedUpVersion)" +
" with the same signature. Not backing it up."
)
// build up blobMap from old snapshot
val chunkIds = oldApk.splitsList.flatMap { val chunkIds = oldApk.splitsList.flatMap {
it.chunkIdsList.map { chunkId -> chunkId.hexFromProto() } it.chunkIdsList.map { chunkId -> chunkId.hexFromProto() }
} }
val blobMap = chunkIds.associateWith { chunkId -> if (blobCache.containsAll(chunkIds)) {
latestSnapshot.blobsMap[chunkId] ?: error("Missing blob for $chunkId") Log.d(
TAG, "Package $packageName with version $version" +
" already has a backup ($backedUpVersion)" +
" with the same signature. Not backing it up."
)
// all blobs are cached, i.e. still on backend, so no new backup needed
val blobMap = chunkIds.associateWith { chunkId ->
latestSnapshot.blobsMap[chunkId] ?: error("Missing blob for $chunkId")
}
// important: add old APK to snapshot or it wouldn't be part of backup
snapshotCreator.onApkBackedUp(packageInfo, oldApk, blobMap)
return
} else {
Log.w(TAG, "Blobs for APKs of $packageName have issues in backend. Fixing...")
} }
// TODO could also check if all blobs are (still) available in BlobCache
// important: add old APK to snapshot or it wouldn't be part of backup
snapshotCreator.onApkBackedUp(packageInfo, oldApk, blobMap)
return
} }
// builder for Apk object // builder for Apk object

View file

@ -33,6 +33,7 @@ val workerModule = module {
backupReceiver = get(), backupReceiver = get(),
appBackupManager = get(), appBackupManager = get(),
settingsManager = get(), settingsManager = get(),
blobCache = get(),
) )
} }
single { single {

View file

@ -24,6 +24,7 @@ import com.stevesoltys.seedvault.metadata.PackageMetadataMap
import com.stevesoltys.seedvault.proto.Snapshot import com.stevesoltys.seedvault.proto.Snapshot
import com.stevesoltys.seedvault.repo.AppBackupManager import com.stevesoltys.seedvault.repo.AppBackupManager
import com.stevesoltys.seedvault.repo.BackupReceiver import com.stevesoltys.seedvault.repo.BackupReceiver
import com.stevesoltys.seedvault.repo.BlobCache
import com.stevesoltys.seedvault.repo.Loader import com.stevesoltys.seedvault.repo.Loader
import com.stevesoltys.seedvault.repo.SnapshotCreator import com.stevesoltys.seedvault.repo.SnapshotCreator
import com.stevesoltys.seedvault.repo.SnapshotManager import com.stevesoltys.seedvault.repo.SnapshotManager
@ -70,6 +71,7 @@ internal class ApkBackupRestoreTest : TransportTest() {
private val backupStateManager: BackupStateManager = mockk() private val backupStateManager: BackupStateManager = mockk()
private val backupReceiver: BackupReceiver = mockk() private val backupReceiver: BackupReceiver = mockk()
private val appBackupManager: AppBackupManager = mockk() private val appBackupManager: AppBackupManager = mockk()
private val blobCache: BlobCache = mockk()
private val snapshotManager: SnapshotManager = mockk() private val snapshotManager: SnapshotManager = mockk()
private val snapshotCreator: SnapshotCreator = mockk() private val snapshotCreator: SnapshotCreator = mockk()
private val loader: Loader = mockk() private val loader: Loader = mockk()
@ -81,7 +83,8 @@ internal class ApkBackupRestoreTest : TransportTest() {
private val apkInstaller: ApkInstaller = mockk() private val apkInstaller: ApkInstaller = mockk()
private val installRestriction: InstallRestriction = mockk() private val installRestriction: InstallRestriction = mockk()
private val apkBackup = ApkBackup(pm, backupReceiver, appBackupManager, settingsManager) private val apkBackup =
ApkBackup(pm, backupReceiver, appBackupManager, settingsManager, blobCache)
private val apkRestore: ApkRestore = ApkRestore( private val apkRestore: ApkRestore = ApkRestore(
context = strictContext, context = strictContext,
backupManager = backupManager, backupManager = backupManager,

View file

@ -23,7 +23,9 @@ import com.stevesoltys.seedvault.proto.copy
import com.stevesoltys.seedvault.repo.AppBackupManager import com.stevesoltys.seedvault.repo.AppBackupManager
import com.stevesoltys.seedvault.repo.BackupData import com.stevesoltys.seedvault.repo.BackupData
import com.stevesoltys.seedvault.repo.BackupReceiver import com.stevesoltys.seedvault.repo.BackupReceiver
import com.stevesoltys.seedvault.repo.BlobCache
import com.stevesoltys.seedvault.repo.SnapshotCreator import com.stevesoltys.seedvault.repo.SnapshotCreator
import com.stevesoltys.seedvault.repo.hexFromProto
import com.stevesoltys.seedvault.transport.backup.BackupTest import com.stevesoltys.seedvault.transport.backup.BackupTest
import io.mockk.Runs import io.mockk.Runs
import io.mockk.coEvery import io.mockk.coEvery
@ -51,9 +53,11 @@ internal class ApkBackupTest : BackupTest() {
private val pm: PackageManager = mockk() private val pm: PackageManager = mockk()
private val backupReceiver: BackupReceiver = mockk() private val backupReceiver: BackupReceiver = mockk()
private val appBackupManager: AppBackupManager = mockk() private val appBackupManager: AppBackupManager = mockk()
private val blobCache: BlobCache = mockk()
private val snapshotCreator: SnapshotCreator = mockk() private val snapshotCreator: SnapshotCreator = mockk()
private val apkBackup = ApkBackup(pm, backupReceiver, appBackupManager, settingsManager) private val apkBackup =
ApkBackup(pm, backupReceiver, appBackupManager, settingsManager, blobCache)
private val signatureBytes = byteArrayOf(0x01, 0x02, 0x03) private val signatureBytes = byteArrayOf(0x01, 0x02, 0x03)
private val signatureHash = byteArrayOf(0x03, 0x02, 0x01) private val signatureHash = byteArrayOf(0x03, 0x02, 0x01)
@ -110,7 +114,9 @@ internal class ApkBackupTest : BackupTest() {
val apk = apk.copy { versionCode = packageInfo.longVersionCode } val apk = apk.copy { versionCode = packageInfo.longVersionCode }
val app = app { this.apk = apk } val app = app { this.apk = apk }
val s = snapshot.copy { apps.put(packageName, app) } val s = snapshot.copy { apps.put(packageName, app) }
val chunkIds = apk.splitsList.flatMap { it.chunkIdsList.hexFromProto() }
expectChecks() expectChecks()
every { blobCache.containsAll(chunkIds) } returns true
every { every {
snapshotCreator.onApkBackedUp(packageInfo, apk, blobMap) snapshotCreator.onApkBackedUp(packageInfo, apk, blobMap)
} just Runs } just Runs
@ -162,6 +168,47 @@ internal class ApkBackupTest : BackupTest() {
} }
} }
@Test
fun `does back up the same version when blobs are missing from cache`(@TempDir tmpDir: Path) =
runBlocking {
val tmpFile = File(tmpDir.toAbsolutePath().toString())
packageInfo.applicationInfo!!.sourceDir = File(tmpFile, "test.apk").apply {
assertTrue(createNewFile())
}.absolutePath
val apk = apk.copy {
versionCode = packageInfo.longVersionCode
splits.clear()
splits.add(baseSplit)
}
val app = app { this.apk = apk }
val s = snapshot.copy { apps.put(packageName, app) }
val chunkIds = apk.splitsList.flatMap { it.chunkIdsList.hexFromProto() }
expectChecks()
every { blobCache.containsAll(chunkIds) } returns false // blobs missing here
every {
pm.getInstallSourceInfo(packageInfo.packageName)
} returns InstallSourceInfo(null, null, null, apk.installer)
coEvery {
backupReceiver.readFromStream("APK backup $packageName ", any())
} returns apkBackupData
every {
snapshotCreator.onApkBackedUp(packageInfo, match<Snapshot.Apk> {
it.installer == apk.installer
}, apkBackupData.blobMap)
} just Runs
apkBackup.backupApkIfNecessary(packageInfo, s)
coVerify {
backupReceiver.readFromStream("APK backup $packageName ", any())
snapshotCreator.onApkBackedUp(packageInfo, match<Snapshot.Apk> {
it.installer == apk.installer
}, apkBackupData.blobMap)
}
}
@Test @Test
fun `throws exception when APK doesn't exist`() { fun `throws exception when APK doesn't exist`() {
packageInfo.applicationInfo!!.sourceDir = "/tmp/doesNotExist" packageInfo.applicationInfo!!.sourceDir = "/tmp/doesNotExist"