Update dependency io.javalin:javalin to v6 (#1152)

* Update dependency io.javalin:javalin to v6

* Simple compile fixes

* Simple compile fixes pass 2

* Add results to futures

* Setup jetty server and api routes

* Setup Cors

* Setup basic auth

* Documentation stubs

* Replace chapter mutex cache

* Fix compile

* Disable Jetty Logging

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Syer10 <syer10@users.noreply.github.com>
This commit is contained in:
renovate[bot]
2024-11-17 15:00:53 -05:00
committed by GitHub
parent ba1c2845b6
commit 9cd8cb3d54
31 changed files with 478 additions and 363 deletions

View File

@@ -3,7 +3,7 @@ kotlin = "2.0.21"
coroutines = "1.9.0" coroutines = "1.9.0"
serialization = "1.7.3" serialization = "1.7.3"
okhttp = "5.0.0-alpha.14" # Major version is locked by Tachiyomi extensions okhttp = "5.0.0-alpha.14" # Major version is locked by Tachiyomi extensions
javalin = "4.6.8" # Javalin 5.0.0+ requires Java 11 javalin = "6.3.0" # Javalin 5.0.0+ requires Java 11
jackson = "2.13.3" # jackson version locked by javalin, ref: `io.javalin.core.util.OptionalDependency` jackson = "2.13.3" # jackson version locked by javalin, ref: `io.javalin.core.util.OptionalDependency`
exposed = "0.40.1" exposed = "0.40.1"
dex2jar = "v64" # Stuck until https://github.com/ThexXTURBOXx/dex2jar/issues/27 is fixed dex2jar = "v64" # Stuck until https://github.com/ThexXTURBOXx/dex2jar/issues/27 is fixed
@@ -104,6 +104,7 @@ xmlpull = "xmlpull:xmlpull:1.1.3.4a"
# Disk & File # Disk & File
appdirs = "net.harawata:appdirs:1.2.2" appdirs = "net.harawata:appdirs:1.2.2"
cache4k = "io.github.reactivecircus.cache4k:cache4k:0.13.0"
zip4j = "net.lingala.zip4j:zip4j:2.11.5" zip4j = "net.lingala.zip4j:zip4j:2.11.5"
commonscompress = "org.apache.commons:commons-compress:1.27.1" commonscompress = "org.apache.commons:commons-compress:1.27.1"
junrar = "com.github.junrar:junrar:7.5.5" junrar = "com.github.junrar:junrar:7.5.5"
@@ -200,7 +201,7 @@ okhttp = [
] ]
javalin = [ javalin = [
"javalin-core", "javalin-core",
"javalin-openapi", #"javalin-openapi",
] ]
jackson = [ jackson = [
"jackson-databind", "jackson-databind",

View File

@@ -72,6 +72,7 @@ dependencies {
implementation(libs.asm) implementation(libs.asm)
// Disk & File // Disk & File
implementation(libs.cache4k)
implementation(libs.zip4j) implementation(libs.zip4j)
implementation(libs.commonscompress) implementation(libs.commonscompress)
implementation(libs.junrar) implementation(libs.junrar)

View File

@@ -7,7 +7,7 @@ package suwayomi.tachidesk.global.controller
* License, v. 2.0. If a copy of the MPL was not distributed with this * License, v. 2.0. If a copy of the MPL was not distributed with this
* 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 io.javalin.http.HttpCode import io.javalin.http.HttpStatus
import suwayomi.tachidesk.global.impl.GlobalMeta import suwayomi.tachidesk.global.impl.GlobalMeta
import suwayomi.tachidesk.server.util.formParam import suwayomi.tachidesk.server.util.formParam
import suwayomi.tachidesk.server.util.handler import suwayomi.tachidesk.server.util.handler
@@ -28,7 +28,7 @@ object GlobalMetaController {
ctx.status(200) ctx.status(200)
}, },
withResults = { withResults = {
httpCode(HttpCode.OK) httpCode(HttpStatus.OK)
}, },
) )
@@ -48,8 +48,8 @@ object GlobalMetaController {
ctx.status(200) ctx.status(200)
}, },
withResults = { withResults = {
httpCode(HttpCode.OK) httpCode(HttpStatus.OK)
httpCode(HttpCode.NOT_FOUND) httpCode(HttpStatus.NOT_FOUND)
}, },
) )
} }

View File

@@ -7,7 +7,7 @@ package suwayomi.tachidesk.global.controller
* License, v. 2.0. If a copy of the MPL was not distributed with this * License, v. 2.0. If a copy of the MPL was not distributed with this
* 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 io.javalin.http.HttpCode import io.javalin.http.HttpStatus
import suwayomi.tachidesk.global.impl.About import suwayomi.tachidesk.global.impl.About
import suwayomi.tachidesk.global.impl.AboutDataClass import suwayomi.tachidesk.global.impl.AboutDataClass
import suwayomi.tachidesk.global.impl.AppUpdate import suwayomi.tachidesk.global.impl.AppUpdate
@@ -31,7 +31,7 @@ object SettingsController {
ctx.json(About.getAbout()) ctx.json(About.getAbout())
}, },
withResults = { withResults = {
json<AboutDataClass>(HttpCode.OK) json<AboutDataClass>(HttpStatus.OK)
}, },
) )
@@ -45,12 +45,13 @@ object SettingsController {
} }
}, },
behaviorOf = { ctx -> behaviorOf = { ctx ->
ctx.future( ctx.future {
future { AppUpdate.checkUpdate() }, future { AppUpdate.checkUpdate() }
) .thenApply { ctx.json(it) }
}
}, },
withResults = { withResults = {
json<Array<UpdateDataClass>>(HttpCode.OK) json<Array<UpdateDataClass>>(HttpStatus.OK)
}, },
) )
} }

View File

@@ -9,6 +9,7 @@ package suwayomi.tachidesk.graphql.controller
import io.javalin.http.ContentType import io.javalin.http.ContentType
import io.javalin.http.Context import io.javalin.http.Context
import io.javalin.http.HttpStatus
import io.javalin.websocket.WsConfig import io.javalin.websocket.WsConfig
import suwayomi.tachidesk.graphql.server.TachideskGraphQLServer import suwayomi.tachidesk.graphql.server.TachideskGraphQLServer
import suwayomi.tachidesk.graphql.server.TemporaryFileStorage import suwayomi.tachidesk.graphql.server.TemporaryFileStorage
@@ -20,11 +21,17 @@ object GraphQLController {
/** execute graphql query */ /** execute graphql query */
fun execute(ctx: Context) { fun execute(ctx: Context) {
ctx.future( ctx.future {
future { future {
server.execute(ctx) server.execute(ctx)
}, }.thenApply {
) if (it != null) {
ctx.json(it)
} else {
ctx.status(HttpStatus.BAD_REQUEST)
}
}
}
} }
fun playground(ctx: Context) { fun playground(ctx: Context) {

View File

@@ -30,7 +30,7 @@ class BackupMutation {
val (clientMutationId, backup) = input val (clientMutationId, backup) = input
return future { return future {
val restoreId = ProtoBackupImport.restore(backup.content) val restoreId = ProtoBackupImport.restore(backup.content())
withTimeout(10.seconds) { withTimeout(10.seconds) {
ProtoBackupImport.notifyFlow.first { ProtoBackupImport.notifyFlow.first {

View File

@@ -165,9 +165,9 @@ class ExtensionMutation {
return future { return future {
asDataFetcherResult { asDataFetcherResult {
Extension.installExternalExtension(extensionFile.content, extensionFile.filename) Extension.installExternalExtension(extensionFile.content(), extensionFile.filename())
val dbExtension = transaction { ExtensionTable.select { ExtensionTable.apkName eq extensionFile.filename }.first() } val dbExtension = transaction { ExtensionTable.select { ExtensionTable.apkName eq extensionFile.filename() }.first() }
InstallExternalExtensionPayload( InstallExternalExtensionPayload(
clientMutationId, clientMutationId,

View File

@@ -26,7 +26,7 @@ class BackupQuery {
) )
fun validateBackup(input: ValidateBackupInput): ValidateBackupResult { fun validateBackup(input: ValidateBackupInput): ValidateBackupResult {
val result = ProtoBackupValidator.validate(input.backup.content) val result = ProtoBackupValidator.validate(input.backup.content())
return ValidateBackupResult( return ValidateBackupResult(
result.missingSourceIds.map { ValidateBackupSource(it.first, it.second) }, result.missingSourceIds.map { ValidateBackupSource(it.first, it.second) },
result.missingTrackers.map { ValidateBackupTracker(it) }, result.missingTrackers.map { ValidateBackupTracker(it) },

View File

@@ -13,7 +13,7 @@ import com.expediagroup.graphql.server.types.GraphQLRequest
import com.expediagroup.graphql.server.types.GraphQLServerRequest import com.expediagroup.graphql.server.types.GraphQLServerRequest
import io.javalin.http.Context import io.javalin.http.Context
import io.javalin.http.UploadedFile import io.javalin.http.UploadedFile
import io.javalin.plugin.json.jsonMapper import io.javalin.json.fromJsonString
import java.io.IOException import java.io.IOException
class JavalinGraphQLRequestParser : GraphQLRequestParser<Context> { class JavalinGraphQLRequestParser : GraphQLRequestParser<Context> {
@@ -33,20 +33,13 @@ class JavalinGraphQLRequestParser : GraphQLRequestParser<Context> {
} }
val request = val request =
context.jsonMapper().fromJsonString( context.jsonMapper().fromJsonString<GraphQLServerRequest>(formParam)
formParam,
GraphQLServerRequest::class.java,
)
@Suppress("UNCHECKED_CAST")
val map = val map =
context context
.formParam("map") .formParam("map")
?.let { ?.let {
context.jsonMapper().fromJsonString( context.jsonMapper().fromJsonString<Map<String, List<String>>>(it)
it,
Map::class.java as Class<Map<String, List<String>>>,
)
}.orEmpty() }.orEmpty()
val mapItems = val mapItems =

View File

@@ -69,7 +69,7 @@ class ApolloSubscriptionProtocolHandler(
if (operationMessage.type != GQL_PING.type) { if (operationMessage.type != GQL_PING.type) {
logger.debug { logger.debug {
"GraphQL subscription client message, sessionId=${context.sessionId} type=${operationMessage.type} operationName=${ "GraphQL subscription client message, sessionId=${context.sessionId()} type=${operationMessage.type} operationName=${
getOperationName(operationMessage.payload) getOperationName(operationMessage.payload)
} ${ } ${
if (serverConfig.gqlDebugLogsEnabled.value) { if (serverConfig.gqlDebugLogsEnabled.value) {
@@ -118,7 +118,7 @@ class ApolloSubscriptionProtocolHandler(
if (sessionState.doesOperationExist(operationMessage)) { if (sessionState.doesOperationExist(operationMessage)) {
sessionState.terminateSession(context, CloseStatus(4409, "Subscriber for ${operationMessage.id} already exists")) sessionState.terminateSession(context, CloseStatus(4409, "Subscriber for ${operationMessage.id} already exists"))
logger.info("Already subscribed to operation ${operationMessage.id} for session ${context.sessionId}") logger.info("Already subscribed to operation ${operationMessage.id} for session ${context.sessionId()}")
return emptyFlow() return emptyFlow()
} }
@@ -174,7 +174,7 @@ class ApolloSubscriptionProtocolHandler(
private fun onPing(): Flow<SubscriptionOperationMessage> = flowOf(pongMessage) private fun onPing(): Flow<SubscriptionOperationMessage> = flowOf(pongMessage)
private fun onDisconnect(context: WsContext): Flow<SubscriptionOperationMessage> { private fun onDisconnect(context: WsContext): Flow<SubscriptionOperationMessage> {
logger.debug("Session \"${context.sessionId}\" disconnected") logger.debug("Session \"${context.sessionId()}\" disconnected")
sessionState.terminateSession(context, CloseStatus(1000, "Normal Closure")) sessionState.terminateSession(context, CloseStatus(1000, "Normal Closure"))
return emptyFlow() return emptyFlow()
} }

View File

@@ -36,14 +36,14 @@ internal class ApolloSubscriptionSessionState {
context: WsContext, context: WsContext,
graphQLContext: GraphQLContext, graphQLContext: GraphQLContext,
) { ) {
cachedGraphQLContext[context.sessionId] = graphQLContext cachedGraphQLContext[context.sessionId()] = graphQLContext
} }
/** /**
* Return the graphQL context for this session. * Return the graphQL context for this session.
*/ */
fun getGraphQLContext(context: WsContext): GraphQLContext = fun getGraphQLContext(context: WsContext): GraphQLContext =
cachedGraphQLContext[context.sessionId] ?: emptyMap<Any, Any>().toGraphQLContext() cachedGraphQLContext[context.sessionId()] ?: emptyMap<Any, Any>().toGraphQLContext()
/** /**
* Save the operation that is sending data to the client. * Save the operation that is sending data to the client.
@@ -58,7 +58,7 @@ internal class ApolloSubscriptionSessionState {
val id = operationMessage.id val id = operationMessage.id
if (id != null) { if (id != null) {
activeOperations[id] = subscription activeOperations[id] = subscription
sessionToOperationId.getOrPut(context.sessionId) { CopyOnWriteArrayList() } += id sessionToOperationId.getOrPut(context.sessionId()) { CopyOnWriteArrayList() } += id
} }
} }
@@ -87,10 +87,10 @@ internal class ApolloSubscriptionSessionState {
context: WsContext, context: WsContext,
code: CloseStatus, code: CloseStatus,
) { ) {
sessionToOperationId.remove(context.sessionId)?.forEach { sessionToOperationId.remove(context.sessionId())?.forEach {
removeActiveOperation(it) removeActiveOperation(it)
} }
cachedGraphQLContext.remove(context.sessionId) cachedGraphQLContext.remove(context.sessionId())
context.closeSession(code) context.closeSession(code)
} }

View File

@@ -1,6 +1,6 @@
package suwayomi.tachidesk.manga.controller package suwayomi.tachidesk.manga.controller
import io.javalin.http.HttpCode import io.javalin.http.HttpStatus
import suwayomi.tachidesk.manga.impl.backup.BackupFlags import suwayomi.tachidesk.manga.impl.backup.BackupFlags
import suwayomi.tachidesk.manga.impl.backup.proto.ProtoBackupExport import suwayomi.tachidesk.manga.impl.backup.proto.ProtoBackupExport
import suwayomi.tachidesk.manga.impl.backup.proto.ProtoBackupImport import suwayomi.tachidesk.manga.impl.backup.proto.ProtoBackupImport
@@ -28,14 +28,16 @@ object BackupController {
} }
}, },
behaviorOf = { ctx -> behaviorOf = { ctx ->
ctx.future( ctx.future {
future { future {
ProtoBackupImport.restoreLegacy(ctx.bodyAsInputStream()) ProtoBackupImport.restoreLegacy(ctx.bodyInputStream())
}, }.thenApply {
) ctx.json(it)
}
}
}, },
withResults = { withResults = {
httpCode(HttpCode.OK) httpCode(HttpStatus.OK)
}, },
) )
@@ -54,15 +56,17 @@ object BackupController {
}, },
behaviorOf = { ctx -> behaviorOf = { ctx ->
// TODO: rewrite this with ctx.uploadedFiles(), don't call the multipart field "backup.proto.gz" // TODO: rewrite this with ctx.uploadedFiles(), don't call the multipart field "backup.proto.gz"
ctx.future( ctx.future {
future { future {
ProtoBackupImport.restoreLegacy(ctx.uploadedFile("backup.proto.gz")!!.content) ProtoBackupImport.restoreLegacy(ctx.uploadedFile("backup.proto.gz")!!.content())
}, }.thenApply {
) ctx.json(it)
}
}
}, },
withResults = { withResults = {
httpCode(HttpCode.OK) httpCode(HttpStatus.OK)
httpCode(HttpCode.NOT_FOUND) httpCode(HttpStatus.NOT_FOUND)
}, },
) )
@@ -77,7 +81,7 @@ object BackupController {
}, },
behaviorOf = { ctx -> behaviorOf = { ctx ->
ctx.contentType("application/octet-stream") ctx.contentType("application/octet-stream")
ctx.future( ctx.future {
future { future {
ProtoBackupExport.createBackup( ProtoBackupExport.createBackup(
BackupFlags( BackupFlags(
@@ -88,11 +92,11 @@ object BackupController {
includeHistory = true, includeHistory = true,
), ),
) )
}, }.thenApply { ctx.result(it) }
) }
}, },
withResults = { withResults = {
stream(HttpCode.OK) stream(HttpStatus.OK)
}, },
) )
@@ -109,7 +113,7 @@ object BackupController {
ctx.contentType("application/octet-stream") ctx.contentType("application/octet-stream")
ctx.header("Content-Disposition", """attachment; filename="${Backup.getFilename()}"""") ctx.header("Content-Disposition", """attachment; filename="${Backup.getFilename()}"""")
ctx.future( ctx.future {
future { future {
ProtoBackupExport.createBackup( ProtoBackupExport.createBackup(
BackupFlags( BackupFlags(
@@ -120,11 +124,11 @@ object BackupController {
includeHistory = true, includeHistory = true,
), ),
) )
}, }.thenApply { ctx.result(it) }
) }
}, },
withResults = { withResults = {
stream(HttpCode.OK) stream(HttpStatus.OK)
}, },
) )
@@ -138,14 +142,16 @@ object BackupController {
} }
}, },
behaviorOf = { ctx -> behaviorOf = { ctx ->
ctx.future( ctx.future {
future { future {
ProtoBackupValidator.validate(ctx.bodyAsInputStream()) ProtoBackupValidator.validate(ctx.bodyInputStream())
}, }.thenApply {
) ctx.json(it)
}
}
}, },
withResults = { withResults = {
json<ProtoBackupValidator.ValidationResult>(HttpCode.OK) json<ProtoBackupValidator.ValidationResult>(HttpStatus.OK)
}, },
) )
@@ -167,14 +173,16 @@ object BackupController {
} }
}, },
behaviorOf = { ctx -> behaviorOf = { ctx ->
ctx.future( ctx.future {
future { future {
ProtoBackupValidator.validate(ctx.uploadedFile("backup.proto.gz")!!.content) ProtoBackupValidator.validate(ctx.uploadedFile("backup.proto.gz")!!.content())
}, }.thenApply {
) ctx.json(it)
}
}
}, },
withResults = { withResults = {
json<ProtoBackupValidator.ValidationResult>(HttpCode.OK) json<ProtoBackupValidator.ValidationResult>(HttpStatus.OK)
}, },
) )
} }

View File

@@ -7,7 +7,7 @@ package suwayomi.tachidesk.manga.controller
* License, v. 2.0. If a copy of the MPL was not distributed with this * License, v. 2.0. If a copy of the MPL was not distributed with this
* 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 io.javalin.http.HttpCode import io.javalin.http.HttpStatus
import suwayomi.tachidesk.manga.impl.Category import suwayomi.tachidesk.manga.impl.Category
import suwayomi.tachidesk.manga.impl.CategoryManga import suwayomi.tachidesk.manga.impl.CategoryManga
import suwayomi.tachidesk.manga.model.dataclass.CategoryDataClass import suwayomi.tachidesk.manga.model.dataclass.CategoryDataClass
@@ -31,7 +31,7 @@ object CategoryController {
ctx.json(Category.getCategoryList()) ctx.json(Category.getCategoryList())
}, },
withResults = { withResults = {
json<Array<CategoryDataClass>>(HttpCode.OK) json<Array<CategoryDataClass>>(HttpStatus.OK)
}, },
) )
@@ -49,12 +49,12 @@ object CategoryController {
if (Category.createCategory(name) != -1) { if (Category.createCategory(name) != -1) {
ctx.status(200) ctx.status(200)
} else { } else {
ctx.status(HttpCode.BAD_REQUEST) ctx.status(HttpStatus.BAD_REQUEST)
} }
}, },
withResults = { withResults = {
httpCode(HttpCode.OK) httpCode(HttpStatus.OK)
httpCode(HttpCode.BAD_REQUEST) httpCode(HttpStatus.BAD_REQUEST)
}, },
) )
@@ -77,7 +77,7 @@ object CategoryController {
ctx.status(200) ctx.status(200)
}, },
withResults = { withResults = {
httpCode(HttpCode.OK) httpCode(HttpStatus.OK)
}, },
) )
@@ -96,7 +96,7 @@ object CategoryController {
ctx.status(200) ctx.status(200)
}, },
withResults = { withResults = {
httpCode(HttpCode.OK) httpCode(HttpStatus.OK)
}, },
) )
@@ -114,7 +114,7 @@ object CategoryController {
ctx.json(CategoryManga.getCategoryMangaList(categoryId)) ctx.json(CategoryManga.getCategoryMangaList(categoryId))
}, },
withResults = { withResults = {
json<Array<MangaDataClass>>(HttpCode.OK) json<Array<MangaDataClass>>(HttpStatus.OK)
}, },
) )
@@ -134,7 +134,7 @@ object CategoryController {
ctx.status(200) ctx.status(200)
}, },
withResults = { withResults = {
httpCode(HttpCode.OK) httpCode(HttpStatus.OK)
}, },
) )
@@ -155,8 +155,8 @@ object CategoryController {
ctx.status(200) ctx.status(200)
}, },
withResults = { withResults = {
httpCode(HttpCode.OK) httpCode(HttpStatus.OK)
httpCode(HttpCode.NOT_FOUND) httpCode(HttpStatus.NOT_FOUND)
}, },
) )
} }

View File

@@ -7,7 +7,7 @@ package suwayomi.tachidesk.manga.controller
* License, v. 2.0. If a copy of the MPL was not distributed with this * License, v. 2.0. If a copy of the MPL was not distributed with this
* 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 io.javalin.http.HttpCode import io.javalin.http.HttpStatus
import io.javalin.websocket.WsConfig import io.javalin.websocket.WsConfig
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import suwayomi.tachidesk.manga.impl.download.DownloadManager import suwayomi.tachidesk.manga.impl.download.DownloadManager
@@ -48,7 +48,7 @@ object DownloadController {
DownloadManager.start() DownloadManager.start()
}, },
withResults = { withResults = {
httpCode(HttpCode.OK) httpCode(HttpStatus.OK)
}, },
) )
@@ -62,12 +62,13 @@ object DownloadController {
} }
}, },
behaviorOf = { ctx -> behaviorOf = { ctx ->
ctx.future( ctx.future {
future { DownloadManager.stop() }, future { DownloadManager.stop() }
) .thenApply { ctx.status(HttpStatus.OK) }
}
}, },
withResults = { withResults = {
httpCode(HttpCode.OK) httpCode(HttpStatus.OK)
}, },
) )
@@ -81,12 +82,13 @@ object DownloadController {
} }
}, },
behaviorOf = { ctx -> behaviorOf = { ctx ->
ctx.future( ctx.future {
future { DownloadManager.clear() }, future { DownloadManager.clear() }
) .thenApply { ctx.status(HttpStatus.OK) }
}
}, },
withResults = { withResults = {
httpCode(HttpCode.OK) httpCode(HttpStatus.OK)
}, },
) )
@@ -102,15 +104,15 @@ object DownloadController {
} }
}, },
behaviorOf = { ctx, chapterIndex, mangaId -> behaviorOf = { ctx, chapterIndex, mangaId ->
ctx.future( ctx.future {
future { future {
DownloadManager.enqueueWithChapterIndex(mangaId, chapterIndex) DownloadManager.enqueueWithChapterIndex(mangaId, chapterIndex)
}, }.thenApply { ctx.status(HttpStatus.OK) }
) }
}, },
withResults = { withResults = {
httpCode(HttpCode.OK) httpCode(HttpStatus.OK)
httpCode(HttpCode.NOT_FOUND) httpCode(HttpStatus.NOT_FOUND)
}, },
) )
@@ -125,14 +127,14 @@ object DownloadController {
}, },
behaviorOf = { ctx -> behaviorOf = { ctx ->
val inputs = json.decodeFromString<EnqueueInput>(ctx.body()) val inputs = json.decodeFromString<EnqueueInput>(ctx.body())
ctx.future( ctx.future {
future { future {
DownloadManager.enqueue(inputs) DownloadManager.enqueue(inputs)
}, }.thenApply { ctx.status(HttpStatus.OK) }
) }
}, },
withResults = { withResults = {
httpCode(HttpCode.OK) httpCode(HttpStatus.OK)
}, },
) )
@@ -148,14 +150,14 @@ object DownloadController {
}, },
behaviorOf = { ctx -> behaviorOf = { ctx ->
val input = json.decodeFromString<EnqueueInput>(ctx.body()) val input = json.decodeFromString<EnqueueInput>(ctx.body())
ctx.future( ctx.future {
future { future {
DownloadManager.dequeue(input) DownloadManager.dequeue(input)
}, }.thenApply { ctx.status(HttpStatus.OK) }
) }
}, },
withResults = { withResults = {
httpCode(HttpCode.OK) httpCode(HttpStatus.OK)
}, },
) )
@@ -176,7 +178,7 @@ object DownloadController {
ctx.status(200) ctx.status(200)
}, },
withResults = { withResults = {
httpCode(HttpCode.OK) httpCode(HttpStatus.OK)
}, },
) )
@@ -196,7 +198,7 @@ object DownloadController {
DownloadManager.reorder(chapterIndex, mangaId, to) DownloadManager.reorder(chapterIndex, mangaId, to)
}, },
withResults = { withResults = {
httpCode(HttpCode.OK) httpCode(HttpStatus.OK)
}, },
) )
} }

View File

@@ -7,7 +7,7 @@ package suwayomi.tachidesk.manga.controller
* License, v. 2.0. If a copy of the MPL was not distributed with this * License, v. 2.0. If a copy of the MPL was not distributed with this
* 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 io.javalin.http.HttpCode import io.javalin.http.HttpStatus
import mu.KotlinLogging import mu.KotlinLogging
import suwayomi.tachidesk.manga.impl.extension.Extension import suwayomi.tachidesk.manga.impl.extension.Extension
import suwayomi.tachidesk.manga.impl.extension.ExtensionsList import suwayomi.tachidesk.manga.impl.extension.ExtensionsList
@@ -31,14 +31,16 @@ object ExtensionController {
} }
}, },
behaviorOf = { ctx -> behaviorOf = { ctx ->
ctx.future( ctx.future {
future { future {
ExtensionsList.getExtensionList() ExtensionsList.getExtensionList()
}, }.thenApply {
) ctx.json(it)
}
}
}, },
withResults = { withResults = {
json<Array<ExtensionDataClass>>(HttpCode.OK) json<Array<ExtensionDataClass>>(HttpStatus.OK)
}, },
) )
@@ -53,16 +55,18 @@ object ExtensionController {
} }
}, },
behaviorOf = { ctx, pkgName -> behaviorOf = { ctx, pkgName ->
ctx.future( ctx.future {
future { future {
Extension.installExtension(pkgName) Extension.installExtension(pkgName)
}, }.thenApply {
) ctx.status(it)
}
}
}, },
withResults = { withResults = {
httpCode(HttpCode.CREATED) httpCode(HttpStatus.CREATED)
httpCode(HttpCode.FOUND) httpCode(HttpStatus.FOUND)
httpCode(HttpCode.INTERNAL_SERVER_ERROR) httpCode(HttpStatus.INTERNAL_SERVER_ERROR)
}, },
) )
@@ -81,18 +85,23 @@ object ExtensionController {
}, },
behaviorOf = { ctx -> behaviorOf = { ctx ->
val uploadedFile = ctx.uploadedFile("file")!! val uploadedFile = ctx.uploadedFile("file")!!
logger.debug { "Uploaded extension file name: " + uploadedFile.filename } logger.debug { "Uploaded extension file name: " + uploadedFile.filename() }
ctx.future( ctx.future {
future { future {
Extension.installExternalExtension(uploadedFile.content, uploadedFile.filename) Extension.installExternalExtension(
}, uploadedFile.content(),
uploadedFile.filename(),
) )
}.thenApply {
ctx.status(it)
}
}
}, },
withResults = { withResults = {
httpCode(HttpCode.CREATED) httpCode(HttpStatus.CREATED)
httpCode(HttpCode.FOUND) httpCode(HttpStatus.FOUND)
httpCode(HttpCode.INTERNAL_SERVER_ERROR) httpCode(HttpStatus.INTERNAL_SERVER_ERROR)
}, },
) )
@@ -107,17 +116,19 @@ object ExtensionController {
} }
}, },
behaviorOf = { ctx, pkgName -> behaviorOf = { ctx, pkgName ->
ctx.future( ctx.future {
future { future {
Extension.updateExtension(pkgName) Extension.updateExtension(pkgName)
}, }.thenApply {
) ctx.status(it)
}
}
}, },
withResults = { withResults = {
httpCode(HttpCode.CREATED) httpCode(HttpStatus.CREATED)
httpCode(HttpCode.FOUND) httpCode(HttpStatus.FOUND)
httpCode(HttpCode.NOT_FOUND) httpCode(HttpStatus.NOT_FOUND)
httpCode(HttpCode.INTERNAL_SERVER_ERROR) httpCode(HttpStatus.INTERNAL_SERVER_ERROR)
}, },
) )
@@ -136,10 +147,10 @@ object ExtensionController {
ctx.status(200) ctx.status(200)
}, },
withResults = { withResults = {
httpCode(HttpCode.CREATED) httpCode(HttpStatus.CREATED)
httpCode(HttpCode.FOUND) httpCode(HttpStatus.FOUND)
httpCode(HttpCode.NOT_FOUND) httpCode(HttpStatus.NOT_FOUND)
httpCode(HttpCode.INTERNAL_SERVER_ERROR) httpCode(HttpStatus.INTERNAL_SERVER_ERROR)
}, },
) )
@@ -154,19 +165,19 @@ object ExtensionController {
} }
}, },
behaviorOf = { ctx, apkName -> behaviorOf = { ctx, apkName ->
ctx.future( ctx.future {
future { Extension.getExtensionIcon(apkName) } future { Extension.getExtensionIcon(apkName) }
.thenApply { .thenApply {
ctx.header("content-type", it.second) ctx.header("content-type", it.second)
val httpCacheSeconds = 365.days.inWholeSeconds val httpCacheSeconds = 365.days.inWholeSeconds
ctx.header("cache-control", "max-age=$httpCacheSeconds, immutable") ctx.header("cache-control", "max-age=$httpCacheSeconds, immutable")
it.first ctx.result(it.first)
}, }
) }
}, },
withResults = { withResults = {
image(HttpCode.OK) image(HttpStatus.OK)
httpCode(HttpCode.NOT_FOUND) httpCode(HttpStatus.NOT_FOUND)
}, },
) )
} }

View File

@@ -7,7 +7,7 @@ package suwayomi.tachidesk.manga.controller
* License, v. 2.0. If a copy of the MPL was not distributed with this * License, v. 2.0. If a copy of the MPL was not distributed with this
* 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 io.javalin.http.HttpCode import io.javalin.http.HttpStatus
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import suwayomi.tachidesk.manga.impl.CategoryManga import suwayomi.tachidesk.manga.impl.CategoryManga
import suwayomi.tachidesk.manga.impl.Chapter import suwayomi.tachidesk.manga.impl.Chapter
@@ -41,15 +41,15 @@ object MangaController {
} }
}, },
behaviorOf = { ctx, mangaId, onlineFetch -> behaviorOf = { ctx, mangaId, onlineFetch ->
ctx.future( ctx.future {
future { future {
Manga.getManga(mangaId, onlineFetch) Manga.getManga(mangaId, onlineFetch)
}, }.thenApply { ctx.json(it) }
) }
}, },
withResults = { withResults = {
json<MangaDataClass>(HttpCode.OK) json<MangaDataClass>(HttpStatus.OK)
httpCode(HttpCode.NOT_FOUND) httpCode(HttpStatus.NOT_FOUND)
}, },
) )
@@ -65,15 +65,15 @@ object MangaController {
} }
}, },
behaviorOf = { ctx, mangaId, onlineFetch -> behaviorOf = { ctx, mangaId, onlineFetch ->
ctx.future( ctx.future {
future { future {
Manga.getMangaFull(mangaId, onlineFetch) Manga.getMangaFull(mangaId, onlineFetch)
}, }.thenApply { ctx.json(it) }
) }
}, },
withResults = { withResults = {
json<MangaDataClass>(HttpCode.OK) json<MangaDataClass>(HttpStatus.OK)
httpCode(HttpCode.NOT_FOUND) httpCode(HttpStatus.NOT_FOUND)
}, },
) )
@@ -88,19 +88,19 @@ object MangaController {
} }
}, },
behaviorOf = { ctx, mangaId -> behaviorOf = { ctx, mangaId ->
ctx.future( ctx.future {
future { Manga.getMangaThumbnail(mangaId) } future { Manga.getMangaThumbnail(mangaId) }
.thenApply { .thenApply {
ctx.header("content-type", it.second) ctx.header("content-type", it.second)
val httpCacheSeconds = 1.days.inWholeSeconds val httpCacheSeconds = 1.days.inWholeSeconds
ctx.header("cache-control", "max-age=$httpCacheSeconds") ctx.header("cache-control", "max-age=$httpCacheSeconds")
it.first ctx.result(it.first)
}, }
) }
}, },
withResults = { withResults = {
image(HttpCode.OK) image(HttpStatus.OK)
httpCode(HttpCode.NOT_FOUND) httpCode(HttpStatus.NOT_FOUND)
}, },
) )
@@ -115,13 +115,14 @@ object MangaController {
} }
}, },
behaviorOf = { ctx, mangaId -> behaviorOf = { ctx, mangaId ->
ctx.future( ctx.future {
future { Library.addMangaToLibrary(mangaId) }, future { Library.addMangaToLibrary(mangaId) }
) .thenApply { ctx.status(HttpStatus.OK) }
}
}, },
withResults = { withResults = {
httpCode(HttpCode.OK) httpCode(HttpStatus.OK)
httpCode(HttpCode.NOT_FOUND) httpCode(HttpStatus.NOT_FOUND)
}, },
) )
@@ -136,13 +137,14 @@ object MangaController {
} }
}, },
behaviorOf = { ctx, mangaId -> behaviorOf = { ctx, mangaId ->
ctx.future( ctx.future {
future { Library.removeMangaFromLibrary(mangaId) }, future { Library.removeMangaFromLibrary(mangaId) }
) .thenApply { ctx.status(HttpStatus.OK) }
}
}, },
withResults = { withResults = {
httpCode(HttpCode.OK) httpCode(HttpStatus.OK)
httpCode(HttpCode.NOT_FOUND) httpCode(HttpStatus.NOT_FOUND)
}, },
) )
@@ -160,7 +162,7 @@ object MangaController {
ctx.json(CategoryManga.getMangaCategories(mangaId)) ctx.json(CategoryManga.getMangaCategories(mangaId))
}, },
withResults = { withResults = {
json<Array<CategoryDataClass>>(HttpCode.OK) json<Array<CategoryDataClass>>(HttpStatus.OK)
}, },
) )
@@ -180,7 +182,7 @@ object MangaController {
ctx.status(200) ctx.status(200)
}, },
withResults = { withResults = {
httpCode(HttpCode.OK) httpCode(HttpStatus.OK)
}, },
) )
@@ -200,7 +202,7 @@ object MangaController {
ctx.status(200) ctx.status(200)
}, },
withResults = { withResults = {
httpCode(HttpCode.OK) httpCode(HttpStatus.OK)
}, },
) )
@@ -221,8 +223,8 @@ object MangaController {
ctx.status(200) ctx.status(200)
}, },
withResults = { withResults = {
httpCode(HttpCode.OK) httpCode(HttpStatus.OK)
httpCode(HttpCode.NOT_FOUND) httpCode(HttpStatus.NOT_FOUND)
}, },
) )
@@ -242,11 +244,14 @@ object MangaController {
} }
}, },
behaviorOf = { ctx, mangaId, onlineFetch -> behaviorOf = { ctx, mangaId, onlineFetch ->
ctx.future(future { Chapter.getChapterList(mangaId, onlineFetch) }) ctx.future {
future { Chapter.getChapterList(mangaId, onlineFetch) }
.thenApply { ctx.json(it) }
}
}, },
withResults = { withResults = {
json<Array<ChapterDataClass>>(HttpCode.OK) json<Array<ChapterDataClass>>(HttpStatus.OK)
httpCode(HttpCode.NOT_FOUND) httpCode(HttpStatus.NOT_FOUND)
}, },
) )
@@ -266,7 +271,7 @@ object MangaController {
Chapter.modifyChapters(input, mangaId) Chapter.modifyChapters(input, mangaId)
}, },
withResults = { withResults = {
httpCode(HttpCode.OK) httpCode(HttpStatus.OK)
}, },
) )
@@ -291,7 +296,7 @@ object MangaController {
) )
}, },
withResults = { withResults = {
httpCode(HttpCode.OK) httpCode(HttpStatus.OK)
}, },
) )
@@ -307,11 +312,14 @@ object MangaController {
} }
}, },
behaviorOf = { ctx, mangaId, chapterIndex -> behaviorOf = { ctx, mangaId, chapterIndex ->
ctx.future(future { getChapterDownloadReadyByIndex(chapterIndex, mangaId) }) ctx.future {
future { getChapterDownloadReadyByIndex(chapterIndex, mangaId) }
.thenApply { ctx.json(it) }
}
}, },
withResults = { withResults = {
json<ChapterDataClass>(HttpCode.OK) json<ChapterDataClass>(HttpStatus.OK)
httpCode(HttpCode.NOT_FOUND) httpCode(HttpStatus.NOT_FOUND)
}, },
) )
@@ -336,7 +344,7 @@ object MangaController {
ctx.status(200) ctx.status(200)
}, },
withResults = { withResults = {
httpCode(HttpCode.OK) httpCode(HttpStatus.OK)
}, },
) )
@@ -357,8 +365,8 @@ object MangaController {
ctx.status(200) ctx.status(200)
}, },
withResults = { withResults = {
httpCode(HttpCode.OK) httpCode(HttpStatus.OK)
httpCode(HttpCode.NOT_FOUND) httpCode(HttpStatus.NOT_FOUND)
}, },
) )
@@ -381,8 +389,8 @@ object MangaController {
ctx.status(200) ctx.status(200)
}, },
withResults = { withResults = {
httpCode(HttpCode.OK) httpCode(HttpStatus.OK)
httpCode(HttpCode.NOT_FOUND) httpCode(HttpStatus.NOT_FOUND)
}, },
) )
@@ -401,19 +409,19 @@ object MangaController {
} }
}, },
behaviorOf = { ctx, mangaId, chapterIndex, index -> behaviorOf = { ctx, mangaId, chapterIndex, index ->
ctx.future( ctx.future {
future { Page.getPageImage(mangaId, chapterIndex, index) } future { Page.getPageImage(mangaId, chapterIndex, index) }
.thenApply { .thenApply {
ctx.header("content-type", it.second) ctx.header("content-type", it.second)
val httpCacheSeconds = 1.days.inWholeSeconds val httpCacheSeconds = 1.days.inWholeSeconds
ctx.header("cache-control", "max-age=$httpCacheSeconds") ctx.header("cache-control", "max-age=$httpCacheSeconds")
it.first ctx.result(it.first)
}, }
) }
}, },
withResults = { withResults = {
image(HttpCode.OK) image(HttpStatus.OK)
httpCode(HttpCode.NOT_FOUND) httpCode(HttpStatus.NOT_FOUND)
}, },
) )
} }

View File

@@ -7,7 +7,7 @@ package suwayomi.tachidesk.manga.controller
* License, v. 2.0. If a copy of the MPL was not distributed with this * License, v. 2.0. If a copy of the MPL was not distributed with this
* 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 io.javalin.http.HttpCode import io.javalin.http.HttpStatus
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import suwayomi.tachidesk.manga.impl.MangaList import suwayomi.tachidesk.manga.impl.MangaList
import suwayomi.tachidesk.manga.impl.Search import suwayomi.tachidesk.manga.impl.Search
@@ -38,7 +38,7 @@ object SourceController {
ctx.json(Source.getSourceList()) ctx.json(Source.getSourceList())
}, },
withResults = { withResults = {
json<Array<SourceDataClass>>(HttpCode.OK) json<Array<SourceDataClass>>(HttpStatus.OK)
}, },
) )
@@ -56,8 +56,8 @@ object SourceController {
ctx.json(Source.getSource(sourceId)!!) ctx.json(Source.getSource(sourceId)!!)
}, },
withResults = { withResults = {
json<SourceDataClass>(HttpCode.OK) json<SourceDataClass>(HttpStatus.OK)
httpCode(HttpCode.NOT_FOUND) httpCode(HttpStatus.NOT_FOUND)
}, },
) )
@@ -73,14 +73,14 @@ object SourceController {
} }
}, },
behaviorOf = { ctx, sourceId, pageNum -> behaviorOf = { ctx, sourceId, pageNum ->
ctx.future( ctx.future {
future { future {
MangaList.getMangaList(sourceId, pageNum, popular = true) MangaList.getMangaList(sourceId, pageNum, popular = true)
}, }.thenApply { ctx.json(it) }
) }
}, },
withResults = { withResults = {
json<PagedMangaListDataClass>(HttpCode.OK) json<PagedMangaListDataClass>(HttpStatus.OK)
}, },
) )
@@ -96,14 +96,14 @@ object SourceController {
} }
}, },
behaviorOf = { ctx, sourceId, pageNum -> behaviorOf = { ctx, sourceId, pageNum ->
ctx.future( ctx.future {
future { future {
MangaList.getMangaList(sourceId, pageNum, popular = false) MangaList.getMangaList(sourceId, pageNum, popular = false)
}, }.thenApply { ctx.json(it) }
) }
}, },
withResults = { withResults = {
json<PagedMangaListDataClass>(HttpCode.OK) json<PagedMangaListDataClass>(HttpStatus.OK)
}, },
) )
@@ -121,7 +121,7 @@ object SourceController {
ctx.json(Source.getSourcePreferences(sourceId)) ctx.json(Source.getSourcePreferences(sourceId))
}, },
withResults = { withResults = {
json<Array<Source.PreferenceObject>>(HttpCode.OK) json<Array<Source.PreferenceObject>>(HttpStatus.OK)
}, },
) )
@@ -141,7 +141,7 @@ object SourceController {
ctx.json(Source.setSourcePreference(sourceId, preferenceChange.position, preferenceChange.value)) ctx.json(Source.setSourcePreference(sourceId, preferenceChange.position, preferenceChange.value))
}, },
withResults = { withResults = {
httpCode(HttpCode.OK) httpCode(HttpStatus.OK)
}, },
) )
@@ -160,7 +160,7 @@ object SourceController {
ctx.json(Search.getFilterList(sourceId, reset)) ctx.json(Search.getFilterList(sourceId, reset))
}, },
withResults = { withResults = {
json<Array<Search.FilterObject>>(HttpCode.OK) json<Array<Search.FilterObject>>(HttpStatus.OK)
}, },
) )
@@ -189,7 +189,7 @@ object SourceController {
ctx.json(Search.setFilter(sourceId, filterChange)) ctx.json(Search.setFilter(sourceId, filterChange))
}, },
withResults = { withResults = {
httpCode(HttpCode.OK) httpCode(HttpStatus.OK)
}, },
) )
@@ -206,10 +206,13 @@ object SourceController {
} }
}, },
behaviorOf = { ctx, sourceId, searchTerm, pageNum -> behaviorOf = { ctx, sourceId, searchTerm, pageNum ->
ctx.future(future { Search.sourceSearch(sourceId, searchTerm, pageNum) }) ctx.future {
future { Search.sourceSearch(sourceId, searchTerm, pageNum) }
.thenApply { ctx.json(it) }
}
}, },
withResults = { withResults = {
json<PagedMangaListDataClass>(HttpCode.OK) json<PagedMangaListDataClass>(HttpStatus.OK)
}, },
) )
@@ -227,10 +230,13 @@ object SourceController {
}, },
behaviorOf = { ctx, sourceId, pageNum -> behaviorOf = { ctx, sourceId, pageNum ->
val filter = json.decodeFromString<FilterData>(ctx.body()) val filter = json.decodeFromString<FilterData>(ctx.body())
ctx.future(future { Search.sourceFilter(sourceId, pageNum, filter) }) ctx.future {
future { Search.sourceFilter(sourceId, pageNum, filter) }
.thenApply { ctx.json(it) }
}
}, },
withResults = { withResults = {
json<PagedMangaListDataClass>(HttpCode.OK) json<PagedMangaListDataClass>(HttpStatus.OK)
}, },
) )
@@ -249,7 +255,7 @@ object SourceController {
ctx.json(Search.sourceGlobalSearch(searchTerm)) ctx.json(Search.sourceGlobalSearch(searchTerm))
}, },
withResults = { withResults = {
httpCode(HttpCode.OK) httpCode(HttpStatus.OK)
}, },
) )
} }

View File

@@ -7,7 +7,7 @@ package suwayomi.tachidesk.manga.controller
* License, v. 2.0. If a copy of the MPL was not distributed with this * License, v. 2.0. If a copy of the MPL was not distributed with this
* 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 io.javalin.http.HttpCode import io.javalin.http.HttpStatus
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import mu.KotlinLogging import mu.KotlinLogging
import suwayomi.tachidesk.manga.impl.track.Track import suwayomi.tachidesk.manga.impl.track.Track
@@ -36,7 +36,7 @@ object TrackController {
ctx.json(Track.getTrackerList()) ctx.json(Track.getTrackerList())
}, },
withResults = { withResults = {
json<Array<TrackerDataClass>>(HttpCode.OK) json<Array<TrackerDataClass>>(HttpStatus.OK)
}, },
) )
@@ -52,11 +52,14 @@ object TrackController {
behaviorOf = { ctx -> behaviorOf = { ctx ->
val input = json.decodeFromString<Track.LoginInput>(ctx.body()) val input = json.decodeFromString<Track.LoginInput>(ctx.body())
logger.debug { "tracker login $input" } logger.debug { "tracker login $input" }
ctx.future(future { Track.login(input) }) ctx.future {
future { Track.login(input) }
.thenApply { ctx.status(HttpStatus.OK) }
}
}, },
withResults = { withResults = {
httpCode(HttpCode.OK) httpCode(HttpStatus.OK)
httpCode(HttpCode.NOT_FOUND) httpCode(HttpStatus.NOT_FOUND)
}, },
) )
@@ -72,11 +75,14 @@ object TrackController {
behaviorOf = { ctx -> behaviorOf = { ctx ->
val input = json.decodeFromString<Track.LogoutInput>(ctx.body()) val input = json.decodeFromString<Track.LogoutInput>(ctx.body())
logger.debug { "tracker logout $input" } logger.debug { "tracker logout $input" }
ctx.future(future { Track.logout(input) }) ctx.future {
future { Track.logout(input) }
.thenApply { ctx.status(HttpStatus.OK) }
}
}, },
withResults = { withResults = {
httpCode(HttpCode.OK) httpCode(HttpStatus.OK)
httpCode(HttpCode.NOT_FOUND) httpCode(HttpStatus.NOT_FOUND)
}, },
) )
@@ -92,11 +98,14 @@ object TrackController {
behaviorOf = { ctx -> behaviorOf = { ctx ->
val input = json.decodeFromString<Track.SearchInput>(ctx.body()) val input = json.decodeFromString<Track.SearchInput>(ctx.body())
logger.debug { "tracker search $input" } logger.debug { "tracker search $input" }
ctx.future(future { Track.search(input) }) ctx.future {
future { Track.search(input) }
.thenApply { ctx.json(it) }
}
}, },
withResults = { withResults = {
httpCode(HttpCode.OK) httpCode(HttpStatus.OK)
httpCode(HttpCode.NOT_FOUND) httpCode(HttpStatus.NOT_FOUND)
}, },
) )
@@ -112,10 +121,13 @@ object TrackController {
} }
}, },
behaviorOf = { ctx, mangaId, trackerId, remoteId -> behaviorOf = { ctx, mangaId, trackerId, remoteId ->
ctx.future(future { Track.bind(mangaId, trackerId, remoteId.toLong()) }) ctx.future {
future { Track.bind(mangaId, trackerId, remoteId.toLong()) }
.thenApply { ctx.status(HttpStatus.OK) }
}
}, },
withResults = { withResults = {
httpCode(HttpCode.OK) httpCode(HttpStatus.OK)
}, },
) )
@@ -131,10 +143,13 @@ object TrackController {
behaviorOf = { ctx -> behaviorOf = { ctx ->
val input = json.decodeFromString<Track.UpdateInput>(ctx.body()) val input = json.decodeFromString<Track.UpdateInput>(ctx.body())
logger.debug { "tracker update $input" } logger.debug { "tracker update $input" }
ctx.future(future { Track.update(input) }) ctx.future {
future { Track.update(input) }
.thenApply { ctx.status(HttpStatus.OK) }
}
}, },
withResults = { withResults = {
httpCode(HttpCode.OK) httpCode(HttpStatus.OK)
}, },
) )
@@ -148,19 +163,19 @@ object TrackController {
} }
}, },
behaviorOf = { ctx, trackerId -> behaviorOf = { ctx, trackerId ->
ctx.future( ctx.future {
future { Track.getTrackerThumbnail(trackerId) } future { Track.getTrackerThumbnail(trackerId) }
.thenApply { .thenApply {
ctx.header("content-type", it.second) ctx.header("content-type", it.second)
val httpCacheSeconds = 1.days.inWholeSeconds val httpCacheSeconds = 1.days.inWholeSeconds
ctx.header("cache-control", "max-age=$httpCacheSeconds") ctx.header("cache-control", "max-age=$httpCacheSeconds")
it.first ctx.result(it.first)
}, }
) }
}, },
withResults = { withResults = {
image(HttpCode.OK) image(HttpStatus.OK)
httpCode(HttpCode.NOT_FOUND) httpCode(HttpStatus.NOT_FOUND)
}, },
) )
} }

View File

@@ -1,6 +1,6 @@
package suwayomi.tachidesk.manga.controller package suwayomi.tachidesk.manga.controller
import io.javalin.http.HttpCode import io.javalin.http.HttpStatus
import io.javalin.websocket.WsConfig import io.javalin.websocket.WsConfig
import mu.KotlinLogging import mu.KotlinLogging
import suwayomi.tachidesk.manga.impl.Category import suwayomi.tachidesk.manga.impl.Category
@@ -39,14 +39,14 @@ object UpdateController {
} }
}, },
behaviorOf = { ctx, pageNum -> behaviorOf = { ctx, pageNum ->
ctx.future( ctx.future {
future { future {
Chapter.getRecentChapters(pageNum) Chapter.getRecentChapters(pageNum)
}, }.thenApply { ctx.json(it) }
) }
}, },
withResults = { withResults = {
json<PagedMangaChapterListDataClass>(HttpCode.OK) json<PagedMangaChapterListDataClass>(HttpStatus.OK)
}, },
) )
@@ -84,13 +84,13 @@ object UpdateController {
) )
} else { } else {
logger.info { "No Category found" } logger.info { "No Category found" }
ctx.status(HttpCode.BAD_REQUEST) ctx.status(HttpStatus.BAD_REQUEST)
} }
} }
}, },
withResults = { withResults = {
httpCode(HttpCode.OK) httpCode(HttpStatus.OK)
httpCode(HttpCode.BAD_REQUEST) httpCode(HttpStatus.BAD_REQUEST)
}, },
) )
@@ -119,7 +119,7 @@ object UpdateController {
ctx.json(updater.statusDeprecated.value) ctx.json(updater.statusDeprecated.value)
}, },
withResults = { withResults = {
json<UpdateStatus>(HttpCode.OK) json<UpdateStatus>(HttpStatus.OK)
}, },
) )
@@ -134,16 +134,16 @@ object UpdateController {
behaviorOf = { ctx -> behaviorOf = { ctx ->
val updater = Injekt.get<IUpdater>() val updater = Injekt.get<IUpdater>()
logger.info { "Resetting Updater" } logger.info { "Resetting Updater" }
ctx.future( ctx.future {
future { future {
updater.reset() updater.reset()
}.thenApply { }.thenApply {
ctx.status(HttpCode.OK) ctx.status(HttpStatus.OK)
}, }
) }
}, },
withResults = { withResults = {
httpCode(HttpCode.OK) httpCode(HttpStatus.OK)
}, },
) )
} }

View File

@@ -7,13 +7,12 @@ package suwayomi.tachidesk.manga.impl
* License, v. 2.0. If a copy of the MPL was not distributed with this * License, v. 2.0. If a copy of the MPL was not distributed with this
* 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 com.google.common.cache.Cache
import com.google.common.cache.CacheBuilder
import eu.kanade.tachiyomi.source.model.SChapter import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.util.chapter.ChapterRecognition import eu.kanade.tachiyomi.util.chapter.ChapterRecognition
import eu.kanade.tachiyomi.util.chapter.ChapterSanitizer.sanitize import eu.kanade.tachiyomi.util.chapter.ChapterSanitizer.sanitize
import io.github.reactivecircus.cache4k.Cache
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.sync.withLock
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@@ -47,8 +46,8 @@ import suwayomi.tachidesk.manga.model.table.toDataClass
import suwayomi.tachidesk.server.serverConfig import suwayomi.tachidesk.server.serverConfig
import java.time.Instant import java.time.Instant
import java.util.TreeSet import java.util.TreeSet
import java.util.concurrent.TimeUnit
import kotlin.math.max import kotlin.math.max
import kotlin.time.Duration.Companion.minutes
private fun List<ChapterDataClass>.removeDuplicates(currentChapter: ChapterDataClass): List<ChapterDataClass> = private fun List<ChapterDataClass>.removeDuplicates(currentChapter: ChapterDataClass): List<ChapterDataClass> =
groupBy { it.chapterNumber } groupBy { it.chapterNumber }
@@ -124,9 +123,9 @@ object Chapter {
} }
val map: Cache<Int, Mutex> = val map: Cache<Int, Mutex> =
CacheBuilder Cache
.newBuilder() .Builder<Int, Mutex>()
.expireAfterAccess(10, TimeUnit.MINUTES) .expireAfterAccess(10.minutes)
.build() .build()
suspend fun fetchChapterList(mangaId: Int): List<SChapter> { suspend fun fetchChapterList(mangaId: Int): List<SChapter> {

View File

@@ -15,7 +15,7 @@ import eu.kanade.tachiyomi.source.local.LocalSource
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.model.UpdateStrategy import eu.kanade.tachiyomi.source.model.UpdateStrategy
import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.source.online.HttpSource
import io.javalin.http.HttpCode import io.javalin.http.HttpStatus
import mu.KLogger import mu.KLogger
import mu.KotlinLogging import mu.KotlinLogging
import okhttp3.CacheControl import okhttp3.CacheControl
@@ -305,9 +305,9 @@ object Manga {
val tryToRefreshUrl = val tryToRefreshUrl =
!refreshUrl && !refreshUrl &&
listOf( listOf(
HttpCode.GONE.status, HttpStatus.GONE.code,
HttpCode.MOVED_PERMANENTLY.status, HttpStatus.MOVED_PERMANENTLY.code,
HttpCode.NOT_FOUND.status, HttpStatus.NOT_FOUND.code,
523, // (Cloudflare) Origin Is Unreachable 523, // (Cloudflare) Origin Is Unreachable
522, // (Cloudflare) Connection timed out 522, // (Cloudflare) Connection timed out
).contains(e.code) ).contains(e.code)

View File

@@ -10,7 +10,8 @@ package suwayomi.tachidesk.manga.impl
import eu.kanade.tachiyomi.source.CatalogueSource import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.source.model.Filter import eu.kanade.tachiyomi.source.model.Filter
import eu.kanade.tachiyomi.source.model.FilterList import eu.kanade.tachiyomi.source.model.FilterList
import io.javalin.plugin.json.JsonMapper import io.javalin.json.JsonMapper
import io.javalin.json.fromJsonString
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import suwayomi.tachidesk.manga.impl.MangaList.processEntries import suwayomi.tachidesk.manga.impl.MangaList.processEntries
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource.getCatalogueSourceOrStub import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource.getCatalogueSourceOrStub
@@ -129,7 +130,7 @@ object Search {
is Filter.CheckBox -> filter.state = change.state.toBooleanStrict() is Filter.CheckBox -> filter.state = change.state.toBooleanStrict()
is Filter.TriState -> filter.state = change.state.toInt() is Filter.TriState -> filter.state = change.state.toInt()
is Filter.Group<*> -> { is Filter.Group<*> -> {
val groupChange = jsonMapper.fromJsonString(change.state, FilterChange::class.java) val groupChange = jsonMapper.fromJsonString<FilterChange>(change.state)
when (val groupFilter = filter.state[groupChange.position]) { when (val groupFilter = filter.state[groupChange.position]) {
is Filter.CheckBox -> groupFilter.state = groupChange.state.toBooleanStrict() is Filter.CheckBox -> groupFilter.state = groupChange.state.toBooleanStrict()

View File

@@ -11,7 +11,8 @@ import androidx.preference.Preference
import androidx.preference.PreferenceScreen import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.source.ConfigurableSource import eu.kanade.tachiyomi.source.ConfigurableSource
import eu.kanade.tachiyomi.source.sourcePreferences import eu.kanade.tachiyomi.source.sourcePreferences
import io.javalin.plugin.json.JsonMapper import io.javalin.json.JsonMapper
import io.javalin.json.fromJsonString
import mu.KotlinLogging import mu.KotlinLogging
import org.jetbrains.exposed.sql.and import org.jetbrains.exposed.sql.and
import org.jetbrains.exposed.sql.insert import org.jetbrains.exposed.sql.insert
@@ -131,11 +132,10 @@ object Source {
value: String, value: String,
getValue: (Preference) -> Any = { pref -> getValue: (Preference) -> Any = { pref ->
println(jsonMapper::class.java.name) println(jsonMapper::class.java.name)
@Suppress("UNCHECKED_CAST")
when (pref.defaultValueType) { when (pref.defaultValueType) {
"String" -> value "String" -> value
"Boolean" -> value.toBoolean() "Boolean" -> value.toBoolean()
"Set<String>" -> jsonMapper.fromJsonString(value, List::class.java as Class<List<String>>).toSet() "Set<String>" -> jsonMapper.fromJsonString<List<String>>(value).toSet()
else -> throw RuntimeException("Unsupported type conversion") else -> throw RuntimeException("Unsupported type conversion")
} }
}, },

View File

@@ -104,11 +104,11 @@ object DownloadManager {
} }
fun addClient(ctx: WsContext) { fun addClient(ctx: WsContext) {
clients[ctx.sessionId] = ctx clients[ctx.sessionId()] = ctx
} }
fun removeClient(ctx: WsContext) { fun removeClient(ctx: WsContext) {
clients.remove(ctx.sessionId) clients.remove(ctx.sessionId())
} }
fun notifyClient(ctx: WsContext) { fun notifyClient(ctx: WsContext) {

View File

@@ -41,7 +41,7 @@ object UpdaterSocket : Websocket<UpdateStatus>() {
} }
override fun addClient(ctx: WsContext) { override fun addClient(ctx: WsContext) {
logger.info { ctx.sessionId } logger.info { ctx.sessionId() }
super.addClient(ctx) super.addClient(ctx)
if (job?.isActive != true) { if (job?.isActive != true) {
job = start() job = start()

View File

@@ -8,12 +8,12 @@ abstract class Websocket<T> {
protected val clients = ConcurrentHashMap<String, WsContext>() protected val clients = ConcurrentHashMap<String, WsContext>()
open fun addClient(ctx: WsContext) { open fun addClient(ctx: WsContext) {
clients[ctx.sessionId] = ctx clients[ctx.sessionId()] = ctx
notifyClient(ctx, null) notifyClient(ctx, null)
} }
open fun removeClient(ctx: WsContext) { open fun removeClient(ctx: WsContext) {
clients.remove(ctx.sessionId) clients.remove(ctx.sessionId())
} }
open fun notifyAllClients(value: T) { open fun notifyAllClients(value: T) {

View File

@@ -9,12 +9,8 @@ package suwayomi.tachidesk.server
import io.javalin.Javalin import io.javalin.Javalin
import io.javalin.apibuilder.ApiBuilder.path import io.javalin.apibuilder.ApiBuilder.path
import io.javalin.core.security.RouteRole import io.javalin.http.UnauthorizedResponse
import io.javalin.http.staticfiles.Location import io.javalin.http.staticfiles.Location
import io.javalin.plugin.openapi.OpenApiOptions
import io.javalin.plugin.openapi.OpenApiPlugin
import io.javalin.plugin.openapi.ui.SwaggerOptions
import io.swagger.v3.oas.models.info.Info
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.SupervisorJob
@@ -22,7 +18,6 @@ import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.future.future import kotlinx.coroutines.future.future
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import mu.KotlinLogging import mu.KotlinLogging
import org.eclipse.jetty.server.Server
import org.eclipse.jetty.server.ServerConnector import org.eclipse.jetty.server.ServerConnector
import suwayomi.tachidesk.global.GlobalAPI import suwayomi.tachidesk.global.GlobalAPI
import suwayomi.tachidesk.graphql.GraphQL import suwayomi.tachidesk.graphql.GraphQL
@@ -31,9 +26,7 @@ import suwayomi.tachidesk.server.util.Browser
import suwayomi.tachidesk.server.util.WebInterfaceManager import suwayomi.tachidesk.server.util.WebInterfaceManager
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import java.io.IOException import java.io.IOException
import java.lang.IllegalArgumentException
import java.util.concurrent.CompletableFuture import java.util.concurrent.CompletableFuture
import kotlin.NoSuchElementException
import kotlin.concurrent.thread import kotlin.concurrent.thread
object JavalinSetup { object JavalinSetup {
@@ -46,7 +39,28 @@ object JavalinSetup {
fun <T> future(block: suspend CoroutineScope.() -> T): CompletableFuture<T> = scope.future(block = block) fun <T> future(block: suspend CoroutineScope.() -> T): CompletableFuture<T> = scope.future(block = block)
fun javalinSetup() { fun javalinSetup() {
val server = Server() val app =
Javalin.create { config ->
if (serverConfig.webUIEnabled.value) {
val serveWebUI = {
config.spaRoot.addFile("/root", applicationDirs.webUIRoot + "/index.html", Location.EXTERNAL)
}
WebInterfaceManager.setServeWebUI(serveWebUI)
runBlocking {
WebInterfaceManager.setupWebUI()
}
logger.info { "Serving web static files for ${serverConfig.webUIFlavor.value}" }
config.staticFiles.add(applicationDirs.webUIRoot, Location.EXTERNAL)
serveWebUI()
// config.registerPlugin(OpenApiPlugin(getOpenApiOptions()))
}
var connectorAdded = false
config.jetty.modifyServer { server ->
if (!connectorAdded) {
val connector = val connector =
ServerConnector(server).apply { ServerConnector(server).apply {
host = serverConfig.ip.value host = serverConfig.ip.value
@@ -54,7 +68,12 @@ object JavalinSetup {
} }
server.addConnector(connector) server.addConnector(connector)
serverConfig.subscribeTo(combine(serverConfig.ip, serverConfig.port) { ip, port -> Pair(ip, port) }, { (newIp, newPort) -> serverConfig.subscribeTo(
combine(
serverConfig.ip,
serverConfig.port,
) { ip, port -> Pair(ip, port) },
{ (newIp, newPort) ->
val oldIp = connector.host val oldIp = connector.host
val oldPort = connector.port val oldPort = connector.port
@@ -64,43 +83,40 @@ object JavalinSetup {
connector.start() connector.start()
logger.info { "Server ip and/or port changed from $oldIp:$oldPort to $newIp:$newPort " } logger.info { "Server ip and/or port changed from $oldIp:$oldPort to $newIp:$newPort " }
}) },
)
val app = connectorAdded = true
Javalin.create { config ->
if (serverConfig.webUIEnabled.value) {
val serveWebUI = {
config.addSinglePageRoot("/", applicationDirs.webUIRoot + "/index.html", Location.EXTERNAL)
} }
WebInterfaceManager.setServeWebUI(serveWebUI)
runBlocking {
WebInterfaceManager.setupWebUI()
} }
logger.info { "Serving web static files for ${serverConfig.webUIFlavor.value}" } config.bundledPlugins.enableCors { cors ->
config.addStaticFiles(applicationDirs.webUIRoot, Location.EXTERNAL) cors.addRule {
serveWebUI() it.anyHost()
}
config.registerPlugin(OpenApiPlugin(getOpenApiOptions()))
} }
config.server { server } config.router.apiBuilder {
path("api/") {
path("v1/") {
GlobalAPI.defineEndpoints()
MangaAPI.defineEndpoints()
}
GraphQL.defineEndpoints()
}
}
}
config.enableCorsForAllOrigins() app.beforeMatched { ctx ->
config.accessManager { handler, ctx, _ ->
fun credentialsValid(): Boolean { fun credentialsValid(): Boolean {
val (username, password) = ctx.basicAuthCredentials() val basicAuthCredentials = ctx.basicAuthCredentials() ?: return true
return username == serverConfig.basicAuthUsername.value && password == serverConfig.basicAuthPassword.value val (username, password) = basicAuthCredentials
return username == serverConfig.basicAuthUsername.value &&
password == serverConfig.basicAuthPassword.value
} }
if (serverConfig.basicAuthEnabled.value && !(ctx.basicAuthCredentialsExist() && credentialsValid())) { if (serverConfig.basicAuthEnabled.value && !credentialsValid()) {
ctx.header("WWW-Authenticate", "Basic") ctx.header("WWW-Authenticate", "Basic")
ctx.status(401).json("Unauthorized") throw UnauthorizedResponse()
} else {
handler.handle(ctx)
}
} }
} }
@@ -139,36 +155,22 @@ object JavalinSetup {
ctx.result(e.message ?: "Bad Request") ctx.result(e.message ?: "Bad Request")
} }
app.routes {
path("api/") {
path("v1/") {
GlobalAPI.defineEndpoints()
MangaAPI.defineEndpoints()
}
GraphQL.defineEndpoints()
}
}
app.start() app.start()
} }
private fun getOpenApiOptions(): OpenApiOptions { // private fun getOpenApiOptions(): OpenApiOptions {
val applicationInfo = // val applicationInfo =
Info().apply { // Info().apply {
version("1.0") // version("1.0")
description("Suwayomi-Server Api") // description("Suwayomi-Server Api")
} // }
return OpenApiOptions(applicationInfo).apply { // return OpenApiOptions(applicationInfo).apply {
path("/api/openapi.json") // path("/api/openapi.json")
swagger( // swagger(
SwaggerOptions("/api/swagger-ui").apply { // SwaggerOptions("/api/swagger-ui").apply {
title("Suwayomi-Server Swagger Documentation") // title("Suwayomi-Server Swagger Documentation")
}, // },
) // )
} // }
} // }
object Auth {
enum class Role : RouteRole { ANYONE, USER_READ, USER_WRITE }
}
} }

View File

@@ -13,8 +13,8 @@ import eu.kanade.tachiyomi.App
import eu.kanade.tachiyomi.createAppModule import eu.kanade.tachiyomi.createAppModule
import eu.kanade.tachiyomi.network.NetworkHelper import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.source.local.LocalSource import eu.kanade.tachiyomi.source.local.LocalSource
import io.javalin.plugin.json.JavalinJackson import io.javalin.json.JavalinJackson
import io.javalin.plugin.json.JsonMapper import io.javalin.json.JsonMapper
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged
@@ -237,9 +237,7 @@ fun applicationSetup() {
runMigrations(applicationDirs) runMigrations(applicationDirs)
// Disable jetty's logging // Disable jetty's logging
System.setProperty("org.eclipse.jetty.util.log.announce", "false") setLogLevelFor("org.eclipse.jetty", Level.OFF)
System.setProperty("org.eclipse.jetty.util.log.class", "org.eclipse.jetty.util.log.StdErrLog")
System.setProperty("org.eclipse.jetty.LEVEL", "OFF")
// socks proxy settings // socks proxy settings
serverConfig.subscribeTo( serverConfig.subscribeTo(

View File

@@ -7,7 +7,8 @@ package suwayomi.tachidesk.server.util
* License, v. 2.0. If a copy of the MPL was not distributed with this * License, v. 2.0. If a copy of the MPL was not distributed with this
* 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 io.javalin.plugin.json.JsonMapper import io.javalin.json.JsonMapper
import io.javalin.json.fromJsonString
import mu.KotlinLogging import mu.KotlinLogging
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.Request.Builder import okhttp3.Request.Builder
@@ -59,7 +60,7 @@ object AppMutex {
} }
return try { return try {
jsonMapper.fromJsonString(response, AboutDataClass::class.java) jsonMapper.fromJsonString<AboutDataClass>(response)
AppMutexState.TachideskInstanceRunning AppMutexState.TachideskInstanceRunning
} catch (e: IOException) { } catch (e: IOException) {
AppMutexState.OtherApplicationRunning AppMutexState.OtherApplicationRunning

View File

@@ -1,11 +1,8 @@
package suwayomi.tachidesk.server.util package suwayomi.tachidesk.server.util
import io.javalin.http.Context import io.javalin.http.Context
import io.javalin.http.HttpCode import io.javalin.http.Handler
import io.javalin.plugin.openapi.dsl.DocumentedHandler import io.javalin.http.HttpStatus
import io.javalin.plugin.openapi.dsl.OpenApiDocumentation
import io.javalin.plugin.openapi.dsl.documented
import io.swagger.v3.oas.models.Operation
fun <T> getSimpleParamItem( fun <T> getSimpleParamItem(
ctx: Context, ctx: Context,
@@ -160,30 +157,30 @@ sealed class Param<T> {
class ResultsBuilder { class ResultsBuilder {
val results = mutableListOf<ResultType>() val results = mutableListOf<ResultType>()
inline fun <reified T> json(code: HttpCode) { inline fun <reified T> json(code: HttpStatus) {
results += ResultType.MimeType(code, "application/json", T::class.java) results += ResultType.MimeType(code, "application/json", T::class.java)
} }
fun plainText(code: HttpCode) { fun plainText(code: HttpStatus) {
results += ResultType.MimeType(code, "text/plain", String::class.java) results += ResultType.MimeType(code, "text/plain", String::class.java)
} }
fun image(code: HttpCode) { fun image(code: HttpStatus) {
results += ResultType.MimeType(code, "image/*", ByteArray::class.java) results += ResultType.MimeType(code, "image/*", ByteArray::class.java)
} }
fun stream(code: HttpCode) { fun stream(code: HttpStatus) {
results += ResultType.MimeType(code, "application/octet-stream", ByteArray::class.java) results += ResultType.MimeType(code, "application/octet-stream", ByteArray::class.java)
} }
inline fun <reified T> mime( inline fun <reified T> mime(
code: HttpCode, code: HttpStatus,
mime: String, mime: String,
) { ) {
results += ResultType.MimeType(code, mime, T::class.java) results += ResultType.MimeType(code, mime, T::class.java)
} }
fun httpCode(code: HttpCode) { fun httpCode(code: HttpStatus) {
results += ResultType.StatusCode(code) results += ResultType.StatusCode(code)
} }
} }
@@ -192,20 +189,20 @@ sealed class ResultType {
abstract fun applyTo(documentation: OpenApiDocumentation) abstract fun applyTo(documentation: OpenApiDocumentation)
data class MimeType( data class MimeType(
val code: HttpCode, val code: HttpStatus,
val mime: String, val mime: String,
private val clazz: Class<*>, private val clazz: Class<*>,
) : ResultType() { ) : ResultType() {
override fun applyTo(documentation: OpenApiDocumentation) { override fun applyTo(documentation: OpenApiDocumentation) {
documentation.result(code.status.toString(), clazz, mime) documentation.result(code.code.toString(), clazz, mime)
} }
} }
data class StatusCode( data class StatusCode(
val code: HttpCode, val code: HttpStatus,
) : ResultType() { ) : ResultType() {
override fun applyTo(documentation: OpenApiDocumentation) { override fun applyTo(documentation: OpenApiDocumentation) {
documentation.result<Unit>(code.status.toString()) documentation.result<Unit>(code.code.toString())
} }
} }
} }
@@ -576,3 +573,67 @@ inline fun <
) )
}, },
) )
@Suppress("UNUSED")
class OpenApiDocumentation {
fun operation(block: Operation.() -> Unit) {}
fun result(
toString: Any,
clazz: Class<*>,
mime: String,
) {
}
fun <T> result(toString: Any) {
}
fun <T> formParam(
key: String,
defaultValue: T? = null,
isRequired: Boolean = false,
) {}
fun <T> queryParam(
key: String,
defaultValue: T? = null,
isRepeatable: Boolean = false,
) {}
fun <T> pathParam(
key: String,
defaultValue: T? = null,
) {}
fun <T> body() {}
fun uploadedFile(
name: String,
block: (DocumentationFile) -> Unit,
) {}
}
class DocumentationFile {
fun description(string: String) {}
fun required(boolean: Boolean) {}
}
class DocumentedHandler(
private val handler: (ctx: Context) -> Unit,
) : Handler {
override fun handle(ctx: Context) {
handler(ctx)
}
}
fun documented(
documentation: OpenApiDocumentation,
handle: (ctx: Context) -> Unit,
): DocumentedHandler = DocumentedHandler(handle)
class Operation {
fun summary(string: String) {}
fun description(string: String) {}
}

View File

@@ -3,7 +3,7 @@ package suwayomi.tachidesk.manga.controller
import suwayomi.tachidesk.test.ApplicationTest import suwayomi.tachidesk.test.ApplicationTest
// import io.javalin.http.Context // import io.javalin.http.Context
// import io.javalin.http.HttpCode // import io.javalin.http.HttpStatus
// import io.mockk.every // import io.mockk.every
// import io.mockk.mockk // import io.mockk.mockk
// import io.mockk.verify // import io.mockk.verify
@@ -31,7 +31,7 @@ internal class UpdateControllerTest : ApplicationTest() {
// fun `POST non existent Category Id should give error`() { // fun `POST non existent Category Id should give error`() {
// every { ctx.formParam("category") } returns "1" // every { ctx.formParam("category") } returns "1"
// UpdateController.categoryUpdate(ctx) // UpdateController.categoryUpdate(ctx)
// verify { ctx.status(HttpCode.BAD_REQUEST) } // verify { ctx.status(HttpStatus.BAD_REQUEST) }
// val updater by DI.global.instance<IUpdater>() // val updater by DI.global.instance<IUpdater>()
// assertEquals(0, updater.status.value.numberOfJobs) // assertEquals(0, updater.status.value.numberOfJobs)
// } // }
@@ -43,7 +43,7 @@ internal class UpdateControllerTest : ApplicationTest() {
// CategoryManga.addMangaToCategory(1, 1) // CategoryManga.addMangaToCategory(1, 1)
// every { ctx.formParam("category") } returns "1" // every { ctx.formParam("category") } returns "1"
// UpdateController.categoryUpdate(ctx) // UpdateController.categoryUpdate(ctx)
// verify { ctx.status(HttpCode.OK) } // verify { ctx.status(HttpStatus.OK) }
// val updater by DI.global.instance<IUpdater>() // val updater by DI.global.instance<IUpdater>()
// assertEquals(1, updater.status.value.numberOfJobs) // assertEquals(1, updater.status.value.numberOfJobs)
// } // }
@@ -59,7 +59,7 @@ internal class UpdateControllerTest : ApplicationTest() {
// createLibraryManga("mangaInDefault") // createLibraryManga("mangaInDefault")
// every { ctx.formParam("category") } returns null // every { ctx.formParam("category") } returns null
// UpdateController.categoryUpdate(ctx) // UpdateController.categoryUpdate(ctx)
// verify { ctx.status(HttpCode.OK) } // verify { ctx.status(HttpStatus.OK) }
// val updater by DI.global.instance<IUpdater>() // val updater by DI.global.instance<IUpdater>()
// assertEquals(3, updater.status.value.numberOfJobs) // assertEquals(3, updater.status.value.numberOfJobs)
// } // }