[db] explicitly clear repo data before updating with full index

In JSON, keys can come in any order, so we need to handle the case that we receive packages before the repo metadata. Now we explicitly clear data and rename the insert method to insertOrReplace in order to make it clear that data gets replaced.

Also, we pass the repoId into the constructor of the DB stream receivers to make clear that one receiver is meant to receive a single pre-existing repo.
This commit is contained in:
Torsten Grote
2022-04-28 08:47:01 -03:00
parent 8d60009e49
commit 7fbb08de1b
12 changed files with 184 additions and 94 deletions

View File

@@ -28,7 +28,7 @@ internal class AppTest : DbTest() {
@Test
fun insertGetDeleteSingleApp() {
val repoId = repoDao.insert(getRandomRepo())
val repoId = repoDao.insertOrReplace(getRandomRepo())
val metadataV2 = getRandomMetadataV2()
appDao.insert(repoId, packageId, metadataV2)
@@ -53,7 +53,7 @@ internal class AppTest : DbTest() {
@Test
fun testAppOverViewItem() {
val repoId = repoDao.insert(getRandomRepo())
val repoId = repoDao.insertOrReplace(getRandomRepo())
val packageId1 = getRandomString()
val packageId2 = getRandomString()
val packageId3 = getRandomString()
@@ -90,7 +90,7 @@ internal class AppTest : DbTest() {
assertNull(apps3.find { it.packageId == packageId3 })
// app4 is the same as app1 and thus will not be shown again
val repoId2 = repoDao.insert(getRandomRepo())
val repoId2 = repoDao.insertOrReplace(getRandomRepo())
val app4 = getRandomMetadataV2().copy(name = name2, icon = icons2)
appDao.insert(repoId2, packageId1, app4)
val apps4 = appDao.getAppOverviewItems().getOrAwaitValue() ?: fail()
@@ -99,8 +99,8 @@ internal class AppTest : DbTest() {
@Test
fun testAppByRepoWeight() {
val repoId1 = repoDao.insert(getRandomRepo())
val repoId2 = repoDao.insert(getRandomRepo())
val repoId1 = repoDao.insertOrReplace(getRandomRepo())
val repoId2 = repoDao.insertOrReplace(getRandomRepo())
val metadata1 = getRandomMetadataV2()
val metadata2 = metadata1.copy(lastUpdated = metadata1.lastUpdated + 1)

View File

@@ -6,8 +6,16 @@ import androidx.test.core.app.ApplicationProvider.getApplicationContext
import androidx.test.ext.junit.runners.AndroidJUnit4
import kotlinx.serialization.SerializationException
import org.apache.commons.io.input.CountingInputStream
import org.fdroid.CompatibilityChecker
import org.fdroid.index.v1.IndexV1StreamProcessor
import org.fdroid.index.v2.IndexStreamProcessor
import org.fdroid.index.v1.IndexV1StreamReceiver
import org.fdroid.index.v2.AntiFeatureV2
import org.fdroid.index.v2.CategoryV2
import org.fdroid.index.v2.IndexV2StreamProcessor
import org.fdroid.index.v2.MetadataV2
import org.fdroid.index.v2.PackageVersionV2
import org.fdroid.index.v2.ReleaseChannelV2
import org.fdroid.index.v2.RepoV2
import org.junit.Test
import org.junit.runner.RunWith
import kotlin.math.roundToInt
@@ -25,7 +33,8 @@ internal class IndexV1InsertTest : DbTest() {
val fileSize = c.resources.assets.openFd("index-v1.json").use { it.length }
val inputStream = CountingInputStream(c.resources.assets.open("index-v1.json"))
var currentByteCount: Long = 0
val indexProcessor = IndexV1StreamProcessor(DbV1StreamReceiver(db) { true }, null) {
val repoId = db.getRepositoryDao().insertEmptyRepo("https://f-droid.org/repo")
val streamReceiver = TestStreamReceiver(repoId) {
val bytesRead = inputStream.byteCount
val bytesSinceLastCall = bytesRead - currentByteCount
if (bytesSinceLastCall > 0) {
@@ -36,13 +45,12 @@ internal class IndexV1InsertTest : DbTest() {
// the stream gets read in big chunks, but ensure they are not too big, e.g. entire file
assertTrue(bytesSinceLastCall < 600_000, "$bytesSinceLastCall")
currentByteCount = bytesRead
bytesRead
}
val indexProcessor = IndexV1StreamProcessor(streamReceiver, null)
db.runInTransaction {
val repoId = db.getRepositoryDao().insertEmptyRepo("https://f-droid.org/repo")
inputStream.use { indexStream ->
indexProcessor.process(repoId, indexStream)
indexProcessor.process(indexStream)
}
}
assertTrue(repoDao.getRepositories().size == 1)
@@ -112,11 +120,11 @@ internal class IndexV1InsertTest : DbTest() {
private fun insertV2ForComparison(version: Int) {
val c = getApplicationContext<Context>()
val inputStream = CountingInputStream(c.resources.assets.open("index-v2.json"))
val indexProcessor = IndexStreamProcessor(DbStreamReceiver(db) { true }, null)
val repoId = db.getRepositoryDao().insertEmptyRepo("https://f-droid.org/repo")
val indexProcessor = IndexV2StreamProcessor(DbV2StreamReceiver(db, { true }, repoId), null)
db.runInTransaction {
val repoId = db.getRepositoryDao().insertEmptyRepo("https://f-droid.org/repo")
inputStream.use { indexStream ->
indexProcessor.process(repoId, version, indexStream)
indexProcessor.process(version, indexStream)
}
}
}
@@ -125,15 +133,17 @@ internal class IndexV1InsertTest : DbTest() {
fun testExceptionWhileStreamingDoesNotSaveIntoDb() {
val c = getApplicationContext<Context>()
val cIn = CountingInputStream(c.resources.assets.open("index-v1.json"))
val indexProcessor = IndexStreamProcessor(DbStreamReceiver(db) { true }, null) {
val compatibilityChecker = CompatibilityChecker {
if (cIn.byteCount > 824096) throw SerializationException()
cIn.byteCount
true
}
val indexProcessor =
IndexV2StreamProcessor(DbV2StreamReceiver(db, compatibilityChecker, 1), null)
assertFailsWith<SerializationException> {
db.runInTransaction {
cIn.use { indexStream ->
indexProcessor.process(1, 42, indexStream)
indexProcessor.process(42, indexStream)
}
}
}
@@ -145,4 +155,40 @@ internal class IndexV1InsertTest : DbTest() {
assertTrue(versionDao.countVersionedStrings() == 0)
}
inner class TestStreamReceiver(
repoId: Long,
private val callback: () -> Unit,
) : IndexV1StreamReceiver {
private val streamReceiver = DbV1StreamReceiver(db, { true }, repoId)
override fun receive(repo: RepoV2, version: Int, certificate: String?) {
streamReceiver.receive(repo, version, certificate)
callback()
}
override fun receive(packageId: String, m: MetadataV2) {
streamReceiver.receive(packageId, m)
callback()
}
override fun receive(packageId: String, v: Map<String, PackageVersionV2>) {
streamReceiver.receive(packageId, v)
callback()
}
override fun updateRepo(
antiFeatures: Map<String, AntiFeatureV2>,
categories: Map<String, CategoryV2>,
releaseChannels: Map<String, ReleaseChannelV2>,
) {
streamReceiver.updateRepo(antiFeatures, categories, releaseChannels)
callback()
}
override fun updateAppMetadata(packageId: String, preferredSigner: String?) {
streamReceiver.updateAppMetadata(packageId, preferredSigner)
callback()
}
}
}

View File

@@ -6,7 +6,8 @@ import androidx.test.core.app.ApplicationProvider.getApplicationContext
import androidx.test.ext.junit.runners.AndroidJUnit4
import kotlinx.serialization.SerializationException
import org.apache.commons.io.input.CountingInputStream
import org.fdroid.index.v2.IndexStreamProcessor
import org.fdroid.CompatibilityChecker
import org.fdroid.index.v2.IndexV2StreamProcessor
import org.junit.Test
import org.junit.runner.RunWith
import kotlin.math.roundToInt
@@ -22,7 +23,7 @@ internal class IndexV2InsertTest : DbTest() {
val fileSize = c.resources.assets.openFd("index-v2.json").use { it.length }
val inputStream = CountingInputStream(c.resources.assets.open("index-v2.json"))
var currentByteCount: Long = 0
val indexProcessor = IndexStreamProcessor(DbStreamReceiver(db) { true }, null) {
val compatibilityChecker = CompatibilityChecker {
val bytesRead = inputStream.byteCount
val bytesSinceLastCall = bytesRead - currentByteCount
if (bytesSinceLastCall > 0) {
@@ -33,13 +34,15 @@ internal class IndexV2InsertTest : DbTest() {
// the stream gets read in big chunks, but ensure they are not too big, e.g. entire file
assertTrue(bytesSinceLastCall < 400_000, "$bytesSinceLastCall")
currentByteCount = bytesRead
bytesRead
true
}
val repoId = db.getRepositoryDao().insertEmptyRepo("https://f-droid.org/repo")
val streamReceiver = DbV2StreamReceiver(db, compatibilityChecker, repoId)
val indexProcessor = IndexV2StreamProcessor(streamReceiver, null)
db.runInTransaction {
val repoId = db.getRepositoryDao().insertEmptyRepo("https://f-droid.org/repo")
inputStream.use { indexStream ->
indexProcessor.process(repoId, 42, indexStream)
indexProcessor.process(42, indexStream)
}
}
assertTrue(repoDao.getRepositories().size == 1)
@@ -60,15 +63,17 @@ internal class IndexV2InsertTest : DbTest() {
fun testExceptionWhileStreamingDoesNotSaveIntoDb() {
val c = getApplicationContext<Context>()
val cIn = CountingInputStream(c.resources.assets.open("index-v2.json"))
val indexProcessor = IndexStreamProcessor(DbStreamReceiver(db) { true }, null) {
val compatibilityChecker = CompatibilityChecker {
if (cIn.byteCount > 824096) throw SerializationException()
cIn.byteCount
true
}
assertFailsWith<SerializationException> {
db.runInTransaction {
val repoId = db.getRepositoryDao().insertEmptyRepo("http://example.org")
val streamReceiver = DbV2StreamReceiver(db, compatibilityChecker, repoId)
val indexProcessor = IndexV2StreamProcessor(streamReceiver, null)
cIn.use { indexStream ->
indexProcessor.process(1, 42, indexStream)
indexProcessor.process(42, indexStream)
}
}
}

View File

@@ -48,10 +48,10 @@ internal class RepositoryDiffTest : DbTest() {
fun timestampDiffTwoReposInDb() {
// insert repo
val repo = getRandomRepo()
repoDao.insert(repo)
repoDao.insertOrReplace(repo)
// insert another repo before updating
repoDao.insert(getRandomRepo())
repoDao.insertOrReplace(getRandomRepo())
// check that the repo got added and retrieved as expected
var repos = repoDao.getRepositories().sortedBy { it.repoId }
@@ -244,7 +244,7 @@ internal class RepositoryDiffTest : DbTest() {
private fun testDiff(repo: RepoV2, json: String, repoChecker: (List<Repository>) -> Unit) {
// insert repo
repoDao.insert(repo)
repoDao.insertOrReplace(repo)
// check that the repo got added and retrieved as expected
var repos = repoDao.getRepositories()

View File

@@ -20,7 +20,7 @@ internal class RepositoryTest : DbTest() {
fun insertAndDeleteTwoRepos() {
// insert first repo
val repo1 = getRandomRepo()
val repoId1 = repoDao.insert(repo1)
val repoId1 = repoDao.insertOrReplace(repo1)
// check that first repo got added and retrieved as expected
var repos = repoDao.getRepositories()
@@ -31,7 +31,7 @@ internal class RepositoryTest : DbTest() {
// insert second repo
val repo2 = getRandomRepo()
val repoId2 = repoDao.insert(repo2)
val repoId2 = repoDao.insertOrReplace(repo2)
// check that both repos got added and retrieved as expected
repos = repoDao.getRepositories().sortedBy { it.repoId }
@@ -58,8 +58,8 @@ internal class RepositoryTest : DbTest() {
}
@Test
fun replacingRepoRemovesAllAssociatedData() {
val repoId = repoDao.insert(getRandomRepo())
fun clearingRepoRemovesAllAssociatedData() {
val repoId = repoDao.insertOrReplace(getRandomRepo())
val repositoryPreferences = repoDao.getRepositoryPreferences(repoId)
val packageId = getRandomString()
val versionId = getRandomString()
@@ -72,22 +72,20 @@ internal class RepositoryTest : DbTest() {
assertEquals(1, versionDao.getAppVersions(repoId, packageId).size)
assertTrue(versionDao.getVersionedStrings(repoId, packageId).isNotEmpty())
val cert = getRandomString()
repoDao.replace(repoId, getRandomRepo(), 42, cert)
repoDao.clear(repoId)
assertEquals(1, repoDao.getRepositories().size)
assertEquals(0, appDao.getAppMetadata().size)
assertEquals(0, appDao.getLocalizedFiles().size)
assertEquals(0, appDao.getLocalizedFileLists().size)
assertEquals(0, versionDao.getAppVersions(repoId, packageId).size)
assertEquals(0, versionDao.getVersionedStrings(repoId, packageId).size)
assertEquals(cert, repoDao.getRepository(repoId)?.certificate)
// preferences are not touched by clearing
assertEquals(repositoryPreferences, repoDao.getRepositoryPreferences(repoId))
}
@Test
fun certGetsUpdates() {
val repoId = repoDao.insert(getRandomRepo())
fun certGetsUpdated() {
val repoId = repoDao.insertOrReplace(getRandomRepo())
assertEquals(1, repoDao.getRepositories().size)
assertEquals(null, repoDao.getRepositories()[0].certificate)

View File

@@ -25,16 +25,16 @@ internal class UpdateCheckerTest : DbTest() {
updateChecker = UpdateChecker(db, context.packageManager)
}
@OptIn(ExperimentalTime::class)
@Test
@OptIn(ExperimentalTime::class)
fun testGetUpdates() {
val inputStream = CountingInputStream(context.resources.assets.open("index-v1.json"))
val indexProcessor = IndexV1StreamProcessor(DbV1StreamReceiver(db) { true }, null)
val repoId = db.getRepositoryDao().insertEmptyRepo("https://f-droid.org/repo")
val indexProcessor = IndexV1StreamProcessor(DbV1StreamReceiver(db, { true }, repoId), null)
db.runInTransaction {
val repoId = db.getRepositoryDao().insertEmptyRepo("https://f-droid.org/repo")
inputStream.use { indexStream ->
indexProcessor.process(repoId, indexStream)
indexProcessor.process(indexStream)
}
}

View File

@@ -25,7 +25,7 @@ internal class VersionTest : DbTest() {
@Test
fun insertGetDeleteSingleVersion() {
val repoId = repoDao.insert(getRandomRepo())
val repoId = repoDao.insertOrReplace(getRandomRepo())
appDao.insert(repoId, packageId, getRandomMetadataV2())
val packageVersion = getRandomPackageVersionV2()
val isCompatible = Random.nextBoolean()
@@ -57,7 +57,7 @@ internal class VersionTest : DbTest() {
@Test
fun insertGetDeleteTwoVersions() {
// insert two versions along with required objects
val repoId = repoDao.insert(getRandomRepo())
val repoId = repoDao.insertOrReplace(getRandomRepo())
appDao.insert(repoId, packageId, getRandomMetadataV2())
val packageVersion1 = getRandomPackageVersionV2()
val packageVersion2 = getRandomPackageVersionV2()

View File

@@ -1,33 +0,0 @@
package org.fdroid.database
import android.content.res.Resources
import androidx.core.os.ConfigurationCompat.getLocales
import androidx.core.os.LocaleListCompat
import org.fdroid.CompatibilityChecker
import org.fdroid.index.v2.IndexStreamReceiver
import org.fdroid.index.v2.PackageV2
import org.fdroid.index.v2.RepoV2
internal class DbStreamReceiver(
private val db: FDroidDatabaseInt,
private val compatibilityChecker: CompatibilityChecker,
) : IndexStreamReceiver {
private val locales: LocaleListCompat = getLocales(Resources.getSystem().configuration)
override fun receive(repoId: Long, repo: RepoV2, version: Int, certificate: String?) {
db.getRepositoryDao().replace(repoId, repo, version, certificate)
}
override fun receive(repoId: Long, packageId: String, p: PackageV2) {
db.getAppDao().insert(repoId, packageId, p.metadata, locales)
db.getVersionDao().insert(repoId, packageId, p.versions) {
compatibilityChecker.isCompatible(it.manifest)
}
}
override fun onStreamEnded(repoId: Long) {
db.afterUpdatingRepo(repoId)
}
}

View File

@@ -12,29 +12,35 @@ import org.fdroid.index.v2.PackageVersionV2
import org.fdroid.index.v2.ReleaseChannelV2
import org.fdroid.index.v2.RepoV2
/**
* Note that this class expects that its [receive] method with [RepoV2] gets called first.
* A different order of calls is not supported.
*/
@Deprecated("Use DbV2StreamReceiver instead")
internal class DbV1StreamReceiver(
private val db: FDroidDatabaseInt,
private val compatibilityChecker: CompatibilityChecker,
private val repoId: Long,
) : IndexV1StreamReceiver {
private val locales: LocaleListCompat = getLocales(Resources.getSystem().configuration)
override fun receive(repoId: Long, repo: RepoV2, version: Int, certificate: String?) {
db.getRepositoryDao().replace(repoId, repo, version, certificate)
override fun receive(repo: RepoV2, version: Int, certificate: String?) {
db.getRepositoryDao().clear(repoId)
db.getRepositoryDao().update(repoId, repo, version, certificate)
}
override fun receive(repoId: Long, packageId: String, m: MetadataV2) {
override fun receive(packageId: String, m: MetadataV2) {
db.getAppDao().insert(repoId, packageId, m, locales)
}
override fun receive(repoId: Long, packageId: String, v: Map<String, PackageVersionV2>) {
override fun receive(packageId: String, v: Map<String, PackageVersionV2>) {
db.getVersionDao().insert(repoId, packageId, v) {
compatibilityChecker.isCompatible(it.manifest)
}
}
override fun updateRepo(
repoId: Long,
antiFeatures: Map<String, AntiFeatureV2>,
categories: Map<String, CategoryV2>,
releaseChannels: Map<String, ReleaseChannelV2>,
@@ -47,7 +53,7 @@ internal class DbV1StreamReceiver(
db.afterUpdatingRepo(repoId)
}
override fun updateAppMetadata(repoId: Long, packageId: String, preferredSigner: String?) {
override fun updateAppMetadata(packageId: String, preferredSigner: String?) {
db.getAppDao().updatePreferredSigner(repoId, packageId, preferredSigner)
}

View File

@@ -0,0 +1,52 @@
package org.fdroid.database
import android.content.res.Resources
import androidx.core.os.ConfigurationCompat.getLocales
import androidx.core.os.LocaleListCompat
import org.fdroid.CompatibilityChecker
import org.fdroid.index.v2.IndexV2StreamReceiver
import org.fdroid.index.v2.PackageV2
import org.fdroid.index.v2.RepoV2
internal class DbV2StreamReceiver(
private val db: FDroidDatabaseInt,
private val compatibilityChecker: CompatibilityChecker,
private val repoId: Long,
) : IndexV2StreamReceiver {
private val locales: LocaleListCompat = getLocales(Resources.getSystem().configuration)
private var clearedRepoData = false
@Synchronized
override fun receive(repo: RepoV2, version: Int, certificate: String?) {
clearRepoDataIfNeeded()
db.getRepositoryDao().update(repoId, repo, version, certificate)
}
@Synchronized
override fun receive(packageId: String, p: PackageV2) {
clearRepoDataIfNeeded()
db.getAppDao().insert(repoId, packageId, p.metadata, locales)
db.getVersionDao().insert(repoId, packageId, p.versions) {
compatibilityChecker.isCompatible(it.manifest)
}
}
@Synchronized
override fun onStreamEnded() {
db.afterUpdatingRepo(repoId)
}
/**
* As it is a valid index to receive packages before the repo,
* we can not clear all repo data when receiving the repo,
* but need to do it once at the beginning.
*/
private fun clearRepoDataIfNeeded() {
if (!clearedRepoData) {
db.getRepositoryDao().clear(repoId)
clearedRepoData = true
}
}
}

View File

@@ -25,8 +25,15 @@ public interface RepositoryDao {
/**
* Use when replacing an existing repo with a full index.
* This removes all existing index data associated with this repo from the database.
* @throws IllegalStateException if no repo with the given [repoId] exists.
*/
public fun replace(repoId: Long, repository: RepoV2, version: Int, certificate: String?)
public fun clear(repoId: Long)
/**
* Updates an existing repo with new data from a full index update.
* Call [clear] first to ensure old data was removed.
*/
public fun update(repoId: Long, repository: RepoV2, version: Int, certificate: String?)
public fun getRepository(repoId: Long): Repository?
public fun insertEmptyRepo(
@@ -50,7 +57,10 @@ public interface RepositoryDao {
internal interface RepositoryDaoInt : RepositoryDao {
@Insert(onConflict = REPLACE)
fun insert(repository: CoreRepository): Long
fun insertOrReplace(repository: CoreRepository): Long
@Update
fun update(repository: CoreRepository)
@Insert(onConflict = REPLACE)
fun insertMirrors(mirrors: List<Mirror>)
@@ -78,7 +88,7 @@ internal interface RepositoryDaoInt : RepositoryDao {
description = mapOf("en-US" to initialRepo.description),
certificate = initialRepo.certificate,
)
val repoId = insert(repo)
val repoId = insertOrReplace(repo)
val repositoryPreferences = RepositoryPreferences(
repoId = repoId,
weight = initialRepo.weight,
@@ -102,7 +112,7 @@ internal interface RepositoryDaoInt : RepositoryDao {
version = null,
certificate = null,
)
val repoId = insert(repo)
val repoId = insertOrReplace(repo)
val currentMaxWeight = getMaxRepositoryWeight()
val repositoryPreferences = RepositoryPreferences(
repoId = repoId,
@@ -117,8 +127,8 @@ internal interface RepositoryDaoInt : RepositoryDao {
@Transaction
@VisibleForTesting
fun insert(repository: RepoV2): Long {
val repoId = insert(repository.toCoreRepository(version = 0))
fun insertOrReplace(repository: RepoV2): Long {
val repoId = insertOrReplace(repository.toCoreRepository(version = 0))
insertRepositoryPreferences(repoId)
insertRepoTables(repoId, repository)
return repoId
@@ -131,9 +141,15 @@ internal interface RepositoryDaoInt : RepositoryDao {
}
@Transaction
override fun replace(repoId: Long, repository: RepoV2, version: Int, certificate: String?) {
val newRepoId = insert(repository.toCoreRepository(repoId, version, certificate))
require(newRepoId == repoId) { "New repoId $newRepoId did not match old $repoId" }
override fun clear(repoId: Long) {
val repo = getRepository(repoId) ?: error("repo with id $repoId does not exist")
// this clears all foreign key associated data since the repo gets replaced
insertOrReplace(repo.repository)
}
@Transaction
override fun update(repoId: Long, repository: RepoV2, version: Int, certificate: String?) {
update(repository.toCoreRepository(repoId, version, certificate))
insertRepoTables(repoId, repository)
}

View File

@@ -65,9 +65,9 @@ public class IndexV1Updater(
db.runInTransaction {
val cert = verifier.getStreamAndVerify { inputStream ->
updateListener?.onStartProcessing() // TODO maybe do more fine-grained reporting
val streamReceiver = DbV1StreamReceiver(db, compatibilityChecker)
val streamReceiver = DbV1StreamReceiver(db, compatibilityChecker, repoId)
val streamProcessor = IndexV1StreamProcessor(streamReceiver, certificate)
streamProcessor.process(repoId, inputStream)
streamProcessor.process(inputStream)
}
// update certificate, if we didn't have any before
if (certificate == null) {