mirror of
https://github.com/Suwayomi/Tachidesk.git
synced 2025-12-22 12:32:34 +01:00
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:
@@ -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()
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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 =
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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)?)?"
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
Reference in New Issue
Block a user