Initial commit

This commit is contained in:
2026-08-27 20:13:42 +08:00
commit e2c06f7efe
36 files changed
+5204

No files matched your search

+34
View File
@@ -0,0 +1,34 @@
plugins {
kotlin("multiplatform")
kotlin("plugin.serialization")
}
kotlin {
listOf(
linuxX64(),
mingwX64()
).forEach { it.binaries.executable { entryPoint = "snippets.main" } }
val ktorVersion = "3.5.2"
sourceSets {
commonMain.dependencies {
implementation("io.ktor:ktor-server-core:${ktorVersion}")
implementation("io.ktor:ktor-server-cio:${ktorVersion}")
implementation("io.ktor:ktor-server-content-negotiation:${ktorVersion}")
implementation("io.ktor:ktor-serialization-kotlinx-json:${ktorVersion}")
implementation("io.ktor:ktor-server-cors:${ktorVersion}")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.11.0")
implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.8.0")
implementation("org.jetbrains.kotlinx:kotlinx-io-core:0.9.1")
}
linuxX64Main.dependencies {
}
mingwX64Main.dependencies {
}
}
}
@@ -0,0 +1,14 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/8/27
*/
package snippets
import kotlinx.io.files.Path
import kotlinx.io.files.SystemFileSystem
val dataDir = Path("./data/snippets/")
.apply { if (!SystemFileSystem.exists(this)) SystemFileSystem.createDirectories(this) }
@@ -0,0 +1,16 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/8/27
*/
package snippets.data
import kotlinx.serialization.Serializable
@Serializable
data class DirStats(
val fileCount: Long,
val totalBytes: Long,
)
@@ -0,0 +1,16 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/8/27
*/
package snippets.data
import kotlinx.serialization.Serializable
@Serializable
data class SnippetContent(
val content: String,
val filename: String,
)
@@ -0,0 +1,18 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/8/27
*/
package snippets.data
import kotlinx.serialization.Serializable
import kotlin.time.Instant
@Serializable
data class SnippetMeta(
val createdAt: Instant,
val filename: String,
val content: String,
)
@@ -0,0 +1,24 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/8/27
*/
package snippets
import io.ktor.server.application.Application
import io.ktor.server.cio.CIO
import io.ktor.server.engine.embeddedServer
import snippets.routing.registerSnippetsRouting
import snippets.routing.registerStatusRouting
fun main() {
embeddedServer(CIO, port = 6868, module = Application::module)
.start(true)
}
fun Application.module() {
registerSnippetsRouting()
registerStatusRouting()
}
@@ -0,0 +1,86 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/8/27
*/
package snippets.routing
import io.ktor.http.*
import io.ktor.serialization.kotlinx.json.*
import io.ktor.server.application.*
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.utils.io.core.*
import kotlinx.io.Buffer
import kotlinx.io.buffered
import kotlinx.io.files.Path
import kotlinx.io.files.SystemFileSystem
import kotlinx.io.readString
import snippets.data.SnippetContent
import snippets.data.SnippetMeta
import snippets.dataDir
import snippets.util.encodeJson
import snippets.util.fromJson
import kotlin.time.Clock
fun Application.registerSnippetsRouting() {
install(ContentNegotiation) {
json()
}
install(CORS) {
allowMethod(HttpMethod.Options)
allowMethod(HttpMethod.Get)
allowMethod(HttpMethod.Post)
allowMethod(HttpMethod.Put)
allowMethod(HttpMethod.Delete)
allowHeader(HttpHeaders.ContentType)
allowHeader(HttpHeaders.Authorization)
allowHeader(HttpHeaders.AccessControlAllowOrigin)
anyHost()
}
routing {
post("/api/v1/snippets") {
val payload = call.receive<SnippetContent>()
val snippetId = generateShortId()
val meta = SnippetMeta(Clock.System.now(), payload.filename, payload.content)
.encodeJson().toByteArray()
val snippetPath = Path(dataDir, snippetId).apply { SystemFileSystem.createDirectories(this) }
SystemFileSystem.sink(Path(snippetPath, "meta.json")).use {
val buffer = Buffer().apply { write(meta) }
it.write(buffer, meta.size.toLong())
}
call.respond(status = HttpStatusCode.Created, message = mapOf("snippet_id" to snippetId))
}
get("/{snippetId}") {
val mode = call.queryParameters["mode"] ?: "serialized"
val snippetId = call.parameters["snippetId"] ?: return@get call.respond(HttpStatusCode.BadRequest)
val snippetPath = Path(dataDir, snippetId)
if (!SystemFileSystem.exists(snippetPath)) {
call.respond(HttpStatusCode.NotFound)
return@get
}
val meta = SystemFileSystem.source(Path(snippetPath, "meta.json"))
.use { it.buffered().readString().fromJson<SnippetMeta>() }
if (mode == "raw") call.respond(meta.content) else call.respond(meta)
}
}
}
fun generateShortId(length: Int = 6): String {
val charPool = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
return (1..length)
.map { charPool.random() }
.joinToString("")
}
@@ -0,0 +1,47 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/8/27
*/
package snippets.routing
import io.ktor.http.HttpStatusCode
import io.ktor.server.application.*
import io.ktor.server.response.respond
import io.ktor.server.routing.*
import kotlinx.io.files.Path
import kotlinx.io.files.SystemFileSystem
import snippets.data.DirStats
import snippets.dataDir
fun Application.registerStatusRouting() {
routing {
get("/api/v1/status") {
call.respond(HttpStatusCode.OK, getDirStats(dataDir))
}
}
}
fun getDirStats(dirPath: Path): DirStats {
if (!SystemFileSystem.exists(dirPath)) return DirStats(0, 0)
var count = 0L
var bytes = 0L
SystemFileSystem.list(dirPath).forEach { path ->
val metadata = SystemFileSystem.metadataOrNull(path) ?: return@forEach
when {
metadata.isRegularFile -> {
count++
bytes += metadata.size
}
metadata.isDirectory -> {
val subStats = getDirStats(path)
count += subStats.fileCount
bytes += subStats.totalBytes
}
}
}
return DirStats(count, bytes)
}
@@ -0,0 +1,28 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/8/27
*/
package snippets.util
import kotlinx.serialization.json.Json
val json: Json = Json {
ignoreUnknownKeys = true
explicitNulls = false
classDiscriminator = "_json_type_"
encodeDefaults = true
coerceInputValues = true
decodeEnumsCaseInsensitive = true
isLenient = true
}
inline fun <reified T> T.encodeJson(): String {
return json.encodeToString(this)
}
inline fun <reified T> String.fromJson(): T {
return json.decodeFromString<T>(this)
}