Add binary file handling with hash-based storage and retrieval

Introduced hash-based storage for binary files in `RequestHandler`, with subdirectory organization. Added methods for creating hashes, saving, and retrieving files. Enabled binary file transmission via WebSocket commands. Updated `Settings` to support configurable data directory.
This commit is contained in:
2025-06-10 20:07:11 +02:00
parent 3746ced387
commit 0bdaa5c94f
2 changed files with 97 additions and 4 deletions

View File

@@ -8,10 +8,11 @@ object Settings {
var port = 9004 var port = 9004
var connectionTimeout = 30000 var connectionTimeout = 30000
var jdbcStatsPort = 6001 var jdbcStatsPort = 6001
var dataDir = "data"
var jdbcDriver = "nl.astraeus.jdbc.Driver" var jdbcDriver = "nl.astraeus.jdbc.Driver"
val jdbcConnectionUrl val jdbcConnectionUrl
get() = "jdbc:stat:webServerPort=$jdbcStatsPort:jdbc:sqlite:data/vst.db" get() = "jdbc:stat:webServerPort=$jdbcStatsPort:jdbc:sqlite:$dataDir/vst.db"
var jdbcUser = "sa" var jdbcUser = "sa"
var jdbcPassword = "" var jdbcPassword = ""
@@ -35,6 +36,8 @@ object Settings {
port = properties.getProperty("port", port.toString()).toInt() port = properties.getProperty("port", port.toString()).toInt()
jdbcStatsPort = properties.getProperty("jdbcStatsPort", jdbcStatsPort.toString()).toInt() jdbcStatsPort = properties.getProperty("jdbcStatsPort", jdbcStatsPort.toString()).toInt()
dataDir = properties.getProperty("dataDir", dataDir)
connectionTimeout = connectionTimeout =
properties.getProperty("connectionTimeout", connectionTimeout.toString()).toInt() properties.getProperty("connectionTimeout", connectionTimeout.toString()).toInt()
jdbcDriver = properties.getProperty("jdbcDriver", jdbcDriver) jdbcDriver = properties.getProperty("jdbcDriver", jdbcDriver)

View File

@@ -17,10 +17,14 @@ import io.undertow.websockets.core.BufferedTextMessage
import io.undertow.websockets.core.WebSocketChannel import io.undertow.websockets.core.WebSocketChannel
import io.undertow.websockets.core.WebSockets import io.undertow.websockets.core.WebSockets
import io.undertow.websockets.spi.WebSocketHttpExchange import io.undertow.websockets.spi.WebSocketHttpExchange
import nl.astraeus.vst.base.Settings
import nl.astraeus.vst.base.db.Database import nl.astraeus.vst.base.db.Database
import nl.astraeus.vst.base.db.PatchDao import nl.astraeus.vst.base.db.PatchDao
import nl.astraeus.vst.base.db.PatchEntity import nl.astraeus.vst.base.db.PatchEntity
import java.io.File
import java.nio.ByteBuffer
import java.nio.file.Paths import java.nio.file.Paths
import java.security.MessageDigest
object VstSessionConfig { object VstSessionConfig {
val config = SessionCookieConfig() val config = SessionCookieConfig()
@@ -34,6 +38,57 @@ class WebsocketHandler(
val session: Session? val session: Session?
) : AbstractReceiveListener(), WebSocketConnectionCallback { ) : AbstractReceiveListener(), WebSocketConnectionCallback {
companion object {
// Ensure the data directory exists
private val dataDir = File(Settings.dataDir).apply {
if (!exists()) {
mkdirs()
}
}
fun fileExists(hash: String): Boolean {
check(hash.length == 64) { "Hash must be 64 characters long" }
var currentDir = dataDir
var remaining = hash
while(remaining.length > 8) {
val subDir = remaining.substring(0, 8)
currentDir = File(currentDir, subDir)
if (!currentDir.exists()) {
return false
}
remaining = remaining.substring(8)
}
return File(currentDir, remaining).exists()
}
// Get file from hash, using subdirectories based on hash
fun getFileFromHash(hash: String): File {
check(hash.length == 64) { "Hash must be 64 characters long" }
var currentDir = dataDir
var remaining = hash
while(remaining.length > 8) {
val subDir = remaining.substring(0, 8)
currentDir = File(currentDir, subDir)
if (!currentDir.exists()) {
currentDir.mkdirs()
}
remaining = remaining.substring(8)
}
return File(currentDir, remaining)
}
// Create SHA-1 hash from binary data
fun createHashFromBytes(bytes: ByteArray): String {
val md = MessageDigest.getInstance("SHA-256")
val digest = md.digest(bytes)
return digest.joinToString("") { "%02x".format(it) }
}
}
override fun onConnect(exchange: WebSocketHttpExchange, channel: WebSocketChannel) { override fun onConnect(exchange: WebSocketHttpExchange, channel: WebSocketChannel) {
channel.receiveSetter.set(this) channel.receiveSetter.set(this)
channel.resumeReceives() channel.resumeReceives()
@@ -78,16 +133,51 @@ class WebsocketHandler(
} }
} }
// todo: add LOAD_BINARY command which expects a hash, "LOAD_BINARY" -> {
// find the file in the data/files/ directory and send it as binary val hash = value.trim()
if (hash.isNotEmpty()) {
if (fileExists(hash)) {
val file = getFileFromHash(hash)
if (file.exists() && file.isFile) {
val bytes = file.readBytes()
WebSockets.sendBinary(ByteBuffer.wrap(bytes), channel, null)
}
}
}
}
} }
} }
} }
override fun onFullBinaryMessage(channel: WebSocketChannel?, message: BufferedBinaryMessage?) { override fun onFullBinaryMessage(channel: WebSocketChannel?, message: BufferedBinaryMessage?) {
// todo: create hash from binary, save file in data/files/ directory, // Process binary message: create hash from binary, save file in data/files/ directory,
// sub directories are 5 characters of the hash per directory // sub directories are 5 characters of the hash per directory
if (channel != null && message != null) {
try {
// Get the binary data
val pooled = message.data
val buffer = pooled.resource[0] // Get the first ByteBuffer
// Convert ByteBuffer to ByteArray
val bytes = ByteArray(buffer.remaining())
buffer.get(bytes)
// Free the pooled resource
pooled.free()
// Create hash from binary data
val hash = createHashFromBytes(bytes)
// Save file in data/files/ directory with subdirectories
val file = getFileFromHash(hash)
file.writeBytes(bytes)
// Send the hash back to the client
WebSockets.sendText("BINARY_SAVED\n$hash", channel, null)
} catch (e: Exception) {
WebSockets.sendText("ERROR\n${e.message}", channel, null)
}
}
} }
} }