Compare commits

...

4 Commits

Author SHA1 Message Date
8ee8f17f96 Refactor WebsocketHandler into a standalone class file 2025-06-10 20:08:28 +02:00
0bdaa5c94f 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.
2025-06-10 20:07:11 +02:00
3746ced387 Refactor SampleEntity to BinaryEntity and update related files and queries 2025-06-10 19:46:19 +02:00
4d7c46093c Update package declarations for SampleEntity files
Standardized package paths in `SampleEntity`, `SampleEntityQueryProvider`, and `SampleDao` from `nl.astraeus.vst.chip.db` to `nl.astraeus.vst.base.db`.
2025-06-10 19:35:10 +02:00
6 changed files with 174 additions and 77 deletions

View File

@@ -8,10 +8,11 @@ object Settings {
var port = 9004
var connectionTimeout = 30000
var jdbcStatsPort = 6001
var dataDir = "data"
var jdbcDriver = "nl.astraeus.jdbc.Driver"
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 jdbcPassword = ""
@@ -35,6 +36,8 @@ object Settings {
port = properties.getProperty("port", port.toString()).toInt()
jdbcStatsPort = properties.getProperty("jdbcStatsPort", jdbcStatsPort.toString()).toInt()
dataDir = properties.getProperty("dataDir", dataDir)
connectionTimeout =
properties.getProperty("connectionTimeout", connectionTimeout.toString()).toInt()
jdbcDriver = properties.getProperty("jdbcDriver", jdbcDriver)

View File

@@ -1,6 +1,6 @@
package nl.astraeus.vst.chip.db
package nl.astraeus.vst.base.db
object SampleDao {
object BinaryDao {
val queryProvider = SampleEntityQueryProvider
fun getSample(waveHash: String): ByteArray {

View File

@@ -1,8 +1,7 @@
package nl.astraeus.vst.chip.db
package nl.astraeus.vst.base.db
import kotlinx.datetime.Clock
import kotlinx.datetime.Instant
import nl.astraeus.vst.base.db.Entity
data class SampleEntity(
var sha1Hash: String,

View File

@@ -1,13 +1,9 @@
package nl.astraeus.vst.chip.db
package nl.astraeus.vst.base.db
import nl.astraeus.vst.base.db.QueryProvider
import nl.astraeus.vst.base.db.SqlStatement
import nl.astraeus.vst.base.db.toDateTimeInstant
import nl.astraeus.vst.base.db.toSqlTimestamp
import java.sql.ResultSet
val SAMPLE_CREATE_QUERY = """
CREATE TABLE SAMPLES (
val BINARY_CREATE_QUERY = """
CREATE TABLE BINARIESYSAMPLES (
SHA1HASH TEXT,
FILENAME TEXT,
LENGTH NUMBER,

View File

@@ -10,16 +10,6 @@ import io.undertow.server.session.Session
import io.undertow.server.session.SessionCookieConfig
import io.undertow.server.session.SessionManager
import io.undertow.util.Headers
import io.undertow.websockets.WebSocketConnectionCallback
import io.undertow.websockets.core.AbstractReceiveListener
import io.undertow.websockets.core.BufferedBinaryMessage
import io.undertow.websockets.core.BufferedTextMessage
import io.undertow.websockets.core.WebSocketChannel
import io.undertow.websockets.core.WebSockets
import io.undertow.websockets.spi.WebSocketHttpExchange
import nl.astraeus.vst.base.db.Database
import nl.astraeus.vst.base.db.PatchDao
import nl.astraeus.vst.base.db.PatchEntity
import java.nio.file.Paths
object VstSessionConfig {
@@ -30,61 +20,6 @@ object VstSessionConfig {
}
}
class WebsocketHandler(
val session: Session?
) : AbstractReceiveListener(), WebSocketConnectionCallback {
override fun onConnect(exchange: WebSocketHttpExchange, channel: WebSocketChannel) {
channel.receiveSetter.set(this)
channel.resumeReceives()
}
override fun onFullTextMessage(channel: WebSocketChannel, message: BufferedTextMessage) {
val vstSession = session?.getAttribute("html-session") as? VstSession
val data = message.data
val commandLength = data.indexOf('\n')
if (commandLength > 0) {
val command = data.substring(0, commandLength)
val value = data.substring(commandLength + 1)
when (command) {
"SAVE" -> {
val patchId = vstSession?.patchId
if (patchId != null) {
Database.transaction {
val patchEntity = PatchDao.findById(patchId)
if (patchEntity != null) {
PatchDao.update(patchEntity.copy(patch = value))
} else {
PatchDao.insert(PatchEntity(0, patchId, value))
}
}
WebSockets.sendText("SAVED\n$patchId", channel, null)
}
}
"LOAD" -> {
val patchId = vstSession?.patchId
if (patchId != null) {
Database.transaction {
val patchEntity = PatchDao.findById(patchId)
if (patchEntity != null) {
WebSockets.sendText("LOAD\n${patchEntity.patch}", channel, null)
}
}
}
}
}
}
}
override fun onFullBinaryMessage(channel: WebSocketChannel?, message: BufferedBinaryMessage?) {
// do nothing yet
}
}
object WebsocketConnectHandler : HttpHandler {
override fun handleRequest(exchange: HttpServerExchange) {

View File

@@ -0,0 +1,164 @@
package nl.astraeus.vst.base.web
import io.undertow.server.session.Session
import io.undertow.websockets.WebSocketConnectionCallback
import io.undertow.websockets.core.AbstractReceiveListener
import io.undertow.websockets.core.BufferedBinaryMessage
import io.undertow.websockets.core.BufferedTextMessage
import io.undertow.websockets.core.WebSocketChannel
import io.undertow.websockets.core.WebSockets
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.PatchDao
import nl.astraeus.vst.base.db.PatchEntity
import java.io.File
import java.nio.ByteBuffer
import java.security.MessageDigest
class WebsocketHandler(
val session: Session?
) : 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) {
channel.receiveSetter.set(this)
channel.resumeReceives()
}
override fun onFullTextMessage(channel: WebSocketChannel, message: BufferedTextMessage) {
val vstSession = session?.getAttribute("html-session") as? VstSession
val data = message.data
val commandLength = data.indexOf('\n')
if (commandLength > 0) {
val command = data.substring(0, commandLength)
val value = data.substring(commandLength + 1)
when (command) {
"SAVE" -> {
val patchId = vstSession?.patchId
if (patchId != null) {
Database.transaction {
val patchEntity = PatchDao.findById(patchId)
if (patchEntity != null) {
PatchDao.update(patchEntity.copy(patch = value))
} else {
PatchDao.insert(PatchEntity(0, patchId, value))
}
}
WebSockets.sendText("SAVED\n$patchId", channel, null)
}
}
"LOAD" -> {
val patchId = vstSession?.patchId
if (patchId != null) {
Database.transaction {
val patchEntity = PatchDao.findById(patchId)
if (patchEntity != null) {
WebSockets.sendText("LOAD\n${patchEntity.patch}", channel, null)
}
}
}
}
"LOAD_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?) {
// Process binary message: create hash from binary, save file in data/files/ 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)
}
}
}
}