mirror of
https://github.com/Suwayomi/Suwayomi-Server.git
synced 2026-06-30 17:34:39 -05: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/. */
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.source.local.LocalSource
|
import eu.kanade.tachiyomi.source.local.LocalSource
|
||||||
|
import kotlinx.coroutines.sync.Mutex
|
||||||
|
import kotlinx.coroutines.sync.withLock
|
||||||
import mu.KotlinLogging
|
import mu.KotlinLogging
|
||||||
import org.jetbrains.exposed.dao.id.EntityID
|
import org.jetbrains.exposed.dao.id.EntityID
|
||||||
import org.jetbrains.exposed.sql.ResultRow
|
import org.jetbrains.exposed.sql.ResultRow
|
||||||
@@ -86,117 +88,121 @@ object ExtensionsList {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateExtensionDatabase(foundExtensions: List<OnlineExtension>) {
|
private val updateExtensionDatabaseMutex = Mutex()
|
||||||
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
|
|
||||||
|
|
||||||
// add these because batch updates need matching columns
|
private suspend fun updateExtensionDatabase(foundExtensions: List<OnlineExtension>) {
|
||||||
this[ExtensionTable.hasUpdate] = extensionRecord[ExtensionTable.hasUpdate]
|
updateExtensionDatabaseMutex.withLock {
|
||||||
this[ExtensionTable.isObsolete] = extensionRecord[ExtensionTable.isObsolete]
|
transaction {
|
||||||
|
val uniqueExtensions =
|
||||||
// a previously removed extension is now available again
|
foundExtensions.groupBy { it.pkgName }.mapValues {
|
||||||
if (extensionRecord[ExtensionTable.isObsolete] &&
|
(_, extension) ->
|
||||||
foundExtension.versionCode >= extensionRecord[ExtensionTable.versionCode]
|
extension.maxBy { it.versionCode }
|
||||||
) {
|
}.values
|
||||||
this[ExtensionTable.isObsolete] = false
|
val installedExtensions =
|
||||||
}
|
ExtensionTable.selectAll().toList()
|
||||||
|
.associateBy { it[ExtensionTable.pkgName] }
|
||||||
when {
|
val extensionsToUpdate = mutableListOf<Pair<OnlineExtension, ResultRow>>()
|
||||||
foundExtension.versionCode > extensionRecord[ExtensionTable.versionCode] -> {
|
val extensionsToInsert = mutableListOf<OnlineExtension>()
|
||||||
// there is an update
|
val extensionsToDelete =
|
||||||
this[ExtensionTable.hasUpdate] = true
|
installedExtensions.filter { it.value[ExtensionTable.repo] != null }.mapNotNull { (pkgName, extension) ->
|
||||||
updateMap.putIfAbsent(foundExtension.pkgName, foundExtension)
|
extension.takeUnless { uniqueExtensions.any { it.pkgName == pkgName } }
|
||||||
}
|
}
|
||||||
foundExtension.versionCode < extensionRecord[ExtensionTable.versionCode] -> {
|
uniqueExtensions.forEach {
|
||||||
// somehow the user installed an invalid version
|
val extension = installedExtensions[it.pkgName]
|
||||||
this[ExtensionTable.isObsolete] = true
|
if (extension != null) {
|
||||||
}
|
extensionsToUpdate.add(it to extension)
|
||||||
}
|
} else {
|
||||||
}
|
extensionsToInsert.add(it)
|
||||||
execute(this@transaction)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val extensionsToFullyUpdate = extensionsInstalled[false].orEmpty()
|
if (extensionsToUpdate.isNotEmpty()) {
|
||||||
if (extensionsToFullyUpdate.isNotEmpty()) {
|
val extensionsInstalled =
|
||||||
BatchUpdateStatement(ExtensionTable).apply {
|
extensionsToUpdate
|
||||||
extensionsToFullyUpdate.forEach { (foundExtension, extensionRecord) ->
|
.groupBy { it.second[ExtensionTable.isInstalled] }
|
||||||
addBatch(EntityID(extensionRecord[ExtensionTable.id].value, ExtensionTable))
|
val installedExtensionsToUpdate = extensionsInstalled[true].orEmpty()
|
||||||
// extension is not installed, so we can overwrite the data without a care
|
if (installedExtensionsToUpdate.isNotEmpty()) {
|
||||||
this[ExtensionTable.repo] = foundExtension.repo
|
BatchUpdateStatement(ExtensionTable).apply {
|
||||||
this[ExtensionTable.name] = foundExtension.name
|
installedExtensionsToUpdate.forEach { (foundExtension, extensionRecord) ->
|
||||||
this[ExtensionTable.versionName] = foundExtension.versionName
|
addBatch(EntityID(extensionRecord[ExtensionTable.id].value, ExtensionTable))
|
||||||
this[ExtensionTable.versionCode] = foundExtension.versionCode
|
// Always update icon url and repo
|
||||||
this[ExtensionTable.lang] = foundExtension.lang
|
this[ExtensionTable.iconUrl] = foundExtension.iconUrl
|
||||||
this[ExtensionTable.isNsfw] = foundExtension.isNsfw
|
this[ExtensionTable.repo] = foundExtension.repo
|
||||||
this[ExtensionTable.apkName] = foundExtension.apkName
|
|
||||||
this[ExtensionTable.iconUrl] = foundExtension.iconUrl
|
// 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()) {
|
||||||
if (extensionsToInsert.isNotEmpty()) {
|
ExtensionTable.batchInsert(extensionsToInsert) { foundExtension ->
|
||||||
ExtensionTable.batchInsert(extensionsToInsert) { foundExtension ->
|
this[ExtensionTable.repo] = foundExtension.repo
|
||||||
this[ExtensionTable.repo] = foundExtension.repo
|
this[ExtensionTable.name] = foundExtension.name
|
||||||
this[ExtensionTable.name] = foundExtension.name
|
this[ExtensionTable.pkgName] = foundExtension.pkgName
|
||||||
this[ExtensionTable.pkgName] = foundExtension.pkgName
|
this[ExtensionTable.versionName] = foundExtension.versionName
|
||||||
this[ExtensionTable.versionName] = foundExtension.versionName
|
this[ExtensionTable.versionCode] = foundExtension.versionCode
|
||||||
this[ExtensionTable.versionCode] = foundExtension.versionCode
|
this[ExtensionTable.lang] = foundExtension.lang
|
||||||
this[ExtensionTable.lang] = foundExtension.lang
|
this[ExtensionTable.isNsfw] = foundExtension.isNsfw
|
||||||
this[ExtensionTable.isNsfw] = foundExtension.isNsfw
|
this[ExtensionTable.apkName] = foundExtension.apkName
|
||||||
this[ExtensionTable.apkName] = foundExtension.apkName
|
this[ExtensionTable.iconUrl] = foundExtension.iconUrl
|
||||||
this[ExtensionTable.iconUrl] = foundExtension.iconUrl
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// deal with obsolete extensions
|
// deal with obsolete extensions
|
||||||
val extensionsToRemove =
|
val extensionsToRemove =
|
||||||
extensionsToDelete.groupBy { it[ExtensionTable.isInstalled] }
|
extensionsToDelete.groupBy { it[ExtensionTable.isInstalled] }
|
||||||
.mapValues { (_, extensions) -> extensions.map { it[ExtensionTable.pkgName] } }
|
.mapValues { (_, extensions) -> extensions.map { it[ExtensionTable.pkgName] } }
|
||||||
// not in the repo, so these extensions are obsolete
|
// not in the repo, so these extensions are obsolete
|
||||||
val obsoleteExtensions = extensionsToRemove[true].orEmpty()
|
val obsoleteExtensions = extensionsToRemove[true].orEmpty()
|
||||||
if (obsoleteExtensions.isNotEmpty()) {
|
if (obsoleteExtensions.isNotEmpty()) {
|
||||||
ExtensionTable.update({ ExtensionTable.pkgName inList obsoleteExtensions }) {
|
ExtensionTable.update({ ExtensionTable.pkgName inList obsoleteExtensions }) {
|
||||||
it[isObsolete] = true
|
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