Fix/invalid server settings gql mutation request (#1092)

* Validate setting values on mutation

* Handle invalid negative setting values

* Ensure at least one source is downloading at all times

* Prevent possible IllegalArgumentException

The "serverConfig.maxSourcesInParallel" value could have changed after the if-condition
This commit is contained in:
schroda
2024-11-15 00:08:07 +01:00
committed by GitHub
parent fa4607e232
commit aa1e98544b
6 changed files with 72 additions and 21 deletions

View File

@@ -60,7 +60,7 @@ private fun createRollingFileAppender(
context = logContext context = logContext
setParent(appender) setParent(appender)
fileNamePattern = "$logDirPath/${logFilename}_%d{yyyy-MM-dd}_%i.log.gz" fileNamePattern = "$logDirPath/${logFilename}_%d{yyyy-MM-dd}_%i.log.gz"
maxHistory = maxFiles maxHistory = maxFiles.coerceAtLeast(0)
setMaxFileSize(fileSizeValueOfOrDefault(maxFileSize, "10mb")) setMaxFileSize(fileSizeValueOfOrDefault(maxFileSize, "10mb"))
setTotalSizeCap(fileSizeValueOfOrDefault(maxTotalSize, "100mb")) setTotalSizeCap(fileSizeValueOfOrDefault(maxTotalSize, "100mb"))
start() start()

View File

@@ -4,29 +4,47 @@ import kotlinx.coroutines.flow.MutableStateFlow
import suwayomi.tachidesk.graphql.types.PartialSettingsType import suwayomi.tachidesk.graphql.types.PartialSettingsType
import suwayomi.tachidesk.graphql.types.Settings import suwayomi.tachidesk.graphql.types.Settings
import suwayomi.tachidesk.graphql.types.SettingsType import suwayomi.tachidesk.graphql.types.SettingsType
import suwayomi.tachidesk.manga.impl.extension.ExtensionsList.repoMatchRegex
import suwayomi.tachidesk.server.SERVER_CONFIG_MODULE_NAME import suwayomi.tachidesk.server.SERVER_CONFIG_MODULE_NAME
import suwayomi.tachidesk.server.ServerConfig import suwayomi.tachidesk.server.ServerConfig
import suwayomi.tachidesk.server.serverConfig import suwayomi.tachidesk.server.serverConfig
import xyz.nulldev.ts.config.GlobalConfigManager import xyz.nulldev.ts.config.GlobalConfigManager
import java.io.File
private fun validateString( private fun validateValue(
value: String?,
pattern: Regex,
name: String,
) {
validateString(value, pattern, Exception("Invalid format for \"$name\" [$value]"))
}
private fun validateString(
value: String?,
pattern: Regex,
exception: Exception, exception: Exception,
validate: () -> Boolean,
) { ) {
if (value != null && !value.matches(pattern)) { if (!validate()) {
throw exception throw exception
} }
} }
private fun <T> validateValue(
value: T?,
exception: Exception,
validate: (value: T) -> Boolean,
) {
if (value != null) {
validateValue(exception) { validate(value) }
}
}
private fun <T> validateValue(
value: T?,
name: String,
validate: (value: T) -> Boolean,
) {
validateValue(value, Exception("Invalid value for \"$name\" [$value]"), validate)
}
private fun validateFilePath(
value: String?,
name: String,
) {
validateValue(value, name) { File(it).exists() }
}
class SettingsMutation { class SettingsMutation {
data class SetSettingsInput( data class SetSettingsInput(
val clientMutationId: String? = null, val clientMutationId: String? = null,
@@ -39,10 +57,43 @@ class SettingsMutation {
) )
private fun validateSettings(settings: Settings) { private fun validateSettings(settings: Settings) {
validateValue(settings.ip, "ip") { it.matches("^((25[0-5]|(2[0-4]|1\\d|[1-9]|)\\d)\\.?\\b){4}$".toRegex()) }
// proxy
validateValue(settings.socksProxyVersion, "socksProxyVersion") { it == 4 || it == 5 }
// webUI
validateFilePath(settings.electronPath, "electronPath")
validateValue(settings.webUIUpdateCheckInterval, "webUIUpdateCheckInterval") { it == 0.0 || it in 1.0..23.0 }
// downloader
validateFilePath(settings.downloadsPath, "downloadsPath")
validateValue(settings.autoDownloadNewChaptersLimit, "autoDownloadNewChaptersLimit") { it >= 0 }
// extensions
validateValue(settings.extensionRepos, "extensionRepos") { it.all { repoUrl -> repoUrl.matches(repoMatchRegex) } }
// requests
validateValue(settings.maxSourcesInParallel, "maxSourcesInParallel") { it in 1..20 }
// updater
validateValue(settings.globalUpdateInterval, "globalUpdateInterval") { it == 0.0 || it >= 6 }
// misc // misc
val logbackSizePattern = "^[0-9]+(|kb|KB|mb|MB|gb|GB)\$".toRegex() validateValue(settings.maxLogFiles, "maxLogFiles") { it >= 0 }
validateString(settings.maxLogFileSize, logbackSizePattern, "maxLogFileSize")
validateString(settings.maxLogFolderSize, logbackSizePattern, "maxLogFolderSize") val logbackSizePattern = "^[0-9]+(|kb|KB|mb|MB|gb|GB)$".toRegex()
validateValue(settings.maxLogFileSize, "maxLogFolderSize") { it.matches(logbackSizePattern) }
validateValue(settings.maxLogFolderSize, "maxLogFolderSize") { it.matches(logbackSizePattern) }
// backup
validateFilePath(settings.backupPath, "backupPath")
validateValue(settings.backupTime, "backupTime") { it.matches("^([0-1]?[0-9]|2[0-3]):[0-5][0-9]$".toRegex()) }
validateValue(settings.backupInterval, "backupInterval") { it == 0 || it >= 1 }
validateValue(settings.backupTTL, "backupTTL") { it == 0 || it >= 1 }
// local source
validateFilePath(settings.localSourcePath, "localSourcePath")
} }
private fun <SettingType : Any> updateSetting( private fun <SettingType : Any> updateSetting(

View File

@@ -400,7 +400,7 @@ object Chapter {
if (serverConfig.autoDownloadNewChaptersLimit.value == 0) { if (serverConfig.autoDownloadNewChaptersLimit.value == 0) {
chaptersToConsiderForDownloadLimit.size chaptersToConsiderForDownloadLimit.size
} else { } else {
serverConfig.autoDownloadNewChaptersLimit.value.coerceAtMost(chaptersToConsiderForDownloadLimit.size) serverConfig.autoDownloadNewChaptersLimit.value.coerceIn(0, chaptersToConsiderForDownloadLimit.size)
} }
val limitedChaptersToDownload = chaptersToConsiderForDownloadLimit.subList(0, latestChapterToDownloadIndex) val limitedChaptersToDownload = chaptersToConsiderForDownloadLimit.subList(0, latestChapterToDownloadIndex)
val limitedChaptersToDownloadWithDuplicates = val limitedChaptersToDownloadWithDuplicates =

View File

@@ -241,14 +241,14 @@ object DownloadManager {
"Failed: ${downloadQueue.size - availableDownloads.size}" "Failed: ${downloadQueue.size - availableDownloads.size}"
} }
if (runningDownloaders.size < serverConfig.maxSourcesInParallel.value) { if (runningDownloaders.size < serverConfig.maxSourcesInParallel.value.coerceAtLeast(1)) {
availableDownloads availableDownloads
.asSequence() .asSequence()
.map { it.manga.sourceId } .map { it.manga.sourceId }
.distinct() .distinct()
.minus( .minus(
runningDownloaders.map { it.sourceId }.toSet(), runningDownloaders.map { it.sourceId }.toSet(),
).take(serverConfig.maxSourcesInParallel.value - runningDownloaders.size) ).take((serverConfig.maxSourcesInParallel.value - runningDownloaders.size).coerceAtLeast(0))
.map { getDownloader(it) } .map { getDownloader(it) }
.forEach { .forEach {
it.start() it.start()

View File

@@ -224,7 +224,7 @@ object ExtensionsList {
this this
} }
private val repoMatchRegex = val repoMatchRegex =
( (
"https:\\/\\/(?>www\\.|raw\\.)?(github|githubusercontent)\\.com" + "https:\\/\\/(?>www\\.|raw\\.)?(github|githubusercontent)\\.com" +
"\\/([^\\/]+)\\/([^\\/]+)(?>(?>\\/tree|\\/blob)?\\/([^\\/\\n]*))?(?>\\/([^\\/\\n]*\\.json)?)?" "\\/([^\\/]+)\\/([^\\/]+)(?>(?>\\/tree|\\/blob)?\\/([^\\/\\n]*))?(?>\\/([^\\/\\n]*\\.json)?)?"

View File

@@ -228,7 +228,7 @@ object WebInterfaceManager {
private fun getServedWebUIFlavor(): WebUIFlavor = private fun getServedWebUIFlavor(): WebUIFlavor =
WebUIFlavor.from(preferences.getString(SERVED_WEBUI_FLAVOR_KEY, WebUIFlavor.default.uiName)!!) WebUIFlavor.from(preferences.getString(SERVED_WEBUI_FLAVOR_KEY, WebUIFlavor.default.uiName)!!)
private fun isAutoUpdateEnabled(): Boolean = serverConfig.webUIUpdateCheckInterval.value.toInt() != 0 private fun isAutoUpdateEnabled(): Boolean = serverConfig.webUIUpdateCheckInterval.value.toInt() > 0
@OptIn(DelicateCoroutinesApi::class) @OptIn(DelicateCoroutinesApi::class)
private fun scheduleWebUIUpdateCheck() { private fun scheduleWebUIUpdateCheck() {