feat(api): configure server port and context path

Closes: #1264
This commit is contained in:
Gauthier Roebroeck 2023-10-30 11:34:36 +08:00
parent 6059b85e4f
commit 3f390371f7
8 changed files with 188 additions and 11 deletions

View file

@ -65,6 +65,26 @@ class KomgaSettingsProvider(
field = value
eventPublisher.publishEvent(TaskPoolSizeChangedEvent())
}
var serverPort: Int? =
serverSettingsDao.getSettingByKey(Settings.SERVER_PORT.name, Int::class.java)
set(value) {
if (value != null)
serverSettingsDao.saveSetting(Settings.SERVER_PORT.name, value)
else
serverSettingsDao.deleteSetting(Settings.SERVER_PORT.name)
field = value
}
var serverContextPath: String? =
serverSettingsDao.getSettingByKey(Settings.SERVER_CONTEXT_PATH.name, String::class.java)
set(value) {
if (value != null)
serverSettingsDao.saveSetting(Settings.SERVER_CONTEXT_PATH.name, value)
else
serverSettingsDao.deleteSetting(Settings.SERVER_CONTEXT_PATH.name)
field = value
}
}
private enum class Settings {
@ -74,4 +94,6 @@ private enum class Settings {
REMEMBER_ME_DURATION,
THUMBNAIL_SIZE,
TASK_POOL_SIZE,
SERVER_PORT,
SERVER_CONTEXT_PATH,
}

View file

@ -33,6 +33,10 @@ class ServerSettingsDao(
saveSetting(key, value.toString())
}
fun deleteSetting(key: String) {
dsl.deleteFrom(s).where(s.KEY.eq(key)).execute()
}
fun deleteAll() {
dsl.deleteFrom(s).execute()
}

View file

@ -0,0 +1,25 @@
package org.gotson.komga.infrastructure.web
import mu.KotlinLogging
import org.gotson.komga.infrastructure.configuration.KomgaSettingsProvider
import org.springframework.boot.web.server.WebServerFactoryCustomizer
import org.springframework.boot.web.servlet.server.ConfigurableServletWebServerFactory
import org.springframework.stereotype.Component
private val logger = KotlinLogging.logger {}
@Component
class WebServerConfiguration(
private val settingsProvider: KomgaSettingsProvider,
) : WebServerFactoryCustomizer<ConfigurableServletWebServerFactory> {
override fun customize(factory: ConfigurableServletWebServerFactory) {
settingsProvider.serverPort?.let {
if (it > 1) factory.setPort(it)
else logger.warn { "Ignoring invalid server port: $it" }
}
settingsProvider.serverContextPath?.let {
if (it.startsWith("/") && !it.endsWith("/")) factory.setContextPath(it)
else logger.warn { "Ignoring invalid server context path: $it" }
}
}
}

View file

@ -1,12 +1,16 @@
package org.gotson.komga.interfaces.api.rest
import jakarta.servlet.ServletContext
import jakarta.validation.Valid
import org.gotson.komga.domain.model.ROLE_ADMIN
import org.gotson.komga.infrastructure.configuration.KomgaSettingsProvider
import org.gotson.komga.interfaces.api.rest.dto.SettingMultiSource
import org.gotson.komga.interfaces.api.rest.dto.SettingsDto
import org.gotson.komga.interfaces.api.rest.dto.SettingsUpdateDto
import org.gotson.komga.interfaces.api.rest.dto.toDomain
import org.gotson.komga.interfaces.api.rest.dto.toDto
import org.springframework.beans.factory.annotation.Value
import org.springframework.boot.autoconfigure.web.ServerProperties
import org.springframework.http.HttpStatus
import org.springframework.http.MediaType
import org.springframework.security.access.prepost.PreAuthorize
@ -23,8 +27,15 @@ import kotlin.time.Duration.Companion.days
@PreAuthorize("hasRole('$ROLE_ADMIN')")
class SettingsController(
private val komgaSettingsProvider: KomgaSettingsProvider,
@Value("\${server.port:#{null}}") private val configServerPort: Int?,
@Value("\${server.servlet.context-path:#{null}}") private val configServerContextPath: String?,
serverProperties: ServerProperties,
servletContext: ServletContext,
) {
private val effectiveServerPort = serverProperties.port
private val effectiveServerContextPath = servletContext.contextPath
@GetMapping
fun getSettings(): SettingsDto =
SettingsDto(
@ -33,6 +44,8 @@ class SettingsController(
komgaSettingsProvider.rememberMeDuration.inWholeDays,
komgaSettingsProvider.thumbnailSize.toDto(),
komgaSettingsProvider.taskPoolSize,
SettingMultiSource(configServerPort, komgaSettingsProvider.serverPort, effectiveServerPort),
SettingMultiSource(configServerContextPath, komgaSettingsProvider.serverContextPath, effectiveServerContextPath),
)
@PatchMapping
@ -47,5 +60,8 @@ class SettingsController(
if (newSettings.renewRememberMeKey == true) komgaSettingsProvider.renewRememberMeKey()
newSettings.thumbnailSize?.let { komgaSettingsProvider.thumbnailSize = it.toDomain() }
newSettings.taskPoolSize?.let { komgaSettingsProvider.taskPoolSize = it }
if (newSettings.isSet("serverPort")) komgaSettingsProvider.serverPort = newSettings.serverPort
if (newSettings.isSet("serverContextPath")) komgaSettingsProvider.serverContextPath = newSettings.serverContextPath
}
}

View file

@ -6,4 +6,12 @@ data class SettingsDto(
val rememberMeDurationDays: Long,
val thumbnailSize: ThumbnailSizeDto,
val taskPoolSize: Int,
val serverPort: SettingMultiSource<Int?>,
val serverContextPath: SettingMultiSource<String?>,
)
data class SettingMultiSource<T>(
val configurationSource: T,
val databaseSource: T,
val effectiveValue: T,
)

View file

@ -1,14 +1,38 @@
package org.gotson.komga.interfaces.api.rest.dto
import jakarta.validation.constraints.Max
import jakarta.validation.constraints.Pattern
import jakarta.validation.constraints.Positive
import kotlin.properties.Delegates
class SettingsUpdateDto {
private val isSet = mutableMapOf<String, Boolean>()
fun isSet(prop: String) = isSet.getOrDefault(prop, false)
var deleteEmptyCollections: Boolean? = null
var deleteEmptyReadLists: Boolean? = null
data class SettingsUpdateDto(
val deleteEmptyCollections: Boolean? = null,
val deleteEmptyReadLists: Boolean? = null,
@get:Positive
val rememberMeDurationDays: Long? = null,
val renewRememberMeKey: Boolean? = null,
val thumbnailSize: ThumbnailSizeDto? = null,
var rememberMeDurationDays: Long? = null
var renewRememberMeKey: Boolean? = null
var thumbnailSize: ThumbnailSizeDto? = null
@get:Positive
val taskPoolSize: Int? = null,
)
var taskPoolSize: Int? = null
@get:Positive
@get:Max(65535)
var serverPort: Int?
by Delegates.observable(null) { prop, _, _ ->
isSet[prop.name] = true
}
@get:Pattern(regexp = "^\\/[\\w-\\/]*[a-zA-Z0-9]\$")
var serverContextPath: String?
by Delegates.observable(null) { prop, _, _ ->
isSet[prop.name] = true
}
}

View file

@ -43,7 +43,7 @@ class ServerSettingsDaoTest(
}
@Test
fun `given existing setting when saving again then it is overriden`() {
fun `given existing setting when saving again then it is overridden`() {
serverSettingsDao.saveSetting("setting", "value")
val initial = serverSettingsDao.getSettingByKey("setting", String::class.java)
@ -53,4 +53,16 @@ class ServerSettingsDaoTest(
val updated = serverSettingsDao.getSettingByKey("setting", String::class.java)
assertThat(updated).isEqualTo("updated")
}
@Test
fun `given existing setting when deleting then it is deleted`() {
serverSettingsDao.saveSetting("setting", "value")
val initial = serverSettingsDao.getSettingByKey("setting", String::class.java)
assertThat(initial).isEqualTo("value")
serverSettingsDao.deleteSetting("setting")
val updated = serverSettingsDao.getSettingByKey("setting", String::class.java)
assertThat(updated).isNull()
}
}

View file

@ -54,6 +54,8 @@ class SettingsControllerTest(
komgaSettingsProvider.rememberMeDuration = 5.days
komgaSettingsProvider.thumbnailSize = ThumbnailSize.LARGE
komgaSettingsProvider.taskPoolSize = 4
komgaSettingsProvider.serverPort = 1234
komgaSettingsProvider.serverContextPath = "/example"
mockMvc.get("/api/v1/settings")
.andExpect {
@ -63,6 +65,12 @@ class SettingsControllerTest(
jsonPath("rememberMeDurationDays") { value(5) }
jsonPath("thumbnailSize") { value("LARGE") }
jsonPath("taskPoolSize") { value(4) }
jsonPath("serverPort.configurationSource") { value(25600) }
jsonPath("serverPort.databaseSource") { value(1234) }
jsonPath("serverPort.effectiveValue") { value(25600) }
jsonPath("serverContextPath.configurationSource") { value(null) }
jsonPath("serverContextPath.databaseSource") { value("/example") }
jsonPath("serverContextPath.effectiveValue") { value("") }
}
}
@ -74,6 +82,8 @@ class SettingsControllerTest(
komgaSettingsProvider.rememberMeDuration = 5.days
komgaSettingsProvider.thumbnailSize = ThumbnailSize.LARGE
komgaSettingsProvider.taskPoolSize = 4
komgaSettingsProvider.serverPort = 1234
komgaSettingsProvider.serverContextPath = "/example"
val rememberMeKey = komgaSettingsProvider.rememberMeKey
@ -84,7 +94,9 @@ class SettingsControllerTest(
"rememberMeDurationDays": 15,
"renewRememberMeKey": true,
"thumbnailSize": "MEDIUM",
"taskPoolSize": 8
"taskPoolSize": 8,
"serverPort": 5678,
"serverContextPath": "/komga-hyphen/subpath123"
}
""".trimIndent()
@ -102,6 +114,52 @@ class SettingsControllerTest(
assertThat(komgaSettingsProvider.rememberMeKey).isNotEqualTo(rememberMeKey)
assertThat(komgaSettingsProvider.thumbnailSize).isEqualTo(ThumbnailSize.MEDIUM)
assertThat(komgaSettingsProvider.taskPoolSize).isEqualTo(8)
assertThat(komgaSettingsProvider.serverPort).isEqualTo(5678)
assertThat(komgaSettingsProvider.serverContextPath).isEqualTo("/komga-hyphen/subpath123")
}
@Test
@WithMockCustomUser(roles = [ROLE_ADMIN])
fun `given admin user when deleting settings then deletable settings are deleted`() {
komgaSettingsProvider.deleteEmptyCollections = true
komgaSettingsProvider.deleteEmptyReadLists = true
komgaSettingsProvider.rememberMeDuration = 5.days
komgaSettingsProvider.thumbnailSize = ThumbnailSize.LARGE
komgaSettingsProvider.taskPoolSize = 4
komgaSettingsProvider.serverPort = 1234
komgaSettingsProvider.serverContextPath = "/example"
val rememberMeKey = komgaSettingsProvider.rememberMeKey
//language=JSON
val jsonString = """
{
"deleteEmptyCollections": null,
"rememberMeDurationDays": null,
"renewRememberMeKey": null,
"thumbnailSize": null,
"taskPoolSize": null,
"serverPort": null,
"serverContextPath": null
}
""".trimIndent()
mockMvc.patch("/api/v1/settings") {
contentType = MediaType.APPLICATION_JSON
content = jsonString
}
.andExpect {
status { isNoContent() }
}
assertThat(komgaSettingsProvider.deleteEmptyCollections).isTrue
assertThat(komgaSettingsProvider.deleteEmptyReadLists).isTrue
assertThat(komgaSettingsProvider.rememberMeDuration).isEqualTo(5.days)
assertThat(komgaSettingsProvider.rememberMeKey).isEqualTo(rememberMeKey)
assertThat(komgaSettingsProvider.thumbnailSize).isEqualTo(ThumbnailSize.LARGE)
assertThat(komgaSettingsProvider.taskPoolSize).isEqualTo(4)
assertThat(komgaSettingsProvider.serverPort).isNull()
assertThat(komgaSettingsProvider.serverContextPath).isNull()
}
@ParameterizedTest
@ -113,7 +171,15 @@ class SettingsControllerTest(
//language=JSON
"""{"thumbnailSize": "HUGE"}""",
"""{"taskPoolSize": 0}""",
"""{"taskPoolSize": -15}""",
"""{"serverPort": 0}""",
"""{"serverPort": -5}""",
"""{"serverPort": 65536}""",
"""{"serverContextPath": "noSlashBegin"}""",
"""{"serverContextPath": "/slashEnd/"}""",
"""{"serverContextPath": "/invalid=character"}""",
"""{"serverContextPath": "/invalid/end-"}""",
"""{"serverContextPath": "/invalid/end_"}""",
"""{"serverContextPath": "/日本語"}""",
],
)
fun `given admin user when updating with invalid settings then returns bad request`(jsonString: String) {