使用Ktor替代NanoHTTPD后端
This commit is contained in:
@@ -218,8 +218,12 @@ dependencies {
|
|||||||
implementation(libs.androidsvg)
|
implementation(libs.androidsvg)
|
||||||
implementation(libs.glide.svg)
|
implementation(libs.glide.svg)
|
||||||
implementation(libs.glide.recyclerview)
|
implementation(libs.glide.recyclerview)
|
||||||
implementation(libs.nanohttpd.nanohttpd)
|
implementation(libs.ktor.server.core)
|
||||||
implementation(libs.nanohttpd.websocket)
|
implementation(libs.ktor.server.cio)
|
||||||
|
implementation(libs.ktor.server.content.negotiation)
|
||||||
|
implementation(libs.ktor.serialization.gson)
|
||||||
|
implementation(libs.ktor.server.cors)
|
||||||
|
implementation(libs.ktor.server.websockets)
|
||||||
implementation(libs.zxing.lite)
|
implementation(libs.zxing.lite)
|
||||||
implementation(libs.colorpicker)
|
implementation(libs.colorpicker)
|
||||||
implementation(libs.colorpicker.compose)
|
implementation(libs.colorpicker.compose)
|
||||||
|
|||||||
@@ -29,8 +29,7 @@ import io.legado.app.utils.startForegroundServiceCompat
|
|||||||
import io.legado.app.utils.startService
|
import io.legado.app.utils.startService
|
||||||
import io.legado.app.utils.stopService
|
import io.legado.app.utils.stopService
|
||||||
import io.legado.app.utils.toastOnUi
|
import io.legado.app.utils.toastOnUi
|
||||||
import io.legado.app.web.HttpServer
|
import io.legado.app.web.KtorServer
|
||||||
import io.legado.app.web.WebSocketServer
|
|
||||||
import splitties.init.appCtx
|
import splitties.init.appCtx
|
||||||
import splitties.systemservices.powerManager
|
import splitties.systemservices.powerManager
|
||||||
import splitties.systemservices.wifiManager
|
import splitties.systemservices.wifiManager
|
||||||
@@ -76,8 +75,7 @@ class WebService : BaseService() {
|
|||||||
setReferenceCounted(false)
|
setReferenceCounted(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
private var httpServer: HttpServer? = null
|
private var ktorServer: KtorServer? = null
|
||||||
private var webSocketServer: WebSocketServer? = null
|
|
||||||
private var notificationList = mutableListOf(appCtx.getString(R.string.service_starting))
|
private var notificationList = mutableListOf(appCtx.getString(R.string.service_starting))
|
||||||
private val networkChangedListener by lazy {
|
private val networkChangedListener by lazy {
|
||||||
NetworkChangedListener(this)
|
NetworkChangedListener(this)
|
||||||
@@ -143,32 +141,21 @@ class WebService : BaseService() {
|
|||||||
}
|
}
|
||||||
networkChangedListener.unRegister()
|
networkChangedListener.unRegister()
|
||||||
isRun = false
|
isRun = false
|
||||||
if (httpServer?.isAlive == true) {
|
ktorServer?.stop()
|
||||||
httpServer?.stop()
|
|
||||||
}
|
|
||||||
if (webSocketServer?.isAlive == true) {
|
|
||||||
webSocketServer?.stop()
|
|
||||||
}
|
|
||||||
postEvent(EventBus.WEB_SERVICE, "")
|
postEvent(EventBus.WEB_SERVICE, "")
|
||||||
FlowEventBus.post(EventBus.WEB_SERVICE, "")
|
FlowEventBus.post(EventBus.WEB_SERVICE, "")
|
||||||
upTile(false)
|
upTile(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun upWebServer() {
|
private fun upWebServer() {
|
||||||
if (httpServer?.isAlive == true) {
|
ktorServer?.stop()
|
||||||
httpServer?.stop()
|
|
||||||
}
|
|
||||||
if (webSocketServer?.isAlive == true) {
|
|
||||||
webSocketServer?.stop()
|
|
||||||
}
|
|
||||||
val addressList = NetworkUtils.getLocalIPAddress()
|
val addressList = NetworkUtils.getLocalIPAddress()
|
||||||
if (addressList.any()) {
|
if (addressList.any()) {
|
||||||
val port = getPort()
|
val port = getPort()
|
||||||
httpServer = HttpServer(port)
|
ktorServer = KtorServer(port)
|
||||||
webSocketServer = WebSocketServer(port + 1)
|
|
||||||
try {
|
try {
|
||||||
httpServer?.start()
|
ktorServer?.start()
|
||||||
webSocketServer?.start(1000 * 30) // 通信超时设置
|
ktorServer?.startWebSocket(port + 1)
|
||||||
notificationList.clear()
|
notificationList.clear()
|
||||||
notificationList.addAll(addressList.map { address ->
|
notificationList.addAll(addressList.map { address ->
|
||||||
getString(
|
getString(
|
||||||
|
|||||||
@@ -1,152 +0,0 @@
|
|||||||
package io.legado.app.web
|
|
||||||
|
|
||||||
import android.graphics.Bitmap
|
|
||||||
import fi.iki.elonen.NanoHTTPD
|
|
||||||
import io.legado.app.api.ReturnData
|
|
||||||
import io.legado.app.api.controller.BookController
|
|
||||||
import io.legado.app.api.controller.BookSourceController
|
|
||||||
import io.legado.app.api.controller.ReplaceRuleController
|
|
||||||
import io.legado.app.api.controller.RssSourceController
|
|
||||||
import io.legado.app.help.coroutine.Coroutine
|
|
||||||
import io.legado.app.service.WebService
|
|
||||||
import io.legado.app.utils.GSON
|
|
||||||
import io.legado.app.utils.LogUtils
|
|
||||||
import io.legado.app.utils.stackTraceStr
|
|
||||||
import io.legado.app.web.utils.AssetsWeb
|
|
||||||
import kotlinx.coroutines.runBlocking
|
|
||||||
import okio.Pipe
|
|
||||||
import okio.buffer
|
|
||||||
import java.io.ByteArrayInputStream
|
|
||||||
import java.io.ByteArrayOutputStream
|
|
||||||
|
|
||||||
class HttpServer(port: Int) : NanoHTTPD(port) {
|
|
||||||
private val assetsWeb = AssetsWeb("web")
|
|
||||||
|
|
||||||
override fun serve(session: IHTTPSession): Response {
|
|
||||||
WebService.serve()
|
|
||||||
var returnData: ReturnData? = null
|
|
||||||
val ct = ContentType(session.headers["content-type"]).tryUTF8()
|
|
||||||
session.headers["content-type"] = ct.contentTypeHeader
|
|
||||||
var uri = session.uri
|
|
||||||
|
|
||||||
val startAt = System.currentTimeMillis()
|
|
||||||
LogUtils.d(TAG) {
|
|
||||||
"${session.method.name} - $uri - ${session.queryParameterString} - Start($startAt)"
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
when (session.method) {
|
|
||||||
Method.OPTIONS -> {
|
|
||||||
val response = newFixedLengthResponse("")
|
|
||||||
response.addHeader("Access-Control-Allow-Methods", "POST")
|
|
||||||
response.addHeader("Access-Control-Allow-Headers", "content-type")
|
|
||||||
response.addHeader("Access-Control-Allow-Origin", session.headers["origin"])
|
|
||||||
//response.addHeader("Access-Control-Max-Age", "3600");
|
|
||||||
return response
|
|
||||||
}
|
|
||||||
|
|
||||||
Method.POST -> {
|
|
||||||
val files = HashMap<String, String>()
|
|
||||||
session.parseBody(files)
|
|
||||||
val postData = files["postData"]
|
|
||||||
|
|
||||||
returnData = runBlocking {
|
|
||||||
when (uri) {
|
|
||||||
"/saveBookSource" -> BookSourceController.saveSource(postData)
|
|
||||||
"/saveBookSources" -> BookSourceController.saveSources(postData)
|
|
||||||
"/deleteBookSources" -> BookSourceController.deleteSources(postData)
|
|
||||||
"/saveBook" -> BookController.saveBook(postData)
|
|
||||||
"/deleteBook" -> BookController.deleteBook(postData)
|
|
||||||
"/saveBookProgress" -> BookController.saveBookProgress(postData)
|
|
||||||
"/addLocalBook" -> BookController.addLocalBook(session.parameters, files)
|
|
||||||
"/saveReadConfig" -> BookController.saveWebReadConfig(postData)
|
|
||||||
"/saveRssSource" -> RssSourceController.saveSource(postData)
|
|
||||||
"/saveRssSources" -> RssSourceController.saveSources(postData)
|
|
||||||
"/deleteRssSources" -> RssSourceController.deleteSources(postData)
|
|
||||||
"/saveReplaceRule" -> ReplaceRuleController.saveRule(postData)
|
|
||||||
"/deleteReplaceRule" -> ReplaceRuleController.delete(postData)
|
|
||||||
"/testReplaceRule" -> ReplaceRuleController.testRule(postData)
|
|
||||||
else -> null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Method.GET -> {
|
|
||||||
val parameters = session.parameters
|
|
||||||
|
|
||||||
returnData = when (uri) {
|
|
||||||
"/getBookSource" -> BookSourceController.getSource(parameters)
|
|
||||||
"/getBookSources" -> BookSourceController.sources
|
|
||||||
"/getBookshelf" -> BookController.bookshelf
|
|
||||||
"/getChapterList" -> BookController.getChapterList(parameters)
|
|
||||||
"/refreshToc" -> BookController.refreshToc(parameters)
|
|
||||||
"/getBookContent" -> BookController.getBookContent(parameters)
|
|
||||||
"/cover" -> BookController.getCover(parameters)
|
|
||||||
"/image" -> BookController.getImg(parameters)
|
|
||||||
"/getReadConfig" -> BookController.getWebReadConfig()
|
|
||||||
"/getRssSource" -> RssSourceController.getSource(parameters)
|
|
||||||
"/getRssSources" -> RssSourceController.sources
|
|
||||||
"/getReplaceRules" -> ReplaceRuleController.allRules
|
|
||||||
else -> null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> Unit
|
|
||||||
}
|
|
||||||
|
|
||||||
if (returnData == null) {
|
|
||||||
if (uri.endsWith("/"))
|
|
||||||
uri += "index.html"
|
|
||||||
return assetsWeb.getResponse(uri)
|
|
||||||
}
|
|
||||||
|
|
||||||
val response = if (returnData.data is Bitmap) {
|
|
||||||
val outputStream = ByteArrayOutputStream()
|
|
||||||
(returnData.data as Bitmap).compress(Bitmap.CompressFormat.PNG, 100, outputStream)
|
|
||||||
val byteArray = outputStream.toByteArray()
|
|
||||||
outputStream.close()
|
|
||||||
val inputStream = ByteArrayInputStream(byteArray)
|
|
||||||
newFixedLengthResponse(
|
|
||||||
Response.Status.OK,
|
|
||||||
"image/png",
|
|
||||||
inputStream,
|
|
||||||
byteArray.size.toLong()
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
val data = returnData.data
|
|
||||||
if (data is List<*> && data.size > 3000) {
|
|
||||||
val pipe = Pipe(16 * 1024)
|
|
||||||
Coroutine.async {
|
|
||||||
pipe.sink.buffer().outputStream().bufferedWriter(Charsets.UTF_8).use {
|
|
||||||
GSON.toJson(returnData, it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
newChunkedResponse(
|
|
||||||
Response.Status.OK,
|
|
||||||
"application/json",
|
|
||||||
pipe.source.buffer().inputStream()
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
newFixedLengthResponse(GSON.toJson(returnData))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
response.addHeader("Access-Control-Allow-Methods", "GET, POST")
|
|
||||||
response.addHeader("Access-Control-Allow-Origin", session.headers["origin"])
|
|
||||||
LogUtils.d(TAG) {
|
|
||||||
"${session.method.name} - $uri - ${session.queryParameterString} - End($startAt)"
|
|
||||||
}
|
|
||||||
return response
|
|
||||||
} catch (e: Exception) {
|
|
||||||
LogUtils.d(TAG) {
|
|
||||||
"${session.method.name} - $uri - ${session.queryParameterString} - Error End($startAt)\n$e\n${e.stackTraceStr}"
|
|
||||||
}
|
|
||||||
return newFixedLengthResponse(e.message)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private const val TAG = "HttpServer"
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,207 @@
|
|||||||
|
package io.legado.app.web
|
||||||
|
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import io.ktor.http.*
|
||||||
|
import io.ktor.http.content.*
|
||||||
|
import io.ktor.serialization.gson.*
|
||||||
|
import io.ktor.server.application.*
|
||||||
|
import io.ktor.server.cio.*
|
||||||
|
import io.ktor.server.engine.*
|
||||||
|
import io.ktor.server.plugins.contentnegotiation.*
|
||||||
|
import io.ktor.server.plugins.cors.routing.*
|
||||||
|
import io.ktor.server.request.*
|
||||||
|
import io.ktor.server.response.*
|
||||||
|
import io.ktor.server.routing.*
|
||||||
|
import io.ktor.server.websocket.*
|
||||||
|
import io.ktor.util.pipeline.*
|
||||||
|
import io.legado.app.api.ReturnData
|
||||||
|
import io.legado.app.api.controller.BookController
|
||||||
|
import io.legado.app.api.controller.BookSourceController
|
||||||
|
import io.legado.app.api.controller.ReplaceRuleController
|
||||||
|
import io.legado.app.api.controller.RssSourceController
|
||||||
|
import io.legado.app.model.localBook.LocalBook
|
||||||
|
import io.legado.app.service.WebService
|
||||||
|
import io.legado.app.utils.LogUtils
|
||||||
|
import io.legado.app.utils.stackTraceStr
|
||||||
|
import io.legado.app.web.socket.BookSearchWebSocket
|
||||||
|
import io.legado.app.web.socket.BookSourceDebugWebSocket
|
||||||
|
import io.legado.app.web.socket.RssSourceDebugWebSocket
|
||||||
|
import io.legado.app.web.utils.AssetsWeb
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import java.io.ByteArrayOutputStream
|
||||||
|
|
||||||
|
class KtorServer(private val port: Int) {
|
||||||
|
private var server: ApplicationEngine? = null
|
||||||
|
private var wsServer: ApplicationEngine? = null
|
||||||
|
private val assetsWeb = AssetsWeb("web")
|
||||||
|
|
||||||
|
fun start() {
|
||||||
|
server = embeddedServer(CIO, port = port) {
|
||||||
|
install(ContentNegotiation) {
|
||||||
|
gson {
|
||||||
|
setLenient()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
install(CORS) {
|
||||||
|
anyHost()
|
||||||
|
allowHeader(HttpHeaders.ContentType)
|
||||||
|
allowMethod(HttpMethod.Options)
|
||||||
|
allowMethod(HttpMethod.Post)
|
||||||
|
allowMethod(HttpMethod.Get)
|
||||||
|
}
|
||||||
|
|
||||||
|
routing {
|
||||||
|
post("/saveBookSource") { handlePost { BookSourceController.saveSource(it) } }
|
||||||
|
post("/saveBookSources") { handlePost { BookSourceController.saveSources(it) } }
|
||||||
|
post("/deleteBookSources") { handlePost { BookSourceController.deleteSources(it) } }
|
||||||
|
post("/saveBook") { handlePost { BookController.saveBook(it) } }
|
||||||
|
post("/deleteBook") { handlePost { BookController.deleteBook(it) } }
|
||||||
|
post("/saveBookProgress") { handlePost { BookController.saveBookProgress(it) } }
|
||||||
|
post("/addLocalBook") {
|
||||||
|
WebService.serve()
|
||||||
|
val multipart = call.receiveMultipart()
|
||||||
|
var fileName: String? = null
|
||||||
|
var fileBytes: ByteArray? = null
|
||||||
|
multipart.forEachPart { part ->
|
||||||
|
when (part) {
|
||||||
|
is PartData.FormItem -> {
|
||||||
|
if (part.name == "fileName") fileName = part.value
|
||||||
|
}
|
||||||
|
is PartData.FileItem -> {
|
||||||
|
fileBytes = part.streamProvider().readBytes()
|
||||||
|
}
|
||||||
|
else -> {}
|
||||||
|
}
|
||||||
|
part.dispose()
|
||||||
|
}
|
||||||
|
if (fileName != null && fileBytes != null) {
|
||||||
|
val returnData = withContext(Dispatchers.IO) {
|
||||||
|
kotlin.runCatching {
|
||||||
|
val uri = LocalBook.saveBookFile(fileBytes!!.inputStream(), fileName!!)
|
||||||
|
LocalBook.importFile(uri)
|
||||||
|
ReturnData().setData(true)
|
||||||
|
}.getOrElse {
|
||||||
|
ReturnData().setErrorMsg(it.localizedMessage ?: "Save book error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
respondReturnData(returnData)
|
||||||
|
} else {
|
||||||
|
call.respond(HttpStatusCode.BadRequest, "Missing fileName or fileData")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
post("/saveReadConfig") { handlePost { BookController.saveWebReadConfig(it) } }
|
||||||
|
post("/saveRssSource") { handlePost { RssSourceController.saveSource(it) } }
|
||||||
|
post("/saveRssSources") { handlePost { RssSourceController.saveSources(it) } }
|
||||||
|
post("/deleteRssSources") { handlePost { RssSourceController.deleteSources(it) } }
|
||||||
|
post("/saveReplaceRule") { handlePost { ReplaceRuleController.saveRule(it) } }
|
||||||
|
post("/deleteReplaceRule") { handlePost { ReplaceRuleController.delete(it) } }
|
||||||
|
post("/testReplaceRule") { handlePost { ReplaceRuleController.testRule(it) } }
|
||||||
|
|
||||||
|
get("/getBookSource") { handleGet { BookSourceController.getSource(it) } }
|
||||||
|
get("/getBookSources") { handleGet { BookSourceController.sources } }
|
||||||
|
get("/getBookshelf") { handleGet { BookController.bookshelf } }
|
||||||
|
get("/getChapterList") { handleGet { BookController.getChapterList(it) } }
|
||||||
|
get("/refreshToc") { handleGet { BookController.refreshToc(it) } }
|
||||||
|
get("/getBookContent") { handleGet { BookController.getBookContent(it) } }
|
||||||
|
get("/cover") { handleGet { BookController.getCover(it) } }
|
||||||
|
get("/image") { handleGet { BookController.getImg(it) } }
|
||||||
|
get("/getReadConfig") { handleGet { BookController.getWebReadConfig() } }
|
||||||
|
get("/getRssSource") { handleGet { RssSourceController.getSource(it) } }
|
||||||
|
get("/getRssSources") { handleGet { RssSourceController.sources } }
|
||||||
|
get("/getReplaceRules") { handleGet { ReplaceRuleController.allRules } }
|
||||||
|
|
||||||
|
get("{...}") {
|
||||||
|
WebService.serve()
|
||||||
|
var uri = call.request.uri.substringBefore("?")
|
||||||
|
if (uri.endsWith("/")) uri += "index.html"
|
||||||
|
val inputStream = assetsWeb.getInputStream(uri)
|
||||||
|
if (inputStream != null) {
|
||||||
|
call.respondOutputStream(ContentType.parse(assetsWeb.getMimeType(uri))) {
|
||||||
|
inputStream.copyTo(this)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
call.respond(HttpStatusCode.NotFound)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.start(wait = false)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun startWebSocket(wsPort: Int) {
|
||||||
|
wsServer = embeddedServer(CIO, port = wsPort) {
|
||||||
|
install(WebSockets)
|
||||||
|
routing {
|
||||||
|
webSocket("/bookSourceDebug") {
|
||||||
|
BookSourceDebugWebSocket(this).handle()
|
||||||
|
}
|
||||||
|
webSocket("/rssSourceDebug") {
|
||||||
|
RssSourceDebugWebSocket(this).handle()
|
||||||
|
}
|
||||||
|
webSocket("/searchBook") {
|
||||||
|
BookSearchWebSocket(this).handle()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.start(wait = false)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun stop() {
|
||||||
|
server?.stop(1000, 1000)
|
||||||
|
wsServer?.stop(1000, 1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun PipelineContext<Unit, ApplicationCall>.handlePost(
|
||||||
|
block: suspend (String?) -> ReturnData
|
||||||
|
) {
|
||||||
|
WebService.serve()
|
||||||
|
try {
|
||||||
|
val postData = call.receiveText()
|
||||||
|
val returnData = block(postData)
|
||||||
|
respondReturnData(returnData)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
LogUtils.e(TAG, e.stackTraceStr)
|
||||||
|
call.respondText(e.message ?: "Unknown error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun PipelineContext<Unit, ApplicationCall>.handleGet(
|
||||||
|
block: (Map<String, List<String>>) -> ReturnData?
|
||||||
|
) {
|
||||||
|
WebService.serve()
|
||||||
|
try {
|
||||||
|
val parameters = call.request.queryParameters.toMap()
|
||||||
|
val returnData = block(parameters)
|
||||||
|
if (returnData != null) {
|
||||||
|
respondReturnData(returnData)
|
||||||
|
} else {
|
||||||
|
call.respond(HttpStatusCode.NotFound)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
LogUtils.e(TAG, e.stackTraceStr)
|
||||||
|
call.respondText(e.message ?: "Unknown error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun PipelineContext<Unit, ApplicationCall>.respondReturnData(returnData: ReturnData) {
|
||||||
|
if (returnData.data is Bitmap) {
|
||||||
|
val bitmap = returnData.data as Bitmap
|
||||||
|
val outputStream = ByteArrayOutputStream()
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
bitmap.compress(Bitmap.CompressFormat.PNG, 100, outputStream)
|
||||||
|
}
|
||||||
|
call.respondBytes(outputStream.toByteArray(), ContentType.Image.PNG)
|
||||||
|
} else {
|
||||||
|
call.respond(returnData)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Parameters.toMap(): Map<String, List<String>> {
|
||||||
|
val map = mutableMapOf<String, List<String>>()
|
||||||
|
this.forEach { s, list -> map[s] = list }
|
||||||
|
return map
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val TAG = "KtorServer"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
package io.legado.app.web
|
|
||||||
|
|
||||||
import fi.iki.elonen.NanoWSD
|
|
||||||
import io.legado.app.service.WebService
|
|
||||||
import io.legado.app.web.socket.*
|
|
||||||
|
|
||||||
class WebSocketServer(port: Int) : NanoWSD(port) {
|
|
||||||
|
|
||||||
override fun openWebSocket(handshake: IHTTPSession): WebSocket? {
|
|
||||||
WebService.serve()
|
|
||||||
return when (handshake.uri) {
|
|
||||||
"/bookSourceDebug" -> {
|
|
||||||
BookSourceDebugWebSocket(handshake)
|
|
||||||
}
|
|
||||||
"/rssSourceDebug" -> {
|
|
||||||
RssSourceDebugWebSocket(handshake)
|
|
||||||
}
|
|
||||||
"/searchBook" -> {
|
|
||||||
BookSearchWebSocket(handshake)
|
|
||||||
}
|
|
||||||
else -> null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
package io.legado.app.web.socket
|
package io.legado.app.web.socket
|
||||||
|
|
||||||
import fi.iki.elonen.NanoHTTPD
|
import io.ktor.server.websocket.*
|
||||||
import fi.iki.elonen.NanoWSD
|
import io.ktor.websocket.*
|
||||||
import io.legado.app.R
|
import io.legado.app.R
|
||||||
import io.legado.app.constant.PreferKey
|
import io.legado.app.constant.PreferKey
|
||||||
import io.legado.app.domain.model.BookSearchScope
|
import io.legado.app.domain.model.BookSearchScope
|
||||||
@@ -11,27 +11,14 @@ import io.legado.app.domain.usecase.SearchBooksUseCase
|
|||||||
import io.legado.app.domain.usecase.SearchRunEvent
|
import io.legado.app.domain.usecase.SearchRunEvent
|
||||||
import io.legado.app.help.config.AppConfig
|
import io.legado.app.help.config.AppConfig
|
||||||
import io.legado.app.ui.config.otherConfig.OtherConfig
|
import io.legado.app.ui.config.otherConfig.OtherConfig
|
||||||
import io.legado.app.utils.GSON
|
import io.legado.app.utils.*
|
||||||
import io.legado.app.utils.fromJsonObject
|
import kotlinx.coroutines.*
|
||||||
import io.legado.app.utils.getPrefBoolean
|
import kotlinx.coroutines.channels.consumeEach
|
||||||
import io.legado.app.utils.isJson
|
|
||||||
import kotlinx.coroutines.CancellationException
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.Dispatchers.IO
|
|
||||||
import kotlinx.coroutines.Job
|
|
||||||
import kotlinx.coroutines.MainScope
|
|
||||||
import kotlinx.coroutines.cancel
|
|
||||||
import kotlinx.coroutines.delay
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import org.koin.core.context.GlobalContext
|
import org.koin.core.context.GlobalContext
|
||||||
import splitties.init.appCtx
|
import splitties.init.appCtx
|
||||||
import java.io.IOException
|
|
||||||
|
|
||||||
class BookSearchWebSocket(handshakeRequest: NanoHTTPD.IHTTPSession) :
|
class BookSearchWebSocket(private val session: DefaultWebSocketServerSession) : CoroutineScope by session {
|
||||||
NanoWSD.WebSocket(handshakeRequest),
|
|
||||||
CoroutineScope by MainScope() {
|
|
||||||
|
|
||||||
private val normalClosure = NanoWSD.WebSocketFrame.CloseCode.NormalClosure
|
|
||||||
private val searchBooksUseCase: SearchBooksUseCase by lazy { GlobalContext.get().get() }
|
private val searchBooksUseCase: SearchBooksUseCase by lazy { GlobalContext.get().get() }
|
||||||
private val searchControl = BookSearchControl()
|
private val searchControl = BookSearchControl()
|
||||||
private val sentBookUrls = linkedSetOf<String>()
|
private val sentBookUrls = linkedSetOf<String>()
|
||||||
@@ -39,62 +26,40 @@ class BookSearchWebSocket(handshakeRequest: NanoHTTPD.IHTTPSession) :
|
|||||||
|
|
||||||
private val SEARCH_FINISH = "Search finish"
|
private val SEARCH_FINISH = "Search finish"
|
||||||
|
|
||||||
override fun onOpen() {
|
suspend fun handle() {
|
||||||
launch(IO) {
|
try {
|
||||||
kotlin.runCatching {
|
session.incoming.consumeEach { frame ->
|
||||||
while (isOpen) {
|
if (frame is Frame.Text) {
|
||||||
ping("ping".toByteArray())
|
val text = frame.readText()
|
||||||
delay(30000)
|
if (!text.isJson()) {
|
||||||
}
|
session.send("数据必须为Json格式")
|
||||||
}
|
session.close(CloseReason(CloseReason.Codes.NORMAL, SEARCH_FINISH))
|
||||||
}
|
return@consumeEach
|
||||||
}
|
}
|
||||||
|
val searchMap = GSON.fromJsonObject<Map<String, String>>(text).getOrNull()
|
||||||
override fun onClose(
|
if (searchMap != null) {
|
||||||
code: NanoWSD.WebSocketFrame.CloseCode,
|
val key = searchMap["key"]?.trim()
|
||||||
reason: String,
|
if (key.isNullOrBlank()) {
|
||||||
initiatedByRemote: Boolean
|
session.send(appCtx.getString(R.string.cannot_empty))
|
||||||
) {
|
session.close(CloseReason(CloseReason.Codes.NORMAL, SEARCH_FINISH))
|
||||||
searchJob?.cancel()
|
return@consumeEach
|
||||||
cancel()
|
}
|
||||||
}
|
startSearch(key)
|
||||||
|
|
||||||
override fun onMessage(message: NanoWSD.WebSocketFrame) {
|
|
||||||
launch(IO) {
|
|
||||||
kotlin.runCatching {
|
|
||||||
if (!message.textPayload.isJson()) {
|
|
||||||
send("数据必须为Json格式")
|
|
||||||
close(normalClosure, SEARCH_FINISH, false)
|
|
||||||
return@launch
|
|
||||||
}
|
|
||||||
val searchMap =
|
|
||||||
GSON.fromJsonObject<Map<String, String>>(message.textPayload).getOrNull()
|
|
||||||
if (searchMap != null) {
|
|
||||||
val key = searchMap["key"]?.trim()
|
|
||||||
if (key.isNullOrBlank()) {
|
|
||||||
send(appCtx.getString(R.string.cannot_empty))
|
|
||||||
close(normalClosure, SEARCH_FINISH, false)
|
|
||||||
return@launch
|
|
||||||
}
|
}
|
||||||
startSearch(key)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printOnDebug()
|
||||||
|
} finally {
|
||||||
|
searchJob?.cancel()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPong(pong: NanoWSD.WebSocketFrame) {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onException(exception: IOException) {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun startSearch(key: String) {
|
private fun startSearch(key: String) {
|
||||||
searchJob?.cancel()
|
searchJob?.cancel()
|
||||||
sentBookUrls.clear()
|
sentBookUrls.clear()
|
||||||
searchControl.resume()
|
searchControl.resume()
|
||||||
searchJob = launch(IO) {
|
searchJob = launch(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
searchBooksUseCase
|
searchBooksUseCase
|
||||||
.execute(
|
.execute(
|
||||||
@@ -113,17 +78,17 @@ class BookSearchWebSocket(handshakeRequest: NanoHTTPD.IHTTPSession) :
|
|||||||
is SearchRunEvent.Progress -> {
|
is SearchRunEvent.Progress -> {
|
||||||
val newBooks = event.upsertBooks.filter { sentBookUrls.add(it.bookUrl) }
|
val newBooks = event.upsertBooks.filter { sentBookUrls.add(it.bookUrl) }
|
||||||
if (newBooks.isNotEmpty()) {
|
if (newBooks.isNotEmpty()) {
|
||||||
send(GSON.toJson(newBooks))
|
session.send(GSON.toJson(newBooks))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
is SearchRunEvent.Finished -> close(normalClosure, SEARCH_FINISH, false)
|
is SearchRunEvent.Finished -> session.close(CloseReason(CloseReason.Codes.NORMAL, SEARCH_FINISH))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (exception: CancellationException) {
|
} catch (exception: CancellationException) {
|
||||||
throw exception
|
throw exception
|
||||||
} catch (exception: Throwable) {
|
} catch (exception: Throwable) {
|
||||||
close(normalClosure, exception.toString(), false)
|
session.close(CloseReason(CloseReason.Codes.INTERNAL_ERROR, exception.toString()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,97 +1,71 @@
|
|||||||
package io.legado.app.web.socket
|
package io.legado.app.web.socket
|
||||||
|
|
||||||
|
import io.ktor.server.websocket.*
|
||||||
import fi.iki.elonen.NanoHTTPD
|
import io.ktor.websocket.*
|
||||||
import fi.iki.elonen.NanoWSD
|
|
||||||
import io.legado.app.R
|
import io.legado.app.R
|
||||||
import io.legado.app.data.appDb
|
import io.legado.app.data.appDb
|
||||||
import io.legado.app.model.Debug
|
import io.legado.app.model.Debug
|
||||||
import io.legado.app.utils.*
|
import io.legado.app.utils.*
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
import kotlinx.coroutines.Dispatchers.IO
|
import kotlinx.coroutines.channels.consumeEach
|
||||||
import splitties.init.appCtx
|
import splitties.init.appCtx
|
||||||
|
|
||||||
import java.io.IOException
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* web端书源调试
|
* web端书源调试
|
||||||
*/
|
*/
|
||||||
class BookSourceDebugWebSocket(handshakeRequest: NanoHTTPD.IHTTPSession) :
|
class BookSourceDebugWebSocket(private val session: DefaultWebSocketServerSession) :
|
||||||
NanoWSD.WebSocket(handshakeRequest),
|
CoroutineScope by session,
|
||||||
CoroutineScope by MainScope(),
|
|
||||||
Debug.Callback {
|
Debug.Callback {
|
||||||
|
|
||||||
private val notPrintState = arrayOf(10, 20, 30, 40)
|
private val notPrintState = arrayOf(10, 20, 30, 40)
|
||||||
|
|
||||||
override fun onOpen() {
|
suspend fun handle() {
|
||||||
launch(IO) {
|
try {
|
||||||
kotlin.runCatching {
|
session.incoming.consumeEach { frame ->
|
||||||
while (isOpen) {
|
if (frame is Frame.Text) {
|
||||||
ping("ping".toByteArray())
|
val text = frame.readText()
|
||||||
delay(30000)
|
if (!text.isJson()) {
|
||||||
|
session.send("数据必须为Json格式")
|
||||||
|
session.close(CloseReason(CloseReason.Codes.NORMAL, "调试结束"))
|
||||||
|
return@consumeEach
|
||||||
|
}
|
||||||
|
val debugBean = GSON.fromJsonObject<Map<String, String>>(text).getOrNull()
|
||||||
|
if (debugBean != null) {
|
||||||
|
val tag = debugBean["tag"]
|
||||||
|
val key = debugBean["key"]
|
||||||
|
if (tag.isNullOrBlank() || key.isNullOrBlank()) {
|
||||||
|
session.send(appCtx.getString(R.string.cannot_empty))
|
||||||
|
session.close(CloseReason(CloseReason.Codes.NORMAL, "调试结束"))
|
||||||
|
return@consumeEach
|
||||||
|
}
|
||||||
|
appDb.bookSourceDao.getBookSource(tag)?.let {
|
||||||
|
Debug.callback = this@BookSourceDebugWebSocket
|
||||||
|
Debug.startDebug(this, it, key)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
session.send("数据必须为Json格式")
|
||||||
|
session.close(CloseReason(CloseReason.Codes.NORMAL, "调试结束"))
|
||||||
|
return@consumeEach
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printOnDebug()
|
||||||
|
} finally {
|
||||||
|
Debug.cancelDebug(true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onClose(
|
|
||||||
code: NanoWSD.WebSocketFrame.CloseCode,
|
|
||||||
reason: String,
|
|
||||||
initiatedByRemote: Boolean
|
|
||||||
) {
|
|
||||||
cancel()
|
|
||||||
Debug.cancelDebug(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onMessage(message: NanoWSD.WebSocketFrame) {
|
|
||||||
launch(IO) {
|
|
||||||
kotlin.runCatching {
|
|
||||||
if (!message.textPayload.isJson()) {
|
|
||||||
send("数据必须为Json格式")
|
|
||||||
close(NanoWSD.WebSocketFrame.CloseCode.NormalClosure, "调试结束", false)
|
|
||||||
return@launch
|
|
||||||
}
|
|
||||||
val debugBean =
|
|
||||||
GSON.fromJsonObject<Map<String, String>>(message.textPayload).getOrNull()
|
|
||||||
if (debugBean != null) {
|
|
||||||
val tag = debugBean["tag"]
|
|
||||||
val key = debugBean["key"]
|
|
||||||
if (tag.isNullOrBlank() || key.isNullOrBlank()) {
|
|
||||||
send(appCtx.getString(R.string.cannot_empty))
|
|
||||||
close(NanoWSD.WebSocketFrame.CloseCode.NormalClosure, "调试结束", false)
|
|
||||||
return@launch
|
|
||||||
}
|
|
||||||
appDb.bookSourceDao.getBookSource(tag)?.let {
|
|
||||||
Debug.callback = this@BookSourceDebugWebSocket
|
|
||||||
Debug.startDebug(this, it, key)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
send("数据必须为Json格式")
|
|
||||||
close(NanoWSD.WebSocketFrame.CloseCode.NormalClosure, "调试结束", false)
|
|
||||||
return@launch
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onPong(pong: NanoWSD.WebSocketFrame) {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onException(exception: IOException) {
|
|
||||||
Debug.cancelDebug(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun printLog(state: Int, msg: String) {
|
override fun printLog(state: Int, msg: String) {
|
||||||
if (state in notPrintState) {
|
if (state in notPrintState) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
runOnIO {
|
launch(Dispatchers.IO) {
|
||||||
runCatching {
|
runCatching {
|
||||||
send(msg)
|
session.send(msg)
|
||||||
if (state == -1 || state == 1000) {
|
if (state == -1 || state == 1000) {
|
||||||
Debug.cancelDebug(true)
|
Debug.cancelDebug(true)
|
||||||
close(NanoWSD.WebSocketFrame.CloseCode.NormalClosure, "调试结束", false)
|
session.close(CloseReason(CloseReason.Codes.NORMAL, "调试结束"))
|
||||||
}
|
}
|
||||||
}.onFailure {
|
}.onFailure {
|
||||||
it.printOnDebug()
|
it.printOnDebug()
|
||||||
|
|||||||
@@ -1,97 +1,70 @@
|
|||||||
package io.legado.app.web.socket
|
package io.legado.app.web.socket
|
||||||
|
|
||||||
|
import io.ktor.server.websocket.*
|
||||||
import fi.iki.elonen.NanoHTTPD
|
import io.ktor.websocket.*
|
||||||
import fi.iki.elonen.NanoWSD
|
|
||||||
import io.legado.app.R
|
import io.legado.app.R
|
||||||
import io.legado.app.data.appDb
|
import io.legado.app.data.appDb
|
||||||
import io.legado.app.model.Debug
|
import io.legado.app.model.Debug
|
||||||
import io.legado.app.utils.*
|
import io.legado.app.utils.*
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
import kotlinx.coroutines.Dispatchers.IO
|
import kotlinx.coroutines.channels.consumeEach
|
||||||
import splitties.init.appCtx
|
import splitties.init.appCtx
|
||||||
|
|
||||||
import java.io.IOException
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* web端订阅源调试
|
* web端订阅源调试
|
||||||
*/
|
*/
|
||||||
class RssSourceDebugWebSocket(handshakeRequest: NanoHTTPD.IHTTPSession) :
|
class RssSourceDebugWebSocket(private val session: DefaultWebSocketServerSession) :
|
||||||
NanoWSD.WebSocket(handshakeRequest),
|
CoroutineScope by session,
|
||||||
CoroutineScope by MainScope(),
|
|
||||||
Debug.Callback {
|
Debug.Callback {
|
||||||
|
|
||||||
private val notPrintState = arrayOf(10, 20, 30, 40)
|
private val notPrintState = arrayOf(10, 20, 30, 40)
|
||||||
|
|
||||||
override fun onOpen() {
|
suspend fun handle() {
|
||||||
launch(IO) {
|
try {
|
||||||
kotlin.runCatching {
|
session.incoming.consumeEach { frame ->
|
||||||
while (isOpen) {
|
if (frame is Frame.Text) {
|
||||||
ping("ping".toByteArray())
|
val text = frame.readText()
|
||||||
delay(30000)
|
if (!text.isJson()) {
|
||||||
}
|
session.send("数据必须为Json格式")
|
||||||
}
|
session.close(CloseReason(CloseReason.Codes.NORMAL, "调试结束"))
|
||||||
}
|
return@consumeEach
|
||||||
}
|
|
||||||
|
|
||||||
override fun onClose(
|
|
||||||
code: NanoWSD.WebSocketFrame.CloseCode,
|
|
||||||
reason: String,
|
|
||||||
initiatedByRemote: Boolean
|
|
||||||
) {
|
|
||||||
cancel()
|
|
||||||
Debug.cancelDebug(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onMessage(message: NanoWSD.WebSocketFrame) {
|
|
||||||
launch(IO) {
|
|
||||||
kotlin.runCatching {
|
|
||||||
if (!message.textPayload.isJson()) {
|
|
||||||
send("数据必须为Json格式")
|
|
||||||
close(NanoWSD.WebSocketFrame.CloseCode.NormalClosure, "调试结束", false)
|
|
||||||
return@launch
|
|
||||||
}
|
|
||||||
val debugBean =
|
|
||||||
GSON.fromJsonObject<Map<String, String>>(message.textPayload).getOrNull()
|
|
||||||
if (debugBean != null) {
|
|
||||||
val tag = debugBean["tag"]
|
|
||||||
if (tag.isNullOrBlank()) {
|
|
||||||
send(appCtx.getString(R.string.cannot_empty))
|
|
||||||
close(NanoWSD.WebSocketFrame.CloseCode.NormalClosure, "调试结束", false)
|
|
||||||
return@launch
|
|
||||||
}
|
}
|
||||||
appDb.rssSourceDao.getByKey(tag)?.let {
|
val debugBean = GSON.fromJsonObject<Map<String, String>>(text).getOrNull()
|
||||||
Debug.callback = this@RssSourceDebugWebSocket
|
if (debugBean != null) {
|
||||||
Debug.startDebug(this, it)
|
val tag = debugBean["tag"]
|
||||||
|
if (tag.isNullOrBlank()) {
|
||||||
|
session.send(appCtx.getString(R.string.cannot_empty))
|
||||||
|
session.close(CloseReason(CloseReason.Codes.NORMAL, "调试结束"))
|
||||||
|
return@consumeEach
|
||||||
|
}
|
||||||
|
appDb.rssSourceDao.getByKey(tag)?.let {
|
||||||
|
Debug.callback = this@RssSourceDebugWebSocket
|
||||||
|
Debug.startDebug(this, it)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printOnDebug()
|
||||||
|
} finally {
|
||||||
|
Debug.cancelDebug(true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPong(pong: NanoWSD.WebSocketFrame) {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onException(exception: IOException) {
|
|
||||||
Debug.cancelDebug(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun printLog(state: Int, msg: String) {
|
override fun printLog(state: Int, msg: String) {
|
||||||
if (state in notPrintState) {
|
if (state in notPrintState) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
runOnIO {
|
launch(Dispatchers.IO) {
|
||||||
runCatching {
|
runCatching {
|
||||||
send(msg)
|
session.send(msg)
|
||||||
if (state == -1 || state == 1000) {
|
if (state == -1 || state == 1000) {
|
||||||
Debug.cancelDebug(true)
|
Debug.cancelDebug(true)
|
||||||
close(NanoWSD.WebSocketFrame.CloseCode.NormalClosure, "调试结束", false)
|
session.close(CloseReason(CloseReason.Codes.NORMAL, "调试结束"))
|
||||||
}
|
}
|
||||||
}.onFailure {
|
}.onFailure {
|
||||||
it.printOnDebug()
|
it.printOnDebug()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,10 +2,9 @@ package io.legado.app.web.utils
|
|||||||
|
|
||||||
import android.content.res.AssetManager
|
import android.content.res.AssetManager
|
||||||
import android.text.TextUtils
|
import android.text.TextUtils
|
||||||
import fi.iki.elonen.NanoHTTPD
|
|
||||||
import splitties.init.appCtx
|
import splitties.init.appCtx
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.IOException
|
import java.io.InputStream
|
||||||
|
|
||||||
|
|
||||||
class AssetsWeb(rootPath: String) {
|
class AssetsWeb(rootPath: String) {
|
||||||
@@ -18,20 +17,19 @@ class AssetsWeb(rootPath: String) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(IOException::class)
|
fun getInputStream(path: String): InputStream? {
|
||||||
fun getResponse(path: String): NanoHTTPD.Response {
|
val path1 = (rootPath + path).replace("/+".toRegex(), File.separator)
|
||||||
var path1 = path
|
return try {
|
||||||
path1 = (rootPath + path1).replace("/+".toRegex(), File.separator)
|
assetManager.open(path1)
|
||||||
val inputStream = assetManager.open(path1)
|
} catch (e: Exception) {
|
||||||
return NanoHTTPD.newChunkedResponse(
|
null
|
||||||
NanoHTTPD.Response.Status.OK,
|
}
|
||||||
getMimeType(path1),
|
|
||||||
inputStream
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getMimeType(path: String): String {
|
fun getMimeType(path: String): String {
|
||||||
val suffix = path.substring(path.lastIndexOf("."))
|
val lastDot = path.lastIndexOf(".")
|
||||||
|
if (lastDot == -1) return "text/html"
|
||||||
|
val suffix = path.substring(lastDot)
|
||||||
return when {
|
return when {
|
||||||
suffix.equals(".html", ignoreCase = true)
|
suffix.equals(".html", ignoreCase = true)
|
||||||
|| suffix.equals(".htm", ignoreCase = true) -> "text/html"
|
|| suffix.equals(".htm", ignoreCase = true) -> "text/html"
|
||||||
@@ -39,6 +37,8 @@ class AssetsWeb(rootPath: String) {
|
|||||||
suffix.equals(".css", ignoreCase = true) -> "text/css"
|
suffix.equals(".css", ignoreCase = true) -> "text/css"
|
||||||
suffix.equals(".ico", ignoreCase = true) -> "image/x-icon"
|
suffix.equals(".ico", ignoreCase = true) -> "image/x-icon"
|
||||||
suffix.equals(".jpg", ignoreCase = true) -> "image/jpg"
|
suffix.equals(".jpg", ignoreCase = true) -> "image/jpg"
|
||||||
|
suffix.equals(".png", ignoreCase = true) -> "image/png"
|
||||||
|
suffix.equals(".svg", ignoreCase = true) -> "image/svg+xml"
|
||||||
else -> "text/html"
|
else -> "text/html"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ lifecycle = "2.10.0"
|
|||||||
glide = "5.0.7"
|
glide = "5.0.7"
|
||||||
gson = "2.14.0"
|
gson = "2.14.0"
|
||||||
jsonPath = "3.0.0"
|
jsonPath = "3.0.0"
|
||||||
|
ktor = "2.3.12"
|
||||||
# issue #3811,不要更新版本,新版引入了一个破坏性变更(详见https://github.com/jhy/jsoup/pull/2017)
|
# issue #3811,不要更新版本,新版引入了一个破坏性变更(详见https://github.com/jhy/jsoup/pull/2017)
|
||||||
# 若要升级请确保相关代码不会受此变更影响(如AnalyzeByJSoup.kt、JsoupXpath库等)
|
# 若要升级请确保相关代码不会受此变更影响(如AnalyzeByJSoup.kt、JsoupXpath库等)
|
||||||
jsoup = "1.16.2"
|
jsoup = "1.16.2"
|
||||||
@@ -180,6 +181,13 @@ markwon-image-glide = { module = "io.noties.markwon:image-glide", version.ref =
|
|||||||
markwon-ext-tables = { module = "io.noties.markwon:ext-tables", version.ref = "markwon" }
|
markwon-ext-tables = { module = "io.noties.markwon:ext-tables", version.ref = "markwon" }
|
||||||
markwon-html = { module = "io.noties.markwon:html", version.ref = "markwon" }
|
markwon-html = { module = "io.noties.markwon:html", version.ref = "markwon" }
|
||||||
|
|
||||||
|
ktor-server-core = { module = "io.ktor:ktor-server-core", version.ref = "ktor" }
|
||||||
|
ktor-server-cio = { module = "io.ktor:ktor-server-cio", version.ref = "ktor" }
|
||||||
|
ktor-server-content-negotiation = { module = "io.ktor:ktor-server-content-negotiation", version.ref = "ktor" }
|
||||||
|
ktor-serialization-gson = { module = "io.ktor:ktor-serialization-gson", version.ref = "ktor" }
|
||||||
|
ktor-server-cors = { module = "io.ktor:ktor-server-cors", version.ref = "ktor" }
|
||||||
|
ktor-server-websockets = { module = "io.ktor:ktor-server-websockets", version.ref = "ktor" }
|
||||||
|
|
||||||
nanohttpd-nanohttpd = { module = "org.nanohttpd:nanohttpd", version.ref = "nanoHttpd" }
|
nanohttpd-nanohttpd = { module = "org.nanohttpd:nanohttpd", version.ref = "nanoHttpd" }
|
||||||
nanohttpd-websocket = { module = "org.nanohttpd:nanohttpd-websocket", version.ref = "nanoHttpd" }
|
nanohttpd-websocket = { module = "org.nanohttpd:nanohttpd-websocket", version.ref = "nanoHttpd" }
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user