Skip to content

Commit eb2f868

Browse files
committed
chore: ktor file browser changes
1 parent 583c53f commit eb2f868

File tree

10 files changed

+317
-25
lines changed

10 files changed

+317
-25
lines changed

backend/jvm/build.gradle.kts

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
1-
import common.githubUser
2-
import common.javaVersion
3-
import common.jvmArguments
4-
import common.tmp
1+
import com.google.devtools.ksp.gradle.KspTask
2+
import common.*
53

64
plugins {
75
plugins.kotlin.jvm
@@ -10,6 +8,7 @@ plugins {
108
alias(libs.plugins.ktor)
119
alias(libs.plugins.exposed)
1210
com.google.cloud.tools.jib
11+
gg.jte.gradle
1312
}
1413

1514
description = "Ktor backend jvm application"
@@ -52,6 +51,13 @@ jib {
5251
containerizingMode = "packaged"
5352
}
5453

54+
jte {
55+
contentType = gg.jte.ContentType.Html
56+
sourceDirectory =
57+
sourceSets.main.map { it.resources.srcDirs.first().resolve("templates").toPath() }
58+
generate()
59+
}
60+
5561
exposedCodeGeneratorConfig { outputDirectory.set(file("src/main/kotlin/dev/suresh")) }
5662

5763
// Configuration to copy webapp to resources
@@ -63,8 +69,13 @@ tasks {
6369
from(webapp)
6470
into(processResources.map { it.destinationDir.resolve(webapp.name) })
6571
}
72+
73+
// Copy webapp to resources
6674
processResources { dependsOn(copyWebApp) }
6775

76+
// Makes sure jte is generated before compilation
77+
withType<KspTask>().configureEach { dependsOn(generateJte) }
78+
6879
// publish { finalizedBy(jibDockerBuild) }
6980
}
7081

@@ -91,13 +102,15 @@ dependencies {
91102
implementation(libs.ktor.server.auth)
92103
implementation(libs.ktor.server.auth.jwt)
93104
implementation(libs.ktor.serialization.json)
105+
94106
// Client dependencies
95107
implementation(libs.ktor.client.java)
96108
implementation(libs.ktor.client.content.negotiation)
97109
implementation(libs.ktor.client.encoding)
98110
implementation(libs.ktor.client.logging)
99111
implementation(libs.ktor.client.resources)
100112
implementation(libs.ktor.client.auth)
113+
101114
// Database
102115
implementation(libs.exposed.core)
103116
implementation(libs.exposed.jdbc)
@@ -106,18 +119,29 @@ dependencies {
106119
implementation(libs.postgresql)
107120
implementation(libs.hikariCP)
108121
implementation(libs.sherlock.sql)
122+
123+
// Templating
124+
implementation(libs.jte.runtime)
125+
// compileOnly(libs.jte.kotlin)
126+
implementation(libs.ktor.server.html)
127+
implementation(libs.kotlinx.html)
128+
implementation(kotlinw("css"))
129+
109130
// Monitoring
110131
implementation(libs.ktor.cohort.core)
111132
implementation(libs.ktor.cohort.hikari)
112133
implementation(libs.micrometer.prometheus)
113134
implementation(libs.ap.loader.all)
135+
114136
// Logging
115137
implementation(libs.logback.classic)
138+
116139
// Testing
117-
testImplementation(platform(libs.testcontainers.bom))
118140
testImplementation(libs.ktor.server.tests)
119141
testImplementation(libs.testcontainers.junit5)
120142
testImplementation(libs.testcontainers.postgresql)
143+
testImplementation(libs.testcontainers.k3s)
144+
testImplementation(libs.kubernetes.client)
121145
testImplementation(libs.konsist)
122146

123147
// Copy web app browserDist

backend/jvm/src/main/kotlin/dev/suresh/routes/Mgmt.kt

Lines changed: 116 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,16 @@ import dev.suresh.plugins.debug
66
import dev.suresh.runOnVirtualThread
77
import io.ktor.http.*
88
import io.ktor.server.application.*
9+
import io.ktor.server.http.content.*
10+
import io.ktor.server.request.*
911
import io.ktor.server.response.*
1012
import io.ktor.server.routing.*
13+
import java.io.File
1114
import java.io.PrintStream
1215
import java.lang.management.ManagementFactory
1316
import jdk.jfr.Configuration
1417
import jdk.jfr.consumer.RecordingStream
15-
import kotlin.io.path.deleteIfExists
16-
import kotlin.io.path.name
17-
import kotlin.io.path.pathString
18+
import kotlin.io.path.*
1819
import kotlin.time.Duration.Companion.milliseconds
1920
import kotlin.time.Duration.Companion.minutes
2021
import kotlin.time.toJavaDuration
@@ -38,12 +39,124 @@ val profiler: AsyncProfiler? by lazy {
3839
ap
3940
}
4041

42+
val docRoot = Path(System.getProperty("java.io.tmpdir"))
43+
4144
fun Route.mgmtRoutes() {
4245

46+
staticFiles(remotePath = "/tmp", dir = docRoot.toFile())
47+
4348
get("/info") {
4449
call.respond(ScopedValue.where(DEBUG, call.debug).get { jvmRuntimeInfo(DEBUG.get()) })
4550
}
4651

52+
get("/browse/{param...}") {
53+
val reqPath =
54+
Path(call.parameters.getAll("param").orEmpty().joinToString(File.separator)).normalize()
55+
println("reqPath: $reqPath")
56+
val path = docRoot.resolve(reqPath)
57+
println("path: $path")
58+
when {
59+
path.exists() -> {
60+
when {
61+
path.isDirectory() -> {
62+
call.respondText(
63+
"""
64+
<!DOCTYPE html>
65+
<html>
66+
<head>
67+
<title>File Browser</title>
68+
<style>
69+
body {
70+
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
71+
margin: 0;
72+
padding: 0;
73+
background-color: #FFFFFF;
74+
color: #333;
75+
}
76+
77+
.container {
78+
max-width: 900px;
79+
margin: auto;
80+
padding: 1em;
81+
}
82+
83+
h2 {
84+
font-weight: 500;
85+
margin-bottom: 1em;
86+
}
87+
88+
ul {
89+
list-style: none;
90+
padding: 0;
91+
margin: 0;
92+
}
93+
94+
ul li {
95+
display: flex;
96+
align-items: center;
97+
background-color: #F9F9F9;
98+
padding: 0.5em;
99+
margin: 0.2em 0;
100+
border-radius: 3px;
101+
transition: background 0.15s ease-in-out;
102+
}
103+
104+
ul li:hover {
105+
background-color: #E9E9E9;
106+
}
107+
108+
ul li a {
109+
text-decoration: none;
110+
color: #333;
111+
}
112+
113+
ul li i {
114+
margin-right: 0.5em;
115+
}
116+
117+
.fa-folder::before {
118+
content: url("data:image/svg+xml,%3Csvg fill='%23000000' viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg' width='24' height='24'%3E%3Cpath d='M19 5.5h-6.28l-0.32 -1a3 3 0 0 0 -2.84 -2H5a3 3 0 0 0 -3 3v13a3 3 0 0 0 3 3h14a3 3 0 0 0 3 -3v-10a3 3 0 0 0 -3 -3Zm1 13a1 1 0 0 1 -1 1H5a1 1 0 0 1 -1 -1v-13a1 1 0 0 1 1 -1h4.56a1 1 0 0 1 0.95 0.68l0.54 1.64a1 1 0 0 0 0.95 0.68h7a1 1 0 0 1 1 1Z'/%3E%3C/svg%3E");
119+
}
120+
.fa-file::before {
121+
content: url("data:image/svg+xml,%3Csvg fill='%23000000' viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg' width='24' height='24'%3E%3Cpath d='M20 8.94a1.31 1.31 0 0 0 -0.06 -0.27v-0.09a1.07 1.07 0 0 0 -0.19 -0.28l-6 -6a1.07 1.07 0 0 0 -0.28 -0.19h-0.09L13.06 2H7a3 3 0 0 0 -3 3v14a3 3 0 0 0 3 3h10a3 3 0 0 0 3 -3V8.94Zm-6 -3.53L16.59 8H14ZM18 19a1 1 0 0 1 -1 1H7a1 1 0 0 1 -1 -1V5a1 1 0 0 1 1 -1h5v5a1 1 0 0 0 1 1h5Z'/%3E%3C/svg%3E");
122+
}
123+
</style>
124+
</head>
125+
<body>
126+
<div class="container">
127+
<h2>Directory listing for: ${reqPath}</h2>
128+
<ul>
129+
${
130+
path.toFile().list().orEmpty().joinToString("\n") { file ->
131+
val icon = if (File(file).isDirectory) "<i class=\"fa-folder\"></i>" else "<i class=\"fa-file\"></i>"
132+
"<li>${icon}<a href=\"/browse/${file}\">${file}</a></li>"
133+
}
134+
}
135+
</ul>
136+
</div>
137+
</body>
138+
</html>
139+
"""
140+
.trimIndent(),
141+
contentType = ContentType.Text.Html,
142+
status = HttpStatusCode.OK)
143+
}
144+
else -> call.respondFile(path.toFile())
145+
}
146+
}
147+
else ->
148+
call.respondText(
149+
"""
150+
|<h2>File not found</h2>
151+
|$reqPath <p>
152+
|"""
153+
.trimMargin(),
154+
contentType = ContentType.Text.Html,
155+
status = HttpStatusCode.NotFound,
156+
)
157+
}
158+
}
159+
47160
get("/profile") {
48161
// Run the blocking operation on virtual thread and make sure
49162
// only one profile operation is running at a time.
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
@import java.nio.file.Path
2+
@import kotlin.io.path.*
3+
4+
@param docDir: Path
5+
@param dir: Path
6+
7+
<!DOCTYPE html>
8+
<html lang="en">
9+
<head>
10+
<title>File Browser</title>
11+
<style>
12+
body {
13+
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
14+
margin: 0;
15+
padding: 0;
16+
background-color: #FFFFFF;
17+
color: #333;
18+
}
19+
20+
.container {
21+
max-width: 900px;
22+
margin: auto;
23+
padding: 1em;
24+
}
25+
26+
h2 {
27+
font-weight: 500;
28+
margin-bottom: 1em;
29+
}
30+
31+
ul {
32+
list-style: none;
33+
padding: 0;
34+
margin: 0;
35+
}
36+
37+
ul li {
38+
display: flex;
39+
align-items: center;
40+
background-color: #F9F9F9;
41+
padding: 0.5em;
42+
margin: 0.2em 0;
43+
border-radius: 3px;
44+
transition: background 0.15s ease-in-out;
45+
}
46+
47+
ul li:hover {
48+
background-color: #E9E9E9;
49+
}
50+
51+
ul li a {
52+
text-decoration: none;
53+
color: #333;
54+
}
55+
56+
ul li i {
57+
margin-right: 0.5em;
58+
}
59+
60+
.fa-folder::before {
61+
content: url("data:image/svg+xml,%3Csvg fill='%23000000' viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg' width='24' height='24'%3E%3Cpath d='M19 5.5h-6.28l-0.32 -1a3 3 0 0 0 -2.84 -2H5a3 3 0 0 0 -3 3v13a3 3 0 0 0 3 3h14a3 3 0 0 0 3 -3v-10a3 3 0 0 0 -3 -3Zm1 13a1 1 0 0 1 -1 1H5a1 1 0 0 1 -1 -1v-13a1 1 0 0 1 1 -1h4.56a1 1 0 0 1 0.95 0.68l0.54 1.64a1 1 0 0 0 0.95 0.68h7a1 1 0 0 1 1 1Z'/%3E%3C/svg%3E");
62+
}
63+
64+
.fa-file::before {
65+
content: url("data:image/svg+xml,%3Csvg fill='%23000000' viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg' width='24' height='24'%3E%3Cpath d='M20 8.94a1.31 1.31 0 0 0 -0.06 -0.27v-0.09a1.07 1.07 0 0 0 -0.19 -0.28l-6 -6a1.07 1.07 0 0 0 -0.28 -0.19h-0.09L13.06 2H7a3 3 0 0 0 -3 3v14a3 3 0 0 0 3 3h10a3 3 0 0 0 3 -3V8.94Zm-6 -3.53L16.59 8H14ZM18 19a1 1 0 0 1 -1 1H7a1 1 0 0 1 -1 -1V5a1 1 0 0 1 1 -1h5v5a1 1 0 0 0 1 1h5Z'/%3E%3C/svg%3E");
66+
}
67+
</style>
68+
</head>
69+
<body>
70+
<div class="container">
71+
<h2>Directory listing for: ${dir.relativeToOrSelf(docDir).pathString}</h2>
72+
<ul>
73+
74+
</ul>
75+
</div>
76+
</body>
77+
</html>
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
package dev.suresh
2+
3+
import io.kubernetes.client.openapi.apis.CoreV1Api
4+
import io.kubernetes.client.util.Config
5+
import kotlin.test.Test
6+
import kotlin.test.assertTrue
7+
import org.junit.jupiter.api.Disabled
8+
import org.slf4j.LoggerFactory
9+
import org.testcontainers.containers.output.Slf4jLogConsumer
10+
import org.testcontainers.junit.jupiter.Container
11+
import org.testcontainers.junit.jupiter.Testcontainers
12+
import org.testcontainers.k3s.K3sContainer
13+
import org.testcontainers.utility.DockerImageName
14+
15+
@Testcontainers
16+
@Disabled
17+
class K8STests {
18+
19+
companion object {
20+
21+
val logger = LoggerFactory.getLogger("K8S")
22+
23+
@Container
24+
val k3s =
25+
K3sContainer(DockerImageName.parse("rancher/k3s:latest"))
26+
.withLogConsumer(Slf4jLogConsumer(logger))
27+
.withReuse(true)
28+
// .withCommand("server", "--disable=traefik",
29+
// "--tls-san=${DockerClientFactory.instance().dockerHostIpAddress()}")
30+
}
31+
32+
@Test
33+
fun testK8S() {
34+
assertTrue(k3s.isRunning)
35+
}
36+
37+
@Test
38+
fun testK8SClient() {
39+
val client = Config.fromConfig(k3s.kubeConfigYaml.reader())
40+
val api = CoreV1Api(client)
41+
api.listNode(
42+
/* pretty = */ null,
43+
/* allowWatchBookmarks = */ null,
44+
/* _continue = */ null,
45+
/* fieldSelector = */ null,
46+
/* labelSelector = */ null,
47+
/* limit = */ null,
48+
/* resourceVersion = */ null,
49+
/* resourceVersionMatch = */ null,
50+
/* sendInitialEvents = */ null,
51+
/* timeoutSeconds = */ null,
52+
/* watch = */ null)
53+
.items
54+
.forEach { println("K8S Node: ${it.metadata?.name}") }
55+
}
56+
}

common/src/commonMain/kotlin/dev/suresh/Platform.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,11 @@ interface Platform {
8080
"${Instant.fromEpochSeconds(epochSeconds).toLocalDateTime(TimeZone.currentSystemDefault())} $tzShortId"
8181
}
8282

83+
// Expect classes are not stable
84+
// expect class Platform {
85+
// val name: String
86+
// }
87+
8388
/** Common JSON instance for serde of JSON data. */
8489
val json by lazy {
8590
Json {

gradle/build-logic/common-plugins/src/main/kotlin/common/MultiPlatformExtns.kt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,9 @@ fun KotlinMultiplatformExtension.commonTarget() {
2626
languageSettings { configureKotlinLang() }
2727
// Apply multiplatform library bom to all source sets
2828
dependencies {
29-
implementation(project.dependencies.platform(libs.kotlin.bom))
30-
implementation(project.dependencies.platform(libs.ktor.bom))
31-
implementation(project.dependencies.platform(libs.kotlin.wrappers.bom))
29+
api(project.dependencies.platform(libs.kotlin.bom))
30+
api(project.dependencies.platform(libs.ktor.bom))
31+
api(project.dependencies.platform(libs.kotlin.wrappers.bom))
3232
}
3333
}
3434

0 commit comments

Comments
 (0)