mirror of
https://github.com/Suwayomi/Tachidesk.git
synced 2025-12-23 21:12:37 +01:00
Add mutex to "updateExtensionDatabase" (#829)
If called in quick succession it is possible that duplicated extensions get inserted to the database, because it has not yet been updated by the first call
This commit is contained in:
@@ -8,6 +8,8 @@ package suwayomi.tachidesk.manga.impl.extension
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import eu.kanade.tachiyomi.source.local.LocalSource
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import mu.KotlinLogging
|
||||
import org.jetbrains.exposed.dao.id.EntityID
|
||||
import org.jetbrains.exposed.sql.ResultRow
|
||||
@@ -86,117 +88,121 @@ object ExtensionsList {
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateExtensionDatabase(foundExtensions: List<OnlineExtension>) {
|
||||
transaction {
|
||||
val uniqueExtensions =
|
||||
foundExtensions.groupBy { it.pkgName }.mapValues {
|
||||
(_, extension) ->
|
||||
extension.maxBy { it.versionCode }
|
||||
}.values
|
||||
val installedExtensions =
|
||||
ExtensionTable.selectAll().toList()
|
||||
.associateBy { it[ExtensionTable.pkgName] }
|
||||
val extensionsToUpdate = mutableListOf<Pair<OnlineExtension, ResultRow>>()
|
||||
val extensionsToInsert = mutableListOf<OnlineExtension>()
|
||||
val extensionsToDelete =
|
||||
installedExtensions.filter { it.value[ExtensionTable.repo] != null }.mapNotNull { (pkgName, extension) ->
|
||||
extension.takeUnless { uniqueExtensions.any { it.pkgName == pkgName } }
|
||||
}
|
||||
uniqueExtensions.forEach {
|
||||
val extension = installedExtensions[it.pkgName]
|
||||
if (extension != null) {
|
||||
extensionsToUpdate.add(it to extension)
|
||||
} else {
|
||||
extensionsToInsert.add(it)
|
||||
}
|
||||
}
|
||||
if (extensionsToUpdate.isNotEmpty()) {
|
||||
val extensionsInstalled =
|
||||
extensionsToUpdate
|
||||
.groupBy { it.second[ExtensionTable.isInstalled] }
|
||||
val installedExtensionsToUpdate = extensionsInstalled[true].orEmpty()
|
||||
if (installedExtensionsToUpdate.isNotEmpty()) {
|
||||
BatchUpdateStatement(ExtensionTable).apply {
|
||||
installedExtensionsToUpdate.forEach { (foundExtension, extensionRecord) ->
|
||||
addBatch(EntityID(extensionRecord[ExtensionTable.id].value, ExtensionTable))
|
||||
// Always update icon url and repo
|
||||
this[ExtensionTable.iconUrl] = foundExtension.iconUrl
|
||||
this[ExtensionTable.repo] = foundExtension.repo
|
||||
private val updateExtensionDatabaseMutex = Mutex()
|
||||
|
||||
// add these because batch updates need matching columns
|
||||
this[ExtensionTable.hasUpdate] = extensionRecord[ExtensionTable.hasUpdate]
|
||||
this[ExtensionTable.isObsolete] = extensionRecord[ExtensionTable.isObsolete]
|
||||
|
||||
// a previously removed extension is now available again
|
||||
if (extensionRecord[ExtensionTable.isObsolete] &&
|
||||
foundExtension.versionCode >= extensionRecord[ExtensionTable.versionCode]
|
||||
) {
|
||||
this[ExtensionTable.isObsolete] = false
|
||||
}
|
||||
|
||||
when {
|
||||
foundExtension.versionCode > extensionRecord[ExtensionTable.versionCode] -> {
|
||||
// there is an update
|
||||
this[ExtensionTable.hasUpdate] = true
|
||||
updateMap.putIfAbsent(foundExtension.pkgName, foundExtension)
|
||||
}
|
||||
foundExtension.versionCode < extensionRecord[ExtensionTable.versionCode] -> {
|
||||
// somehow the user installed an invalid version
|
||||
this[ExtensionTable.isObsolete] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
execute(this@transaction)
|
||||
private suspend fun updateExtensionDatabase(foundExtensions: List<OnlineExtension>) {
|
||||
updateExtensionDatabaseMutex.withLock {
|
||||
transaction {
|
||||
val uniqueExtensions =
|
||||
foundExtensions.groupBy { it.pkgName }.mapValues {
|
||||
(_, extension) ->
|
||||
extension.maxBy { it.versionCode }
|
||||
}.values
|
||||
val installedExtensions =
|
||||
ExtensionTable.selectAll().toList()
|
||||
.associateBy { it[ExtensionTable.pkgName] }
|
||||
val extensionsToUpdate = mutableListOf<Pair<OnlineExtension, ResultRow>>()
|
||||
val extensionsToInsert = mutableListOf<OnlineExtension>()
|
||||
val extensionsToDelete =
|
||||
installedExtensions.filter { it.value[ExtensionTable.repo] != null }.mapNotNull { (pkgName, extension) ->
|
||||
extension.takeUnless { uniqueExtensions.any { it.pkgName == pkgName } }
|
||||
}
|
||||
uniqueExtensions.forEach {
|
||||
val extension = installedExtensions[it.pkgName]
|
||||
if (extension != null) {
|
||||
extensionsToUpdate.add(it to extension)
|
||||
} else {
|
||||
extensionsToInsert.add(it)
|
||||
}
|
||||
}
|
||||
val extensionsToFullyUpdate = extensionsInstalled[false].orEmpty()
|
||||
if (extensionsToFullyUpdate.isNotEmpty()) {
|
||||
BatchUpdateStatement(ExtensionTable).apply {
|
||||
extensionsToFullyUpdate.forEach { (foundExtension, extensionRecord) ->
|
||||
addBatch(EntityID(extensionRecord[ExtensionTable.id].value, ExtensionTable))
|
||||
// extension is not installed, so we can overwrite the data without a care
|
||||
this[ExtensionTable.repo] = foundExtension.repo
|
||||
this[ExtensionTable.name] = foundExtension.name
|
||||
this[ExtensionTable.versionName] = foundExtension.versionName
|
||||
this[ExtensionTable.versionCode] = foundExtension.versionCode
|
||||
this[ExtensionTable.lang] = foundExtension.lang
|
||||
this[ExtensionTable.isNsfw] = foundExtension.isNsfw
|
||||
this[ExtensionTable.apkName] = foundExtension.apkName
|
||||
this[ExtensionTable.iconUrl] = foundExtension.iconUrl
|
||||
if (extensionsToUpdate.isNotEmpty()) {
|
||||
val extensionsInstalled =
|
||||
extensionsToUpdate
|
||||
.groupBy { it.second[ExtensionTable.isInstalled] }
|
||||
val installedExtensionsToUpdate = extensionsInstalled[true].orEmpty()
|
||||
if (installedExtensionsToUpdate.isNotEmpty()) {
|
||||
BatchUpdateStatement(ExtensionTable).apply {
|
||||
installedExtensionsToUpdate.forEach { (foundExtension, extensionRecord) ->
|
||||
addBatch(EntityID(extensionRecord[ExtensionTable.id].value, ExtensionTable))
|
||||
// Always update icon url and repo
|
||||
this[ExtensionTable.iconUrl] = foundExtension.iconUrl
|
||||
this[ExtensionTable.repo] = foundExtension.repo
|
||||
|
||||
// add these because batch updates need matching columns
|
||||
this[ExtensionTable.hasUpdate] = extensionRecord[ExtensionTable.hasUpdate]
|
||||
this[ExtensionTable.isObsolete] = extensionRecord[ExtensionTable.isObsolete]
|
||||
|
||||
// a previously removed extension is now available again
|
||||
if (extensionRecord[ExtensionTable.isObsolete] &&
|
||||
foundExtension.versionCode >= extensionRecord[ExtensionTable.versionCode]
|
||||
) {
|
||||
this[ExtensionTable.isObsolete] = false
|
||||
}
|
||||
|
||||
when {
|
||||
foundExtension.versionCode > extensionRecord[ExtensionTable.versionCode] -> {
|
||||
// there is an update
|
||||
this[ExtensionTable.hasUpdate] = true
|
||||
updateMap.putIfAbsent(foundExtension.pkgName, foundExtension)
|
||||
}
|
||||
foundExtension.versionCode < extensionRecord[ExtensionTable.versionCode] -> {
|
||||
// somehow the user installed an invalid version
|
||||
this[ExtensionTable.isObsolete] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
execute(this@transaction)
|
||||
}
|
||||
}
|
||||
val extensionsToFullyUpdate = extensionsInstalled[false].orEmpty()
|
||||
if (extensionsToFullyUpdate.isNotEmpty()) {
|
||||
BatchUpdateStatement(ExtensionTable).apply {
|
||||
extensionsToFullyUpdate.forEach { (foundExtension, extensionRecord) ->
|
||||
addBatch(EntityID(extensionRecord[ExtensionTable.id].value, ExtensionTable))
|
||||
// extension is not installed, so we can overwrite the data without a care
|
||||
this[ExtensionTable.repo] = foundExtension.repo
|
||||
this[ExtensionTable.name] = foundExtension.name
|
||||
this[ExtensionTable.versionName] = foundExtension.versionName
|
||||
this[ExtensionTable.versionCode] = foundExtension.versionCode
|
||||
this[ExtensionTable.lang] = foundExtension.lang
|
||||
this[ExtensionTable.isNsfw] = foundExtension.isNsfw
|
||||
this[ExtensionTable.apkName] = foundExtension.apkName
|
||||
this[ExtensionTable.iconUrl] = foundExtension.iconUrl
|
||||
}
|
||||
execute(this@transaction)
|
||||
}
|
||||
execute(this@transaction)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (extensionsToInsert.isNotEmpty()) {
|
||||
ExtensionTable.batchInsert(extensionsToInsert) { foundExtension ->
|
||||
this[ExtensionTable.repo] = foundExtension.repo
|
||||
this[ExtensionTable.name] = foundExtension.name
|
||||
this[ExtensionTable.pkgName] = foundExtension.pkgName
|
||||
this[ExtensionTable.versionName] = foundExtension.versionName
|
||||
this[ExtensionTable.versionCode] = foundExtension.versionCode
|
||||
this[ExtensionTable.lang] = foundExtension.lang
|
||||
this[ExtensionTable.isNsfw] = foundExtension.isNsfw
|
||||
this[ExtensionTable.apkName] = foundExtension.apkName
|
||||
this[ExtensionTable.iconUrl] = foundExtension.iconUrl
|
||||
if (extensionsToInsert.isNotEmpty()) {
|
||||
ExtensionTable.batchInsert(extensionsToInsert) { foundExtension ->
|
||||
this[ExtensionTable.repo] = foundExtension.repo
|
||||
this[ExtensionTable.name] = foundExtension.name
|
||||
this[ExtensionTable.pkgName] = foundExtension.pkgName
|
||||
this[ExtensionTable.versionName] = foundExtension.versionName
|
||||
this[ExtensionTable.versionCode] = foundExtension.versionCode
|
||||
this[ExtensionTable.lang] = foundExtension.lang
|
||||
this[ExtensionTable.isNsfw] = foundExtension.isNsfw
|
||||
this[ExtensionTable.apkName] = foundExtension.apkName
|
||||
this[ExtensionTable.iconUrl] = foundExtension.iconUrl
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// deal with obsolete extensions
|
||||
val extensionsToRemove =
|
||||
extensionsToDelete.groupBy { it[ExtensionTable.isInstalled] }
|
||||
.mapValues { (_, extensions) -> extensions.map { it[ExtensionTable.pkgName] } }
|
||||
// not in the repo, so these extensions are obsolete
|
||||
val obsoleteExtensions = extensionsToRemove[true].orEmpty()
|
||||
if (obsoleteExtensions.isNotEmpty()) {
|
||||
ExtensionTable.update({ ExtensionTable.pkgName inList obsoleteExtensions }) {
|
||||
it[isObsolete] = true
|
||||
// deal with obsolete extensions
|
||||
val extensionsToRemove =
|
||||
extensionsToDelete.groupBy { it[ExtensionTable.isInstalled] }
|
||||
.mapValues { (_, extensions) -> extensions.map { it[ExtensionTable.pkgName] } }
|
||||
// not in the repo, so these extensions are obsolete
|
||||
val obsoleteExtensions = extensionsToRemove[true].orEmpty()
|
||||
if (obsoleteExtensions.isNotEmpty()) {
|
||||
ExtensionTable.update({ ExtensionTable.pkgName inList obsoleteExtensions }) {
|
||||
it[isObsolete] = true
|
||||
}
|
||||
}
|
||||
// is not installed, so we can remove the record without a care
|
||||
val removeExtensions = extensionsToRemove[false].orEmpty()
|
||||
if (removeExtensions.isNotEmpty()) {
|
||||
ExtensionTable.deleteWhere { ExtensionTable.pkgName inList removeExtensions }
|
||||
}
|
||||
}
|
||||
// is not installed, so we can remove the record without a care
|
||||
val removeExtensions = extensionsToRemove[false].orEmpty()
|
||||
if (removeExtensions.isNotEmpty()) {
|
||||
ExtensionTable.deleteWhere { ExtensionTable.pkgName inList removeExtensions }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user