Rewrite Migrations (#577)

* Rewrite Migrations

* Fix Detekt errors

* Do migrations synchronous

* Filter and sort migrations

* Review changes

* Review changes 2

* Fix Detekt errors
This commit is contained in:
Andreas 2024-03-25 18:26:19 +01:00 committed by GitHub
parent 6965e59a64
commit 666d6aa117
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 275 additions and 84 deletions

View file

@ -1,69 +0,0 @@
package eu.kanade.tachiyomi
import android.content.Context
import eu.kanade.domain.source.service.SourcePreferences
import eu.kanade.tachiyomi.data.backup.create.BackupCreateJob
import eu.kanade.tachiyomi.data.library.LibraryUpdateJob
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import logcat.LogPriority
import mihon.domain.extensionrepo.exception.SaveExtensionRepoException
import mihon.domain.extensionrepo.repository.ExtensionRepoRepository
import tachiyomi.core.common.preference.Preference
import tachiyomi.core.common.preference.PreferenceStore
import tachiyomi.core.common.util.lang.launchIO
import tachiyomi.core.common.util.system.logcat
object Migrations {
/**
* Performs a migration when the application is updated.
*
* @return true if a migration is performed, false otherwise.
*/
@Suppress("SameReturnValue", "MagicNumber")
fun upgrade(
context: Context,
preferenceStore: PreferenceStore,
sourcePreferences: SourcePreferences,
extensionRepoRepository: ExtensionRepoRepository,
): Boolean {
val lastVersionCode = preferenceStore.getInt(Preference.appStateKey("last_version_code"), 0)
val oldVersion = lastVersionCode.get()
if (oldVersion < BuildConfig.VERSION_CODE) {
lastVersionCode.set(BuildConfig.VERSION_CODE)
// Always set up background tasks to ensure they're running
LibraryUpdateJob.setupTask(context)
BackupCreateJob.setupTask(context)
// Fresh install
if (oldVersion == 0) {
return false
}
val coroutineScope = CoroutineScope(Dispatchers.IO)
if (oldVersion < 7) {
coroutineScope.launchIO {
for ((index, source) in sourcePreferences.extensionRepos().get().withIndex()) {
try {
extensionRepoRepository.upsertRepo(
source,
"Repo #${index + 1}",
null,
source,
"NOFINGERPRINT-${index + 1}",
)
} catch (e: SaveExtensionRepoException) {
logcat(LogPriority.ERROR, e) { "Error Migrating Extension Repo with baseUrl: $source" }
}
}
sourcePreferences.extensionRepos().delete()
}
}
}
return false
}
}

View file

@ -50,8 +50,6 @@ import cafe.adriel.voyager.navigator.NavigatorDisposeBehavior
import cafe.adriel.voyager.navigator.currentOrThrow import cafe.adriel.voyager.navigator.currentOrThrow
import com.google.accompanist.systemuicontroller.rememberSystemUiController import com.google.accompanist.systemuicontroller.rememberSystemUiController
import eu.kanade.domain.base.BasePreferences import eu.kanade.domain.base.BasePreferences
import eu.kanade.domain.source.service.SourcePreferences
import eu.kanade.domain.ui.UiPreferences
import eu.kanade.presentation.components.AppStateBanners import eu.kanade.presentation.components.AppStateBanners
import eu.kanade.presentation.components.DownloadedOnlyBannerBackgroundColor import eu.kanade.presentation.components.DownloadedOnlyBannerBackgroundColor
import eu.kanade.presentation.components.IncognitoModeBannerBackgroundColor import eu.kanade.presentation.components.IncognitoModeBannerBackgroundColor
@ -61,7 +59,6 @@ import eu.kanade.presentation.more.settings.screen.data.RestoreBackupScreen
import eu.kanade.presentation.util.AssistContentScreen import eu.kanade.presentation.util.AssistContentScreen
import eu.kanade.presentation.util.DefaultNavigatorScreenTransition import eu.kanade.presentation.util.DefaultNavigatorScreenTransition
import eu.kanade.tachiyomi.BuildConfig import eu.kanade.tachiyomi.BuildConfig
import eu.kanade.tachiyomi.Migrations
import eu.kanade.tachiyomi.data.cache.ChapterCache import eu.kanade.tachiyomi.data.cache.ChapterCache
import eu.kanade.tachiyomi.data.download.DownloadCache import eu.kanade.tachiyomi.data.download.DownloadCache
import eu.kanade.tachiyomi.data.notification.NotificationReceiver import eu.kanade.tachiyomi.data.notification.NotificationReceiver
@ -89,7 +86,11 @@ import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import logcat.LogPriority import logcat.LogPriority
import mihon.core.migration.Migrator
import mihon.core.migration.migrations.migrations
import tachiyomi.core.common.Constants import tachiyomi.core.common.Constants
import tachiyomi.core.common.preference.Preference
import tachiyomi.core.common.preference.PreferenceStore
import tachiyomi.core.common.util.lang.launchIO import tachiyomi.core.common.util.lang.launchIO
import tachiyomi.core.common.util.system.logcat import tachiyomi.core.common.util.system.logcat
import tachiyomi.domain.library.service.LibraryPreferences import tachiyomi.domain.library.service.LibraryPreferences
@ -105,9 +106,7 @@ import androidx.compose.ui.graphics.Color.Companion as ComposeColor
class MainActivity : BaseActivity() { class MainActivity : BaseActivity() {
private val sourcePreferences: SourcePreferences by injectLazy()
private val libraryPreferences: LibraryPreferences by injectLazy() private val libraryPreferences: LibraryPreferences by injectLazy()
private val uiPreferences: UiPreferences by injectLazy()
private val preferences: BasePreferences by injectLazy() private val preferences: BasePreferences by injectLazy()
private val downloadCache: DownloadCache by injectLazy() private val downloadCache: DownloadCache by injectLazy()
@ -130,16 +129,7 @@ class MainActivity : BaseActivity() {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
val didMigration = if (isLaunch) { val didMigration = migrate()
Migrations.upgrade(
context = applicationContext,
preferenceStore = Injekt.get(),
sourcePreferences = Injekt.get(),
extensionRepoRepository = Injekt.get(),
)
} else {
false
}
// Do not let the launcher create a new activity http://stackoverflow.com/questions/16283079 // Do not let the launcher create a new activity http://stackoverflow.com/questions/16283079
if (!isTaskRoot) { if (!isTaskRoot) {
@ -350,6 +340,21 @@ class MainActivity : BaseActivity() {
} }
} }
private fun migrate(): Boolean {
val preferenceStore = Injekt.get<PreferenceStore>()
val preference = preferenceStore.getInt(Preference.appStateKey("last_version_code"), 0)
logcat { "Migration from ${preference.get()} to ${BuildConfig.VERSION_CODE}" }
return Migrator.migrate(
old = preference.get(),
new = BuildConfig.VERSION_CODE,
migrations = migrations,
onMigrationComplete = {
logcat { "Updating last version to ${BuildConfig.VERSION_CODE}" }
preference.set(BuildConfig.VERSION_CODE)
},
)
}
/** /**
* Sets custom splash screen exit animation on devices prior to Android 12. * Sets custom splash screen exit animation on devices prior to Android 12.
* *

View file

@ -0,0 +1,19 @@
package mihon.core.migration
interface Migration {
val version: Float
suspend operator fun invoke(migrationContext: MigrationContext): Boolean
companion object {
const val ALWAYS = -1f
fun of(version: Float, action: suspend (MigrationContext) -> Boolean): Migration = object : Migration {
override val version: Float = version
override suspend operator fun invoke(migrationContext: MigrationContext): Boolean {
return action(migrationContext)
}
}
}
}

View file

@ -0,0 +1,10 @@
package mihon.core.migration
import uy.kohesive.injekt.Injekt
class MigrationContext {
inline fun <reified T> get(): T? {
return Injekt.getInstanceOrNull(T::class.java)
}
}

View file

@ -0,0 +1,53 @@
package mihon.core.migration
import kotlinx.coroutines.runBlocking
import tachiyomi.core.common.util.system.logcat
object Migrator {
@SuppressWarnings("ReturnCount")
fun migrate(
old: Int,
new: Int,
migrations: List<Migration>,
dryrun: Boolean = false,
onMigrationComplete: () -> Unit
): Boolean {
val migrationContext = MigrationContext()
if (old == 0) {
return migrationContext.migrate(
migrations = migrations.filter { it.isAlways() },
dryrun = dryrun
)
.also { onMigrationComplete() }
}
if (old >= new) {
return false
}
return migrationContext.migrate(
migrations = migrations.filter { it.isAlways() || it.version.toInt() in (old + 1)..new },
dryrun = dryrun
)
.also { onMigrationComplete() }
}
private fun Migration.isAlways() = version == Migration.ALWAYS
@SuppressWarnings("MaxLineLength")
private fun MigrationContext.migrate(migrations: List<Migration>, dryrun: Boolean): Boolean {
return migrations.sortedBy { it.version }
.map { migration ->
if (!dryrun) {
logcat { "Running migration: { name = ${migration::class.simpleName}, version = ${migration.version} }" }
runBlocking { migration(this@migrate) }
} else {
logcat { "(Dry-run) Running migration: { name = ${migration::class.simpleName}, version = ${migration.version} }" }
true
}
}
.reduce { acc, b -> acc || b }
}
}

View file

@ -0,0 +1,10 @@
package mihon.core.migration.migrations
import mihon.core.migration.Migration
val migrations: List<Migration>
get() = listOf(
SetupBackupCreateMigration(),
SetupLibraryUpdateMigration(),
TrustExtensionRepositoryMigration(),
)

View file

@ -0,0 +1,16 @@
package mihon.core.migration.migrations
import eu.kanade.tachiyomi.App
import eu.kanade.tachiyomi.data.backup.create.BackupCreateJob
import mihon.core.migration.Migration
import mihon.core.migration.MigrationContext
class SetupBackupCreateMigration : Migration {
override val version: Float = Migration.ALWAYS
override suspend fun invoke(migrationContext: MigrationContext): Boolean {
val context = migrationContext.get<App>() ?: return false
BackupCreateJob.setupTask(context)
return true
}
}

View file

@ -0,0 +1,16 @@
package mihon.core.migration.migrations
import eu.kanade.tachiyomi.App
import eu.kanade.tachiyomi.data.library.LibraryUpdateJob
import mihon.core.migration.Migration
import mihon.core.migration.MigrationContext
class SetupLibraryUpdateMigration : Migration {
override val version: Float = Migration.ALWAYS
override suspend fun invoke(migrationContext: MigrationContext): Boolean {
val context = migrationContext.get<App>() ?: return false
LibraryUpdateJob.setupTask(context)
return true
}
}

View file

@ -0,0 +1,35 @@
package mihon.core.migration.migrations
import eu.kanade.domain.source.service.SourcePreferences
import logcat.LogPriority
import mihon.core.migration.Migration
import mihon.core.migration.MigrationContext
import mihon.domain.extensionrepo.exception.SaveExtensionRepoException
import mihon.domain.extensionrepo.repository.ExtensionRepoRepository
import tachiyomi.core.common.util.lang.withIOContext
import tachiyomi.core.common.util.system.logcat
class TrustExtensionRepositoryMigration : Migration {
override val version: Float = 7f
override suspend fun invoke(migrationContext: MigrationContext): Boolean = withIOContext {
val sourcePreferences = migrationContext.get<SourcePreferences>() ?: return@withIOContext false
val extensionRepositoryRepository =
migrationContext.get<ExtensionRepoRepository>() ?: return@withIOContext false
for ((index, source) in sourcePreferences.extensionRepos().get().withIndex()) {
try {
extensionRepositoryRepository.upsertRepo(
source,
"Repo #${index + 1}",
null,
source,
"NOFINGERPRINT-${index + 1}",
)
} catch (e: SaveExtensionRepoException) {
logcat(LogPriority.ERROR, e) { "Error Migrating Extension Repo with baseUrl: $source" }
}
}
sourcePreferences.extensionRepos().delete()
return@withIOContext true
}
}

View file

@ -0,0 +1,96 @@
package mihon.core.migration
import io.mockk.Called
import io.mockk.spyk
import io.mockk.verify
import org.junit.jupiter.api.Assertions
import org.junit.jupiter.api.Test
class MigratorTest {
@Test
fun initialVersion() {
val onMigrationComplete: () -> Unit = {}
val onMigrationCompleteSpy = spyk(onMigrationComplete)
val didMigration = Migrator.migrate(
old = 0,
new = 1,
migrations = listOf(Migration.of(Migration.ALWAYS) { true }, Migration.of(2f) { false }),
onMigrationComplete = onMigrationCompleteSpy
)
verify { onMigrationCompleteSpy() }
Assertions.assertTrue(didMigration)
}
@Test
fun sameVersion() {
val onMigrationComplete: () -> Unit = {}
val onMigrationCompleteSpy = spyk(onMigrationComplete)
val didMigration = Migrator.migrate(
old = 1,
new = 1,
migrations = listOf(Migration.of(Migration.ALWAYS) { true }, Migration.of(2f) { true }),
onMigrationComplete = onMigrationCompleteSpy
)
verify { onMigrationCompleteSpy wasNot Called }
Assertions.assertFalse(didMigration)
}
@Test
fun smallMigration() {
val onMigrationComplete: () -> Unit = {}
val onMigrationCompleteSpy = spyk(onMigrationComplete)
val didMigration = Migrator.migrate(
old = 1,
new = 2,
migrations = listOf(Migration.of(Migration.ALWAYS) { true }, Migration.of(2f) { true }),
onMigrationComplete = onMigrationCompleteSpy
)
verify { onMigrationCompleteSpy() }
Assertions.assertTrue(didMigration)
}
@Test
fun largeMigration() {
val onMigrationComplete: () -> Unit = {}
val onMigrationCompleteSpy = spyk(onMigrationComplete)
val input = listOf(
Migration.of(Migration.ALWAYS) { true },
Migration.of(2f) { true },
Migration.of(3f) { true },
Migration.of(4f) { true },
Migration.of(5f) { true },
Migration.of(6f) { true },
Migration.of(7f) { true },
Migration.of(8f) { true },
Migration.of(9f) { true },
Migration.of(10f) { true },
)
val didMigration = Migrator.migrate(
old = 1,
new = 10,
migrations = input,
onMigrationComplete = onMigrationCompleteSpy
)
verify { onMigrationCompleteSpy() }
Assertions.assertTrue(didMigration)
}
@Test
fun withinRangeMigration() {
val onMigrationComplete: () -> Unit = {}
val onMigrationCompleteSpy = spyk(onMigrationComplete)
val didMigration = Migrator.migrate(
old = 1,
new = 2,
migrations = listOf(
Migration.of(Migration.ALWAYS) { true },
Migration.of(2f) { true },
Migration.of(3f) { false }
),
onMigrationComplete = onMigrationCompleteSpy
)
verify { onMigrationCompleteSpy() }
Assertions.assertTrue(didMigration)
}
}