Add initial archiving service web interface

Signed-off-by: Graham <gpe@openrs2.org>
bzip2
Graham 4 years ago
parent 6e9ba80d0c
commit 47127113f4
  1. 4
      archive/build.gradle.kts
  2. 4
      archive/src/main/kotlin/org/openrs2/archive/ArchiveCommand.kt
  3. 44
      archive/src/main/kotlin/org/openrs2/archive/cache/CacheExporter.kt
  4. 41
      archive/src/main/kotlin/org/openrs2/archive/web/CachesController.kt
  5. 29
      archive/src/main/kotlin/org/openrs2/archive/web/KeysController.kt
  6. 13
      archive/src/main/kotlin/org/openrs2/archive/web/WebCommand.kt
  7. 41
      archive/src/main/kotlin/org/openrs2/archive/web/WebServer.kt
  8. 29
      archive/src/main/resources/org/openrs2/archive/templates/caches/index.html
  9. 3
      buildSrc/src/main/kotlin/Versions.kt

@ -20,9 +20,13 @@ dependencies {
implementation(project(":protocol")) implementation(project(":protocol"))
implementation(project(":util")) implementation(project(":util"))
implementation("com.google.guava:guava:${Versions.guava}") implementation("com.google.guava:guava:${Versions.guava}")
implementation("io.ktor:ktor-server-netty:${Versions.ktor}")
implementation("io.ktor:ktor-thymeleaf:${Versions.ktor}")
implementation("org.flywaydb:flyway-core:${Versions.flyway}") implementation("org.flywaydb:flyway-core:${Versions.flyway}")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:${Versions.kotlinCoroutines}") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:${Versions.kotlinCoroutines}")
implementation("org.postgresql:postgresql:${Versions.postgres}") implementation("org.postgresql:postgresql:${Versions.postgres}")
implementation("org.thymeleaf:thymeleaf:${Versions.thymeleaf}")
implementation("org.thymeleaf.extras:thymeleaf-extras-java8time:${Versions.thymeleafJava8Time}")
} }
publishing { publishing {

@ -5,6 +5,7 @@ import com.github.ajalt.clikt.core.subcommands
import org.openrs2.archive.cache.CacheCommand import org.openrs2.archive.cache.CacheCommand
import org.openrs2.archive.key.KeyCommand import org.openrs2.archive.key.KeyCommand
import org.openrs2.archive.name.NameCommand import org.openrs2.archive.name.NameCommand
import org.openrs2.archive.web.WebCommand
public fun main(args: Array<String>): Unit = ArchiveCommand().main(args) public fun main(args: Array<String>): Unit = ArchiveCommand().main(args)
@ -13,7 +14,8 @@ public class ArchiveCommand : NoOpCliktCommand(name = "archive") {
subcommands( subcommands(
CacheCommand(), CacheCommand(),
KeyCommand(), KeyCommand(),
NameCommand() NameCommand(),
WebCommand()
) )
} }
} }

@ -6,6 +6,7 @@ import org.openrs2.buffer.use
import org.openrs2.cache.Js5Archive import org.openrs2.cache.Js5Archive
import org.openrs2.cache.Store import org.openrs2.cache.Store
import org.openrs2.db.Database import org.openrs2.db.Database
import java.time.Instant
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
@ -14,6 +15,49 @@ public class CacheExporter @Inject constructor(
private val database: Database, private val database: Database,
private val alloc: ByteBufAllocator private val alloc: ByteBufAllocator
) { ) {
public data class Cache(
val id: Long,
val whirlpool: ByteArray,
val game: String,
val build: Int?,
val timestamp: Instant?
)
public suspend fun list(): List<Cache> {
return database.execute { connection ->
connection.prepareStatement(
"""
SELECT c.id, c.whirlpool, g.name, m.build, m.timestamp
FROM master_indexes m
JOIN games g ON g.id = m.game_id
JOIN containers c ON c.id = m.container_id
ORDER BY g.name ASC, m.build ASC, m.timestamp ASC
""".trimIndent()
).use { stmt ->
stmt.executeQuery().use { rows ->
val caches = mutableListOf<Cache>()
while (rows.next()) {
val id = rows.getLong(1)
val whirlpool = rows.getBytes(2)
val game = rows.getString(3)
var build: Int? = rows.getInt(4)
if (rows.wasNull()) {
build = null
}
val timestamp = rows.getTimestamp(5)?.toInstant()
caches += Cache(id, whirlpool, game, build, timestamp)
}
caches
}
}
}
}
public suspend fun export(id: Long, store: Store) { public suspend fun export(id: Long, store: Store) {
// TODO(gpe): think about what to do if there is a collision // TODO(gpe): think about what to do if there is a collision
database.execute { connection -> database.execute { connection ->

@ -0,0 +1,41 @@
package org.openrs2.archive.web
import io.ktor.application.ApplicationCall
import io.ktor.http.ContentType
import io.ktor.http.HttpStatusCode
import io.ktor.response.header
import io.ktor.response.respond
import io.ktor.response.respondOutputStream
import io.ktor.thymeleaf.ThymeleafContent
import io.netty.buffer.ByteBufAllocator
import org.openrs2.archive.cache.CacheExporter
import org.openrs2.cache.DiskStoreZipWriter
import java.util.zip.ZipOutputStream
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
public class CachesController @Inject constructor(
private val exporter: CacheExporter,
private val alloc: ByteBufAllocator
) {
public suspend fun index(call: ApplicationCall) {
val caches = exporter.list()
call.respond(ThymeleafContent("caches/index.html", mapOf("caches" to caches)))
}
public suspend fun export(call: ApplicationCall) {
val id = call.parameters["id"]?.toLongOrNull()
if (id == null) {
call.respond(HttpStatusCode.NotFound)
return
}
call.response.header("Content-Disposition", "attachment; filename=\"cache.zip\"")
call.respondOutputStream(contentType = ContentType.Application.Zip) {
DiskStoreZipWriter(ZipOutputStream(this), alloc = alloc).use { store ->
exporter.export(id, store)
}
}
}
}

@ -0,0 +1,29 @@
package org.openrs2.archive.web
import io.ktor.application.ApplicationCall
import io.ktor.http.HttpStatusCode
import io.ktor.response.respond
import org.openrs2.archive.key.KeyImporter
import org.openrs2.crypto.XteaKey
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
public class KeysController @Inject constructor(
private val importer: KeyImporter
) {
public suspend fun create(call: ApplicationCall) {
val k0 = call.request.queryParameters["key1"]?.toIntOrNull()
val k1 = call.request.queryParameters["key2"]?.toIntOrNull()
val k2 = call.request.queryParameters["key3"]?.toIntOrNull()
val k3 = call.request.queryParameters["key4"]?.toIntOrNull()
if (k0 == null || k1 == null || k2 == null || k3 == null) {
call.respond(HttpStatusCode.BadRequest)
return
}
importer.import(listOf(XteaKey(k0, k1, k2, k3)))
call.respond(HttpStatusCode.NoContent)
}
}

@ -0,0 +1,13 @@
package org.openrs2.archive.web
import com.github.ajalt.clikt.core.CliktCommand
import com.google.inject.Guice
import org.openrs2.archive.ArchiveModule
public class WebCommand : CliktCommand(name = "web") {
override fun run() {
val injector = Guice.createInjector(ArchiveModule)
val server = injector.getInstance(WebServer::class.java)
server.start()
}
}

@ -0,0 +1,41 @@
package org.openrs2.archive.web
import io.ktor.application.call
import io.ktor.application.install
import io.ktor.routing.get
import io.ktor.routing.routing
import io.ktor.server.engine.embeddedServer
import io.ktor.server.netty.Netty
import io.ktor.thymeleaf.Thymeleaf
import org.thymeleaf.extras.java8time.dialect.Java8TimeDialect
import org.thymeleaf.templatemode.TemplateMode
import org.thymeleaf.templateresolver.ClassLoaderTemplateResolver
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
public class WebServer @Inject constructor(
private val cachesController: CachesController,
private val keysController: KeysController
) {
public fun start() {
embeddedServer(Netty, port = 8000) {
install(Thymeleaf) {
addDialect(Java8TimeDialect())
setTemplateResolver(ClassLoaderTemplateResolver().apply {
prefix = "/org/openrs2/archive/templates/"
templateMode = TemplateMode.HTML
})
}
routing {
get("/caches") { cachesController.index(call) }
get("/caches/{id}.zip") { cachesController.export(call) }
// ideally we'd use POST /keys here, but I want to be compatible with the RuneLite/OpenOSRS API
get("/keys/submit") { keysController.create(call) }
}
}.start(wait = true)
}
}

@ -0,0 +1,29 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" lang="en">
<body>
<table>
<thead>
<tr>
<th>Master Index Digest</th>
<th>Game</th>
<th>Build</th>
<th>Timestamp</th>
<th></th>
</tr>
</thead>
<tbody>
<!--/*@thymesVar id="caches" type="java.util.List<org.openrs2.archive.cache.CacheExporter.Cache>"*/-->
<tr th:each="cache : ${caches}">
<td th:text="${@io.netty.buffer.ByteBufUtil@hexDump(cache.whirlpool).substring(0, 8)}">00000000</td>
<td th:text="${cache.game}">runescape</td>
<td th:text="${cache.build}">550</td>
<td th:text="${#temporals.formatISO(cache.timestamp)}"></td>
<td>
<a th:href="${'/caches/' + cache.id + '.zip'}">Download</a>
</td>
</tr>
</tbody>
</table>
</body>
</html>

@ -22,12 +22,15 @@ object Versions {
const val kotlin = "1.4.21-2" const val kotlin = "1.4.21-2"
const val kotlinCoroutines = "1.4.2" const val kotlinCoroutines = "1.4.2"
const val kotlinter = "3.3.0" const val kotlinter = "3.3.0"
const val ktor = "1.5.1"
const val logback = "1.2.3" const val logback = "1.2.3"
const val netty = "4.1.58.Final" const val netty = "4.1.58.Final"
const val nettyIoUring = "0.0.3.Final" const val nettyIoUring = "0.0.3.Final"
const val openrs2Natives = "3.2.0" const val openrs2Natives = "3.2.0"
const val postgres = "42.2.18" const val postgres = "42.2.18"
const val shadowPlugin = "6.1.0" const val shadowPlugin = "6.1.0"
const val thymeleaf = "3.0.12.RELEASE"
const val thymeleafJava8Time = "3.0.4.RELEASE"
const val versionsPlugin = "0.36.0" const val versionsPlugin = "0.36.0"
const val xz = "1.8" const val xz = "1.8"
} }

Loading…
Cancel
Save