From 24c32b70d1035e7933b5bac626b337905fbc117c Mon Sep 17 00:00:00 2001 From: Colt Daily Date: Wed, 28 Aug 2024 08:54:07 -0400 Subject: [PATCH] Add new `Vfs` sub-types: `UrlVfs` & `LocalVfs` (#276) * vfs: add two sub-type Vfs 'UrlVfs' & 'LocalVfs' vfs: refactor Vfs store/load key-values into own KeyValueStorage context: add new urlVfs & applicationVfs types * web: update WebGPUContext to handle new Vfs & KeyValueStorage * vfs: add documenting comments --- .../kotlin/com/littlekt/AssetProvider.kt | 2 +- .../commonMain/kotlin/com/littlekt/Context.kt | 22 +- .../com/littlekt/file/KeyValueStorage.kt | 37 ++++ .../kotlin/com/littlekt/file/LocalVfs.kt | 158 +++++++++++++++ .../kotlin/com/littlekt/file/UrlVfs.kt | 139 +++++++++++++ .../kotlin/com/littlekt/file/Vfs.kt | 188 +----------------- .../kotlin/com/littlekt/file/assets.kt | 19 ++ .../kotlin/com/littlekt/file/vfs/PathInfo.kt | 4 - .../kotlin/com/littlekt/file/vfs/VfsFile.kt | 8 - .../resources/internal/InternalResources.kt | 2 +- .../kotlin/com/littlekt/LittleKtApp.js.kt | 14 +- .../kotlin/com/littlekt/WebGPUContext.kt | 18 +- .../file/{WebVfs.kt => WebKeyValueStorage.kt} | 43 +--- .../kotlin/com/littlekt/file/WebLocalVfs.kt | 47 +++++ .../kotlin/com/littlekt/file/WebUrlVfs.kt | 46 +++++ .../kotlin/com/littlekt/LwjglContext.kt | 17 +- .../com/littlekt/file/JvmApplicationVfs.kt | 23 +++ .../com/littlekt/file/JvmKeyValueStorage.kt | 85 ++++++++ .../kotlin/com/littlekt/file/JvmLocalVfs.kt | 51 +++++ .../com/littlekt/file/JvmResourcesVfs.kt | 23 +++ .../kotlin/com/littlekt/file/JvmUrlVfs.kt | 77 +++++++ .../kotlin/com/littlekt/file/JvmVfs.kt | 160 --------------- 22 files changed, 775 insertions(+), 408 deletions(-) create mode 100644 core/src/commonMain/kotlin/com/littlekt/file/KeyValueStorage.kt create mode 100644 core/src/commonMain/kotlin/com/littlekt/file/LocalVfs.kt create mode 100644 core/src/commonMain/kotlin/com/littlekt/file/UrlVfs.kt create mode 100644 core/src/commonMain/kotlin/com/littlekt/file/assets.kt rename core/src/jsMain/kotlin/com/littlekt/file/{WebVfs.kt => WebKeyValueStorage.kt} (72%) create mode 100644 core/src/jsMain/kotlin/com/littlekt/file/WebLocalVfs.kt create mode 100644 core/src/jsMain/kotlin/com/littlekt/file/WebUrlVfs.kt create mode 100644 core/src/jvmMain/kotlin/com/littlekt/file/JvmApplicationVfs.kt create mode 100644 core/src/jvmMain/kotlin/com/littlekt/file/JvmKeyValueStorage.kt create mode 100644 core/src/jvmMain/kotlin/com/littlekt/file/JvmLocalVfs.kt create mode 100644 core/src/jvmMain/kotlin/com/littlekt/file/JvmResourcesVfs.kt create mode 100644 core/src/jvmMain/kotlin/com/littlekt/file/JvmUrlVfs.kt delete mode 100644 core/src/jvmMain/kotlin/com/littlekt/file/JvmVfs.kt diff --git a/core/src/commonMain/kotlin/com/littlekt/AssetProvider.kt b/core/src/commonMain/kotlin/com/littlekt/AssetProvider.kt index f83e80080..22b88578e 100644 --- a/core/src/commonMain/kotlin/com/littlekt/AssetProvider.kt +++ b/core/src/commonMain/kotlin/com/littlekt/AssetProvider.kt @@ -91,7 +91,7 @@ open class AssetProvider(val context: Context) { parameters: GameAssetParameters = EmptyGameAssetParameter(), ): GameAsset { val sceneAsset = checkOrCreateNewSceneAsset(file, clazz) - context.vfs.launch { loadVfsFile(sceneAsset, file, clazz, parameters) } + file.vfs.launch { loadVfsFile(sceneAsset, file, clazz, parameters) } return sceneAsset } diff --git a/core/src/commonMain/kotlin/com/littlekt/Context.kt b/core/src/commonMain/kotlin/com/littlekt/Context.kt index ce847b551..8a7c6c4ff 100644 --- a/core/src/commonMain/kotlin/com/littlekt/Context.kt +++ b/core/src/commonMain/kotlin/com/littlekt/Context.kt @@ -1,5 +1,6 @@ package com.littlekt +import com.littlekt.file.KeyValueStorage import com.littlekt.file.Vfs import com.littlekt.file.vfs.VfsFile import com.littlekt.input.Input @@ -67,13 +68,28 @@ abstract class Context { abstract val logger: Logger /** The virtual file system access property. */ - abstract val vfs: Vfs + abstract val vfsResources: Vfs + + /** The virtual file system access property. */ + abstract val vfsUrl: Vfs + + /** The virtual file system access property. */ + abstract val vfsApplication: Vfs /** A [VfsFile] used for accessing data based on the **resources** directory. */ abstract val resourcesVfs: VfsFile - /** A [VfsFile] used for storing and reading data based on the **storage** directory. */ - abstract val storageVfs: VfsFile + /** A [VfsFile] used for accessing data from the web or data urls. */ + abstract val urlVfs: VfsFile + + /** A [VfsFile] used for accessing data based on the application working directory. */ + abstract val applicationVfs: VfsFile + + /** + * A [KeyValueStorage] used for storing and reading simple key-value data based on the + * **storage** directory. + */ + abstract val kvStorage: KeyValueStorage /** The [Platform] this context is running on. */ abstract val platform: Platform diff --git a/core/src/commonMain/kotlin/com/littlekt/file/KeyValueStorage.kt b/core/src/commonMain/kotlin/com/littlekt/file/KeyValueStorage.kt new file mode 100644 index 000000000..5862b86d7 --- /dev/null +++ b/core/src/commonMain/kotlin/com/littlekt/file/KeyValueStorage.kt @@ -0,0 +1,37 @@ +package com.littlekt.file + +/** + * A simple key-value storage system. + * + * @author Colton Daily + * @date 8/28/2024 + */ +interface KeyValueStorage { + /** + * Stores the following [data] by the specified [key]. + * + * @return `true` if stored successfully. + */ + fun store(key: String, data: ByteArray): Boolean + + /** + * Stores the following [data] by the specified [key]. + * + * @return `true` if stored successfully. + */ + fun store(key: String, data: String): Boolean + + /** + * Loads data, if it exists, by the specified [key]. + * + * @return stored data, if it exists. + */ + fun load(key: String): ByteBuffer? + + /** + * Loads data, if it exists, by the specified [key]. + * + * @return stored data, if it exists. + */ + fun loadString(key: String): String? +} diff --git a/core/src/commonMain/kotlin/com/littlekt/file/LocalVfs.kt b/core/src/commonMain/kotlin/com/littlekt/file/LocalVfs.kt new file mode 100644 index 000000000..36ee1b7bc --- /dev/null +++ b/core/src/commonMain/kotlin/com/littlekt/file/LocalVfs.kt @@ -0,0 +1,158 @@ +package com.littlekt.file + +import com.littlekt.Context +import com.littlekt.file.vfs.normalize +import com.littlekt.file.vfs.pathInfo +import com.littlekt.log.Logger +import com.littlekt.util.toString +import kotlin.coroutines.CoroutineContext +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.Job +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.channels.ReceiveChannel +import kotlinx.coroutines.channels.SendChannel +import kotlinx.coroutines.selects.select +import kotlinx.serialization.json.Json + +/** + * A [Vfs] that handles reading from files on the local file system. + * + * @author Colton Daily + * @date 8/26/2024 + */ +abstract class LocalVfs(context: Context, logger: Logger, baseDir: String) : + Vfs(context, logger, baseDir) { + + override val json = Json { + ignoreUnknownKeys = true + isLenient = true + } + + protected val job = Job() + + override val coroutineContext: CoroutineContext = job + + private val awaitedAssetsChannel = Channel() + private val assetRefChannel = Channel(Channel.UNLIMITED) + private val loadedAssetChannel = Channel() + + init { + repeat(NUM_LOAD_WORKERS) { loadWorker(assetRefChannel, loadedAssetChannel) } + launch { + val requested = mutableMapOf>() + while (true) { + select { + awaitedAssetsChannel.onReceive { awaited -> + val awaiting = requested[awaited.ref] + if (awaiting == null) { + requested[awaited.ref] = mutableListOf(awaited) + assetRefChannel.send(awaited.ref) + } else { + awaiting.add(awaited) + } + } + loadedAssetChannel.onReceive { loaded -> + val awaiting = requested.remove(loaded.ref)!! + for (awaited in awaiting) { + awaited.awaiting.complete(loaded) + } + } + } + } + } + } + + private fun loadWorker( + assetRefs: ReceiveChannel, + loadedAssets: SendChannel + ) = launch { + for (ref in assetRefs) { + loadedAssets.send(readBytes(ref)) + } + } + + private suspend fun readBytes(ref: AssetRef): LoadedAsset { + return when (ref) { + is RawAssetRef -> loadRawAsset(ref) + is SequenceAssetRef -> loadSequenceStreamAsset(ref) + } + } + + protected abstract suspend fun loadRawAsset(rawRef: RawAssetRef): LoadedRawAsset + + protected abstract suspend fun loadSequenceStreamAsset( + sequenceRef: SequenceAssetRef + ): SequenceStreamCreatedAsset + + /** + * Loads a raw file into a [ByteBuffer] + * + * @param assetPath the path to the file + * @return the raw byte buffer + */ + override suspend fun readBytes(assetPath: String): ByteBuffer { + check(!isHttpAsset(assetPath)) { + "A http url is not a valid asset path. Vfs only loads local files!" + } + val ref = + if (baseDir.endsWith("/")) { + RawAssetRef("$baseDir$assetPath".pathInfo.normalize(), true) + } else if (baseDir.isNotBlank()) { + RawAssetRef("$baseDir/$assetPath".pathInfo.normalize(), true) + } else { + RawAssetRef(assetPath.pathInfo.normalize(), true) + } + val awaitedAsset = AwaitedAsset(ref) + awaitedAssetsChannel.send(awaitedAsset) + val loaded = awaitedAsset.awaiting.await() as LoadedRawAsset + loaded.data?.let { + logger.info { + "Loaded ${assetPathToName(assetPath)} (${(it.capacity / 1024.0 / 1024.0).toString(2)} mb)" + } + } + return loaded.data ?: throw FileNotFoundException(assetPath) + } + + /** + * Opens a stream to a file into a [ByteSequenceStream]. + * + * @param assetPath the path to file + * @return the byte input stream + */ + override suspend fun readStream(assetPath: String): ByteSequenceStream { + check(!isHttpAsset(assetPath)) { + "A http url is not a valid asset path. Vfs only loads local files!" + } + val ref = + if (baseDir.endsWith("/")) { + SequenceAssetRef("$baseDir$assetPath") + } else if (baseDir.isNotBlank()) { + SequenceAssetRef("$baseDir/$assetPath") + } else { + SequenceAssetRef(assetPath) + } + val awaitedAsset = AwaitedAsset(ref) + awaitedAssetsChannel.send(awaitedAsset) + val loaded = awaitedAsset.awaiting.await() as SequenceStreamCreatedAsset + loaded.sequence?.let { logger.info { "Opened stream to ${assetPathToName(assetPath)}." } } + return loaded.sequence ?: throw FileNotFoundException(assetPath) + } + + private fun assetPathToName(assetPath: String): String { + return if (assetPath.startsWith("data:", true)) { + val idx = assetPath.indexOf(';') + assetPath.substring(0 until idx) + } else { + assetPath + } + } + + protected inner class AwaitedAsset( + val ref: AssetRef, + val awaiting: CompletableDeferred = CompletableDeferred(job) + ) + + companion object { + const val NUM_LOAD_WORKERS = 8 + } +} diff --git a/core/src/commonMain/kotlin/com/littlekt/file/UrlVfs.kt b/core/src/commonMain/kotlin/com/littlekt/file/UrlVfs.kt new file mode 100644 index 000000000..a5e909b18 --- /dev/null +++ b/core/src/commonMain/kotlin/com/littlekt/file/UrlVfs.kt @@ -0,0 +1,139 @@ +package com.littlekt.file + +import com.littlekt.Context +import com.littlekt.log.Logger +import com.littlekt.util.toString +import kotlin.coroutines.CoroutineContext +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.Job +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.channels.ReceiveChannel +import kotlinx.coroutines.channels.SendChannel +import kotlinx.coroutines.selects.select +import kotlinx.serialization.json.Json + +/** + * A [Vfs] that handles reading from files from a URL. + * + * @author Colton Daily + * @date 8/26/2024 + */ +abstract class UrlVfs(context: Context, logger: Logger) : Vfs(context, logger, "") { + + override val json = Json { + ignoreUnknownKeys = true + isLenient = true + } + + protected val job = Job() + + override val coroutineContext: CoroutineContext = job + + private val awaitedAssetsChannel = Channel() + private val assetRefChannel = Channel(Channel.UNLIMITED) + private val loadedAssetChannel = Channel() + + init { + repeat(NUM_LOAD_WORKERS) { loadWorker(assetRefChannel, loadedAssetChannel) } + launch { + val requested = mutableMapOf>() + while (true) { + select { + awaitedAssetsChannel.onReceive { awaited -> + val awaiting = requested[awaited.ref] + if (awaiting == null) { + requested[awaited.ref] = mutableListOf(awaited) + assetRefChannel.send(awaited.ref) + } else { + awaiting.add(awaited) + } + } + loadedAssetChannel.onReceive { loaded -> + val awaiting = requested.remove(loaded.ref)!! + for (awaited in awaiting) { + awaited.awaiting.complete(loaded) + } + } + } + } + } + } + + private fun loadWorker( + assetRefs: ReceiveChannel, + loadedAssets: SendChannel + ) = launch { + for (ref in assetRefs) { + loadedAssets.send(readBytes(ref)) + } + } + + private suspend fun readBytes(ref: AssetRef): LoadedAsset { + return when (ref) { + is RawAssetRef -> loadRawAsset(ref) + is SequenceAssetRef -> loadSequenceStreamAsset(ref) + } + } + + protected abstract suspend fun loadRawAsset(rawRef: RawAssetRef): LoadedRawAsset + + protected abstract suspend fun loadSequenceStreamAsset( + sequenceRef: SequenceAssetRef + ): SequenceStreamCreatedAsset + + /** + * Loads a raw file into a [ByteBuffer] + * + * @param assetPath the path to the file + * @return the raw byte buffer + */ + override suspend fun readBytes(assetPath: String): ByteBuffer { + check(isHttpAsset(assetPath)) { "$assetPath is not a valid URL!" } + val ref = RawAssetRef(assetPath, false) + + val awaitedAsset = AwaitedAsset(ref) + awaitedAssetsChannel.send(awaitedAsset) + val loaded = awaitedAsset.awaiting.await() as LoadedRawAsset + loaded.data?.let { + logger.info { + "Loaded ${assetPathToName(assetPath)} (${(it.capacity / 1024.0 / 1024.0).toString(2)} mb)" + } + } + return loaded.data ?: throw FileNotFoundException(assetPath) + } + + /** + * Opens a stream to a file into a [ByteSequenceStream]. + * + * @param assetPath the path to file + * @return the byte input stream + */ + override suspend fun readStream(assetPath: String): ByteSequenceStream { + check(isHttpAsset(assetPath)) { "$assetPath is not a valid URL!" } + val ref = SequenceAssetRef(assetPath) + + val awaitedAsset = AwaitedAsset(ref) + awaitedAssetsChannel.send(awaitedAsset) + val loaded = awaitedAsset.awaiting.await() as SequenceStreamCreatedAsset + loaded.sequence?.let { logger.info { "Opened stream to ${assetPathToName(assetPath)}." } } + return loaded.sequence ?: throw FileNotFoundException(assetPath) + } + + private fun assetPathToName(assetPath: String): String { + return if (assetPath.startsWith("data:", true)) { + val idx = assetPath.indexOf(';') + assetPath.substring(0 until idx) + } else { + assetPath + } + } + + protected inner class AwaitedAsset( + val ref: AssetRef, + val awaiting: CompletableDeferred = CompletableDeferred(job) + ) + + companion object { + const val NUM_LOAD_WORKERS = 8 + } +} diff --git a/core/src/commonMain/kotlin/com/littlekt/file/Vfs.kt b/core/src/commonMain/kotlin/com/littlekt/file/Vfs.kt index ac70aff98..295901995 100644 --- a/core/src/commonMain/kotlin/com/littlekt/file/Vfs.kt +++ b/core/src/commonMain/kotlin/com/littlekt/file/Vfs.kt @@ -3,19 +3,10 @@ package com.littlekt.file import com.littlekt.Context import com.littlekt.file.vfs.VfsFile import com.littlekt.file.vfs.lightCombine -import com.littlekt.file.vfs.normalize import com.littlekt.file.vfs.pathInfo import com.littlekt.log.Logger -import com.littlekt.util.toString -import kotlin.coroutines.CoroutineContext -import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Job -import kotlinx.coroutines.channels.Channel -import kotlinx.coroutines.channels.ReceiveChannel -import kotlinx.coroutines.channels.SendChannel import kotlinx.coroutines.launch -import kotlinx.coroutines.selects.select import kotlinx.serialization.json.Json /** @@ -24,67 +15,19 @@ import kotlinx.serialization.json.Json * @author Colton Daily * @date 11/6/2021 */ -abstract class Vfs(val context: Context, val logger: Logger, var baseDir: String) : CoroutineScope { +abstract class Vfs(val context: Context, val logger: Logger, val baseDir: String = "") : + CoroutineScope { /** The root [VfsFile] that this [Vfs] starts from. */ - val root + val root: VfsFile get() = VfsFile(this, baseDir) + abstract val json: Json + protected open val absolutePath: String get() = "" - @PublishedApi - internal val json = Json { - ignoreUnknownKeys = true - isLenient = true - } - - protected val job = Job() - - override val coroutineContext: CoroutineContext = job - - private val awaitedAssetsChannel = Channel() - private val assetRefChannel = Channel(Channel.UNLIMITED) - private val loadedAssetChannel = Channel() - - init { - repeat(NUM_LOAD_WORKERS) { loadWorker(assetRefChannel, loadedAssetChannel) } - launch { - val requested = mutableMapOf>() - while (true) { - select { - awaitedAssetsChannel.onReceive { awaited -> - val awaiting = requested[awaited.ref] - if (awaiting == null) { - requested[awaited.ref] = mutableListOf(awaited) - assetRefChannel.send(awaited.ref) - } else { - awaiting.add(awaited) - } - } - loadedAssetChannel.onReceive { loaded -> - val awaiting = requested.remove(loaded.ref)!! - for (awaited in awaiting) { - awaited.awaiting.complete(loaded) - } - } - } - } - } - } - - private fun loadWorker( - assetRefs: ReceiveChannel, - loadedAssets: SendChannel - ) = launch { - for (ref in assetRefs) { - loadedAssets.send(readBytes(ref)) - } - } - /** Cancels this vfs job. */ - open fun close() { - job.cancel() - } + open fun close() = Unit /** * Get the absolute path of the queried [path] based off the Vfs path. @@ -94,19 +37,6 @@ abstract class Vfs(val context: Context, val logger: Logger, var baseDir: String open fun getAbsolutePath(path: String) = absolutePath.pathInfo.lightCombine(path.pathInfo).fullPath - private suspend fun readBytes(ref: AssetRef): LoadedAsset { - return when (ref) { - is RawAssetRef -> loadRawAsset(ref) - is SequenceAssetRef -> loadSequenceStreamAsset(ref) - } - } - - protected abstract suspend fun loadRawAsset(rawRef: RawAssetRef): LoadedRawAsset - - protected abstract suspend fun loadSequenceStreamAsset( - sequenceRef: SequenceAssetRef - ): SequenceStreamCreatedAsset - protected open fun isHttpAsset(assetPath: String): Boolean = // maybe use something less naive here? assetPath.startsWith("http://", true) || @@ -114,66 +44,14 @@ abstract class Vfs(val context: Context, val logger: Logger, var baseDir: String assetPath.startsWith("data:", true) /** - * Launches a new coroutine using the this vfs coroutine context. Use this to load assets + * Launches a new coroutine using this vfs coroutine context. Use this to load assets * asynchronously. */ fun launch(block: suspend Vfs.() -> Unit) { (this as CoroutineScope).launch { block.invoke(this@Vfs) } } - /** - * Store array of bytes in the storage directory based on the [key]. - * - * @param key the key of the data - * @param data the data to store - */ - abstract fun store(key: String, data: ByteArray): Boolean - - /** - * Store a string in the storage directory based on the [key]. - * - * @param key the key of the data - * @param data the string to store - */ - abstract fun store(key: String, data: String): Boolean - - /** - * Load an array of bytes from the storage directory based on the [key]. - * - * @param key the key of the data to load - */ - abstract fun load(key: String): ByteBuffer? - - /** - * Load a string from the storage directory based on the [key]. - * - * @param key the key of the string to load - */ - abstract fun loadString(key: String): String? - - /** - * Loads a raw file into a [ByteBuffer] - * - * @param assetPath the path to the file - * @return the raw byte buffer - */ - suspend fun readBytes(assetPath: String): ByteBuffer { - val ref = - if (isHttpAsset(assetPath)) { - RawAssetRef(assetPath, false) - } else { - RawAssetRef("$baseDir/$assetPath".pathInfo.normalize(), true) - } - val awaitedAsset = AwaitedAsset(ref) - awaitedAssetsChannel.send(awaitedAsset) - val loaded = awaitedAsset.awaiting.await() as LoadedRawAsset - loaded.data?.let { - logger.info { - "Loaded ${assetPathToName(assetPath)} (${(it.capacity / 1024.0 / 1024.0).toString(2)} mb)" - } - } - return loaded.data ?: throw FileNotFoundException(assetPath) - } + abstract suspend fun readBytes(assetPath: String): ByteBuffer /** * Opens a stream to a file into a [ByteSequenceStream]. @@ -181,55 +59,7 @@ abstract class Vfs(val context: Context, val logger: Logger, var baseDir: String * @param assetPath the path to file * @return the byte input stream */ - suspend fun readStream(assetPath: String): ByteSequenceStream { - val ref = - if (isHttpAsset(assetPath)) { - SequenceAssetRef(assetPath) - } else { - SequenceAssetRef("$baseDir/$assetPath") - } - val awaitedAsset = AwaitedAsset(ref) - awaitedAssetsChannel.send(awaitedAsset) - val loaded = awaitedAsset.awaiting.await() as SequenceStreamCreatedAsset - loaded.sequence?.let { logger.info { "Opened stream to ${assetPathToName(assetPath)}." } } - return loaded.sequence ?: throw FileNotFoundException(assetPath) - } - - private fun assetPathToName(assetPath: String): String { - return if (assetPath.startsWith("data:", true)) { - val idx = assetPath.indexOf(';') - assetPath.substring(0 until idx) - } else { - assetPath - } - } + abstract suspend fun readStream(assetPath: String): ByteSequenceStream operator fun get(path: String) = root[path] - - protected inner class AwaitedAsset( - val ref: AssetRef, - val awaiting: CompletableDeferred = CompletableDeferred(job) - ) - - companion object { - const val NUM_LOAD_WORKERS = 8 - } } - -sealed class AssetRef - -data class RawAssetRef(val url: String, val isLocal: Boolean) : AssetRef() - -data class SequenceAssetRef(val url: String) : AssetRef() - -sealed class LoadedAsset(val ref: AssetRef, val successful: Boolean) - -class LoadedRawAsset(ref: AssetRef, val data: ByteBuffer?) : LoadedAsset(ref, data != null) - -class SequenceStreamCreatedAsset(ref: AssetRef, val sequence: ByteSequenceStream?) : - LoadedAsset(ref, sequence != null) - -class FileNotFoundException(path: String) : - Exception("File ($path) could not be found! Check to make sure it exists and is not corrupt.") - -class UnsupportedFileTypeException(message: String) : Exception("Unsupported file: $message") diff --git a/core/src/commonMain/kotlin/com/littlekt/file/assets.kt b/core/src/commonMain/kotlin/com/littlekt/file/assets.kt new file mode 100644 index 000000000..77b00cb43 --- /dev/null +++ b/core/src/commonMain/kotlin/com/littlekt/file/assets.kt @@ -0,0 +1,19 @@ +package com.littlekt.file + +sealed class AssetRef + +data class RawAssetRef(val url: String, val isLocal: Boolean) : AssetRef() + +data class SequenceAssetRef(val url: String) : AssetRef() + +sealed class LoadedAsset(val ref: AssetRef, val successful: Boolean) + +class LoadedRawAsset(ref: AssetRef, val data: ByteBuffer?) : LoadedAsset(ref, data != null) + +class SequenceStreamCreatedAsset(ref: AssetRef, val sequence: ByteSequenceStream?) : + LoadedAsset(ref, sequence != null) + +class FileNotFoundException(path: String) : + Exception("File ($path) could not be found! Check to make sure it exists and is not corrupt.") + +class UnsupportedFileTypeException(message: String) : Exception("Unsupported file: $message") diff --git a/core/src/commonMain/kotlin/com/littlekt/file/vfs/PathInfo.kt b/core/src/commonMain/kotlin/com/littlekt/file/vfs/PathInfo.kt index ef0237dd4..50a28ef92 100644 --- a/core/src/commonMain/kotlin/com/littlekt/file/vfs/PathInfo.kt +++ b/core/src/commonMain/kotlin/com/littlekt/file/vfs/PathInfo.kt @@ -3,10 +3,6 @@ package com.littlekt.file.vfs import kotlin.jvm.JvmInline import kotlin.math.min -/** - * @author Colton Daily - * @date 12/20/2021 - */ @JvmInline value class PathInfo(val fullPath: String) fun PathInfo.relativePathTo(relative: PathInfo): String? { diff --git a/core/src/commonMain/kotlin/com/littlekt/file/vfs/VfsFile.kt b/core/src/commonMain/kotlin/com/littlekt/file/vfs/VfsFile.kt index 1ce965850..dd737d8ac 100644 --- a/core/src/commonMain/kotlin/com/littlekt/file/vfs/VfsFile.kt +++ b/core/src/commonMain/kotlin/com/littlekt/file/vfs/VfsFile.kt @@ -35,14 +35,6 @@ data class VfsFile(val vfs: Vfs, val path: String) : VfsNamed(path.pathInfo) { suspend inline fun decodeFromString() = vfs.json.decodeFromString(readString()) - fun writeKeystore(data: ByteArray) = vfs.store(pathInfo.baseName, data) - - fun writeKeystore(data: String) = vfs.store(pathInfo.baseName, data) - - fun readKeystore() = vfs.loadString(pathInfo.baseName) - - fun readKeystoreBytes() = vfs.load(pathInfo.baseName) - fun relativePathTo(relative: VfsFile): String? { if (relative.vfs != this.vfs) return null return this.pathInfo.relativePathTo(relative.pathInfo) diff --git a/core/src/commonMain/kotlin/com/littlekt/resources/internal/InternalResources.kt b/core/src/commonMain/kotlin/com/littlekt/resources/internal/InternalResources.kt index 662a22bd2..59d1ca1d9 100644 --- a/core/src/commonMain/kotlin/com/littlekt/resources/internal/InternalResources.kt +++ b/core/src/commonMain/kotlin/com/littlekt/resources/internal/InternalResources.kt @@ -50,7 +50,7 @@ internal class InternalResources private constructor(private val context: Contex else TextureFormat.RGBA8_UNORM val device = context.graphics.device val page = - context.vfs.json.decodeFromString( + context.vfsUrl.json.decodeFromString( defaultTilesJson.decodeFromBase64().decodeToString() ) val info = AtlasInfo(page.meta, listOf(page)) diff --git a/core/src/jsMain/kotlin/com/littlekt/LittleKtApp.js.kt b/core/src/jsMain/kotlin/com/littlekt/LittleKtApp.js.kt index d181a8a6c..55305f5e3 100644 --- a/core/src/jsMain/kotlin/com/littlekt/LittleKtApp.js.kt +++ b/core/src/jsMain/kotlin/com/littlekt/LittleKtApp.js.kt @@ -6,7 +6,8 @@ actual class LittleKtProps { var height: Int = 540 var canvasId: String = "canvas" var title: String = "LitteKt" - var assetsDir: String = "./" + var resourcesDir: String = "./" + var applicationDir: String = "./" var powerPreference = PowerPreference.HIGH_POWER } @@ -19,7 +20,13 @@ actual fun createLittleKtApp(action: LittleKtProps.() -> Unit): LittleKtApp { props.action() return LittleKtApp( WebGPUContext( - JsConfiguration(props.title, props.canvasId, props.assetsDir, props.powerPreference) + JsConfiguration( + title = props.title, + canvasId = props.canvasId, + resourcesPath = props.resourcesDir, + applicationPath = props.applicationDir, + powerPreference = props.powerPreference + ) ) ) } @@ -31,7 +38,8 @@ actual fun createLittleKtApp(action: LittleKtProps.() -> Unit): LittleKtApp { class JsConfiguration( override val title: String = "LittleKt - JS", val canvasId: String = "canvas", - val rootPath: String = "./", + val resourcesPath: String = "./", + val applicationPath: String = "./", val powerPreference: PowerPreference = PowerPreference.HIGH_POWER ) : ContextConfiguration() diff --git a/core/src/jsMain/kotlin/com/littlekt/WebGPUContext.kt b/core/src/jsMain/kotlin/com/littlekt/WebGPUContext.kt index ca6a83e25..1560dfbbd 100644 --- a/core/src/jsMain/kotlin/com/littlekt/WebGPUContext.kt +++ b/core/src/jsMain/kotlin/com/littlekt/WebGPUContext.kt @@ -2,7 +2,7 @@ package com.littlekt import com.littlekt.async.KT import com.littlekt.async.KtScope -import com.littlekt.file.WebVfs +import com.littlekt.file.* import com.littlekt.file.vfs.VfsFile import com.littlekt.graphics.webgpu.Adapter import com.littlekt.graphics.webgpu.GPURequestAdapterOptions @@ -31,12 +31,20 @@ class WebGPUContext(override val configuration: JsConfiguration) : Context() { override val graphics: WebGPUGraphics = WebGPUGraphics(canvas) override val input: JsInput = JsInput(canvas) override val logger: Logger = Logger(configuration.title) - override val vfs = WebVfs(this, logger, configuration.rootPath) + override val vfsResources: Vfs = WebLocalVfs(this, logger, configuration.resourcesPath) + override val vfsUrl: Vfs = WebUrlVfs(this, logger) + override val vfsApplication: Vfs = WebLocalVfs(this, logger, configuration.applicationPath) + override val resourcesVfs: VfsFile - get() = vfs.root + get() = vfsResources.root + + override val urlVfs: VfsFile + get() = vfsUrl.root + + override val applicationVfs: VfsFile + get() = vfsApplication.root - override val storageVfs: VfsFile - get() = vfs.root + override val kvStorage: KeyValueStorage = WebKeyValueStorage(logger) override val platform: Platform = Platform.WEB override val clipboard: JsClipboard = JsClipboard() diff --git a/core/src/jsMain/kotlin/com/littlekt/file/WebVfs.kt b/core/src/jsMain/kotlin/com/littlekt/file/WebKeyValueStorage.kt similarity index 72% rename from core/src/jsMain/kotlin/com/littlekt/file/WebVfs.kt rename to core/src/jsMain/kotlin/com/littlekt/file/WebKeyValueStorage.kt index dea70cdad..cf6a860a7 100644 --- a/core/src/jsMain/kotlin/com/littlekt/file/WebVfs.kt +++ b/core/src/jsMain/kotlin/com/littlekt/file/WebKeyValueStorage.kt @@ -1,54 +1,19 @@ package com.littlekt.file -import com.littlekt.Context import com.littlekt.log.Logger import kotlinx.browser.localStorage import kotlinx.browser.window -import kotlinx.coroutines.CompletableDeferred -import org.khronos.webgl.ArrayBuffer import org.khronos.webgl.Uint8Array import org.khronos.webgl.get import org.khronos.webgl.set -import org.w3c.dom.* -import org.w3c.xhr.ARRAYBUFFER -import org.w3c.xhr.XMLHttpRequest -import org.w3c.xhr.XMLHttpRequestResponseType +import org.w3c.dom.get +import org.w3c.dom.set /** * @author Colton Daily - * @date 11/6/2021 + * @date 8/28/2024 */ -class WebVfs(context: Context, logger: Logger, assetsBaseDir: String) : - Vfs(context, logger, assetsBaseDir) { - - override suspend fun loadRawAsset(rawRef: RawAssetRef) = - LoadedRawAsset(rawRef, loadRaw(rawRef.url)) - - override suspend fun loadSequenceStreamAsset( - sequenceRef: SequenceAssetRef - ): SequenceStreamCreatedAsset { - val buffer = loadRaw(sequenceRef.url) - val stream = if (buffer != null) JsByteSequenceStream(buffer) else null - return SequenceStreamCreatedAsset(sequenceRef, stream) - } - - private suspend fun loadRaw(url: String): ByteBuffer? { - val data = CompletableDeferred(job) - val req = XMLHttpRequest() - req.responseType = XMLHttpRequestResponseType.ARRAYBUFFER - req.onload = { - val array = Uint8Array(req.response as ArrayBuffer) - data.complete(ByteBufferImpl(array)) - } - req.onerror = { - data.complete(null) - logger.error { "Failed loading resource $url: $it" } - } - req.open("GET", url) - req.send() - - return data.await() - } +class WebKeyValueStorage(private val logger: Logger) : KeyValueStorage { override fun store(key: String, data: ByteArray): Boolean { return try { diff --git a/core/src/jsMain/kotlin/com/littlekt/file/WebLocalVfs.kt b/core/src/jsMain/kotlin/com/littlekt/file/WebLocalVfs.kt new file mode 100644 index 000000000..ee8b358b1 --- /dev/null +++ b/core/src/jsMain/kotlin/com/littlekt/file/WebLocalVfs.kt @@ -0,0 +1,47 @@ +package com.littlekt.file + +import com.littlekt.Context +import com.littlekt.log.Logger +import kotlinx.coroutines.CompletableDeferred +import org.khronos.webgl.ArrayBuffer +import org.khronos.webgl.Uint8Array +import org.w3c.xhr.ARRAYBUFFER +import org.w3c.xhr.XMLHttpRequest +import org.w3c.xhr.XMLHttpRequestResponseType + +/** + * @author Colton Daily + * @date 11/6/2021 + */ +class WebLocalVfs(context: Context, logger: Logger, assetsBaseDir: String) : + LocalVfs(context, logger, assetsBaseDir) { + + override suspend fun loadRawAsset(rawRef: RawAssetRef) = + LoadedRawAsset(rawRef, loadRaw(rawRef.url)) + + override suspend fun loadSequenceStreamAsset( + sequenceRef: SequenceAssetRef + ): SequenceStreamCreatedAsset { + val buffer = loadRaw(sequenceRef.url) + val stream = if (buffer != null) JsByteSequenceStream(buffer) else null + return SequenceStreamCreatedAsset(sequenceRef, stream) + } + + private suspend fun loadRaw(url: String): ByteBuffer? { + val data = CompletableDeferred(job) + val req = XMLHttpRequest() + req.responseType = XMLHttpRequestResponseType.ARRAYBUFFER + req.onload = { + val array = Uint8Array(req.response as ArrayBuffer) + data.complete(ByteBufferImpl(array)) + } + req.onerror = { + data.complete(null) + logger.error { "Failed loading resource $url: $it" } + } + req.open("GET", url) + req.send() + + return data.await() + } +} diff --git a/core/src/jsMain/kotlin/com/littlekt/file/WebUrlVfs.kt b/core/src/jsMain/kotlin/com/littlekt/file/WebUrlVfs.kt new file mode 100644 index 000000000..53ac12995 --- /dev/null +++ b/core/src/jsMain/kotlin/com/littlekt/file/WebUrlVfs.kt @@ -0,0 +1,46 @@ +package com.littlekt.file + +import com.littlekt.Context +import com.littlekt.log.Logger +import kotlinx.coroutines.CompletableDeferred +import org.khronos.webgl.ArrayBuffer +import org.khronos.webgl.Uint8Array +import org.w3c.xhr.ARRAYBUFFER +import org.w3c.xhr.XMLHttpRequest +import org.w3c.xhr.XMLHttpRequestResponseType + +/** + * @author Colton Daily + * @date 11/6/2021 + */ +class WebUrlVfs(context: Context, logger: Logger) : UrlVfs(context, logger) { + + override suspend fun loadRawAsset(rawRef: RawAssetRef) = + LoadedRawAsset(rawRef, loadRaw(rawRef.url)) + + override suspend fun loadSequenceStreamAsset( + sequenceRef: SequenceAssetRef + ): SequenceStreamCreatedAsset { + val buffer = loadRaw(sequenceRef.url) + val stream = if (buffer != null) JsByteSequenceStream(buffer) else null + return SequenceStreamCreatedAsset(sequenceRef, stream) + } + + private suspend fun loadRaw(url: String): ByteBuffer? { + val data = CompletableDeferred(job) + val req = XMLHttpRequest() + req.responseType = XMLHttpRequestResponseType.ARRAYBUFFER + req.onload = { + val array = Uint8Array(req.response as ArrayBuffer) + data.complete(ByteBufferImpl(array)) + } + req.onerror = { + data.complete(null) + logger.error { "Failed loading resource $url: $it" } + } + req.open("GET", url) + req.send() + + return data.await() + } +} diff --git a/core/src/jvmMain/kotlin/com/littlekt/LwjglContext.kt b/core/src/jvmMain/kotlin/com/littlekt/LwjglContext.kt index c60866f62..e7a0045c0 100644 --- a/core/src/jvmMain/kotlin/com/littlekt/LwjglContext.kt +++ b/core/src/jvmMain/kotlin/com/littlekt/LwjglContext.kt @@ -4,8 +4,8 @@ import com.littlekt.async.KtScope import com.littlekt.async.MainDispatcher import com.littlekt.async.mainThread import com.littlekt.audio.OpenALAudioContext +import com.littlekt.file.* import com.littlekt.file.Base64.decodeFromBase64 -import com.littlekt.file.JvmVfs import com.littlekt.file.vfs.VfsFile import com.littlekt.file.vfs.readPixmap import com.littlekt.graphics.webgpu.WGPU_NULL @@ -38,13 +38,20 @@ class LwjglContext(override val configuration: JvmConfiguration) : Context() { override val stats: AppStats = AppStats() override val graphics: LwjglGraphics = LwjglGraphics(this) override val logger: Logger = Logger(configuration.title) + override val vfsResources: Vfs = JvmResourcesVfs(this, logger) + override val vfsUrl: Vfs = JvmUrlVfs(this, logger) + override val vfsApplication: Vfs = JvmApplicationVfs(this, logger, ".") override val input: LwjglInput = LwjglInput(this) - override val vfs = JvmVfs(this, logger, "./.storage", ".") override val resourcesVfs: VfsFile - get() = vfs.root + get() = vfsResources.root - override val storageVfs: VfsFile - get() = VfsFile(vfs, "./.storage") + override val urlVfs: VfsFile + get() = vfsUrl.root + + override val applicationVfs: VfsFile + get() = vfsApplication.root + + override val kvStorage: KeyValueStorage = JvmKeyValueStorage(logger, "./.storage") override val platform: Platform = Platform.DESKTOP diff --git a/core/src/jvmMain/kotlin/com/littlekt/file/JvmApplicationVfs.kt b/core/src/jvmMain/kotlin/com/littlekt/file/JvmApplicationVfs.kt new file mode 100644 index 000000000..88f382b8c --- /dev/null +++ b/core/src/jvmMain/kotlin/com/littlekt/file/JvmApplicationVfs.kt @@ -0,0 +1,23 @@ +package com.littlekt.file + +import com.littlekt.Context +import com.littlekt.log.Logger +import java.io.File +import java.io.FileInputStream +import java.io.InputStream + +/** + * @author Colton Daily + * @date 8/23/2024 + */ +class JvmApplicationVfs(context: Context, logger: Logger, baseDir: String) : + JvmLocalVfs(context, logger, baseDir) { + + init { + HttpCache.initCache(File(".httpCache")) + } + + override fun openLocalStream(assetPath: String): InputStream { + return FileInputStream(assetPath) + } +} diff --git a/core/src/jvmMain/kotlin/com/littlekt/file/JvmKeyValueStorage.kt b/core/src/jvmMain/kotlin/com/littlekt/file/JvmKeyValueStorage.kt new file mode 100644 index 000000000..c4f74d323 --- /dev/null +++ b/core/src/jvmMain/kotlin/com/littlekt/file/JvmKeyValueStorage.kt @@ -0,0 +1,85 @@ +package com.littlekt.file + +import com.littlekt.log.Logger +import java.io.File +import java.io.FileOutputStream +import java.io.IOException +import kotlin.concurrent.thread +import kotlinx.serialization.Serializable +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json + +/** + * @author Colton Daily + * @date 8/23/2024 + */ +class JvmKeyValueStorage(private val logger: Logger, storageBaseDir: String) : KeyValueStorage { + private val storageDir = File(storageBaseDir) + private val keyValueStore = mutableMapOf() + + init { + if (!storageDir.exists() && !storageDir.mkdirs()) { + logger.error { "Failed to create storage directory at $storageBaseDir" } + } + + val persistentKvStorage = File(storageDir, KEY_VALUE_STORAGE_NAME) + if (persistentKvStorage.canRead()) { + try { + val kvStore = Json.decodeFromString(persistentKvStorage.readText()) + kvStore.keyValues.forEach { (k, v) -> keyValueStore[k] = v } + } catch (e: Exception) { + logger.error { "Failed loading key value store: $e" } + e.printStackTrace() + } + } + Runtime.getRuntime() + .addShutdownHook( + thread(false) { + val kvStore = KeyValueStore(keyValueStore.map { (k, v) -> KeyValueEntry(k, v) }) + File(storageDir, KEY_VALUE_STORAGE_NAME).writeText(Json.encodeToString(kvStore)) + } + ) + } + + override fun store(key: String, data: ByteArray): Boolean { + return try { + val file = File(storageDir, key) + FileOutputStream(file).use { it.write(data) } + logger.debug { "Wrote to ${file.absolutePath}" } + true + } catch (e: IOException) { + e.printStackTrace() + false + } + } + + override fun store(key: String, data: String): Boolean { + keyValueStore[key] = data + return true + } + + override fun load(key: String): ByteBuffer? { + val file = File(storageDir, key) + if (!file.canRead()) { + return null + } + return try { + ByteBufferImpl(file.readBytes()) + } catch (e: IOException) { + e.printStackTrace() + null + } + } + + override fun loadString(key: String): String? { + return keyValueStore[key] + } + + companion object { + private const val KEY_VALUE_STORAGE_NAME = ".keyValueStorage.json" + } + + @Serializable private data class KeyValueEntry(val k: String, val v: String) + + @Serializable private data class KeyValueStore(val keyValues: List) +} diff --git a/core/src/jvmMain/kotlin/com/littlekt/file/JvmLocalVfs.kt b/core/src/jvmMain/kotlin/com/littlekt/file/JvmLocalVfs.kt new file mode 100644 index 000000000..9922612a3 --- /dev/null +++ b/core/src/jvmMain/kotlin/com/littlekt/file/JvmLocalVfs.kt @@ -0,0 +1,51 @@ +package com.littlekt.file + +import com.littlekt.Context +import com.littlekt.log.Logger +import java.io.InputStream +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +/** + * @author Colton Daily + * @date 8/23/2024 + */ +abstract class JvmLocalVfs(context: Context, logger: Logger, baseDir: String) : + LocalVfs(context, logger, baseDir) { + + override suspend fun loadRawAsset(rawRef: RawAssetRef): LoadedRawAsset { + check(rawRef.isLocal) { "Only local resource assets may be loaded!" } + return loadLocalRaw(rawRef) + } + + override suspend fun loadSequenceStreamAsset( + sequenceRef: SequenceAssetRef + ): SequenceStreamCreatedAsset { + var sequence: ByteSequenceStream? = null + + withContext(Dispatchers.IO) { + try { + openLocalStream(sequenceRef.url).let { sequence = JvmByteSequenceStream(it) } + } catch (e: Exception) { + logger.error { + "Failed loading creating buffered sequence of ${sequenceRef.url}: $e" + } + } + } + return SequenceStreamCreatedAsset(sequenceRef, sequence) + } + + private suspend fun loadLocalRaw(localRawRef: RawAssetRef): LoadedRawAsset { + var data: ByteBufferImpl? = null + withContext(Dispatchers.IO) { + try { + openLocalStream(localRawRef.url).use { data = ByteBufferImpl(it.readBytes()) } + } catch (e: Exception) { + logger.error { "Failed loading asset ${localRawRef.url}: $e" } + } + } + return LoadedRawAsset(localRawRef, data) + } + + protected abstract fun openLocalStream(assetPath: String): InputStream +} diff --git a/core/src/jvmMain/kotlin/com/littlekt/file/JvmResourcesVfs.kt b/core/src/jvmMain/kotlin/com/littlekt/file/JvmResourcesVfs.kt new file mode 100644 index 000000000..d3791735c --- /dev/null +++ b/core/src/jvmMain/kotlin/com/littlekt/file/JvmResourcesVfs.kt @@ -0,0 +1,23 @@ +package com.littlekt.file + +import com.littlekt.Context +import com.littlekt.log.Logger +import java.io.File +import java.io.FileInputStream +import java.io.InputStream + +/** + * @author Colton Daily + * @date 8/23/2024 + */ +class JvmResourcesVfs(context: Context, logger: Logger) : JvmLocalVfs(context, logger, ".") { + + init { + HttpCache.initCache(File(".httpCache")) + } + + override fun openLocalStream(assetPath: String): InputStream { + return ClassLoader.getSystemResourceAsStream(assetPath.drop(2)) + ?: FileInputStream(assetPath) + } +} diff --git a/core/src/jvmMain/kotlin/com/littlekt/file/JvmUrlVfs.kt b/core/src/jvmMain/kotlin/com/littlekt/file/JvmUrlVfs.kt new file mode 100644 index 000000000..f2bd3b2be --- /dev/null +++ b/core/src/jvmMain/kotlin/com/littlekt/file/JvmUrlVfs.kt @@ -0,0 +1,77 @@ +package com.littlekt.file + +import com.littlekt.Context +import com.littlekt.log.Logger +import java.io.File +import java.io.FileInputStream +import java.io.IOException +import java.io.InputStream +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +/** + * @author Colton Daily + * @date 8/23/2024 + */ +class JvmUrlVfs(context: Context, logger: Logger) : UrlVfs(context, logger) { + + init { + HttpCache.initCache(File(".httpCache")) + } + + override suspend fun loadRawAsset(rawRef: RawAssetRef): LoadedRawAsset { + check(!rawRef.isLocal) { "Only http resource assets may be loaded!" } + return loadHttpRaw(rawRef) + } + + override suspend fun loadSequenceStreamAsset( + sequenceRef: SequenceAssetRef + ): SequenceStreamCreatedAsset { + var sequence: ByteSequenceStream? = null + + withContext(Dispatchers.IO) { + try { + openLocalStream(sequenceRef.url).let { sequence = JvmByteSequenceStream(it) } + } catch (e: Exception) { + logger.error { + "Failed loading creating buffered sequence of ${sequenceRef.url}: $e" + } + } + } + return SequenceStreamCreatedAsset(sequenceRef, sequence) + } + + private suspend fun loadHttpRaw(httpRawRef: RawAssetRef): LoadedRawAsset { + var data: ByteBufferImpl? = null + + if (httpRawRef.url.startsWith("data:", true)) { + data = decodeDataUrl(httpRawRef.url) + } else { + withContext(Dispatchers.IO) { + try { + val f = + HttpCache.loadHttpResource(httpRawRef.url) + ?: throw IOException("Failed downloading ${httpRawRef.url}") + runCatching { FileInputStream(f).use { data = ByteBufferImpl(it.readBytes()) } } + } catch (e: Exception) { + logger.error { "Failed loading asset ${httpRawRef.url}: $e" } + } + } + } + return LoadedRawAsset(httpRawRef, data) + } + + private fun decodeDataUrl(dataUrl: String): ByteBufferImpl { + val dataIdx = dataUrl.indexOf(";base64,") + 8 + return ByteBufferImpl(java.util.Base64.getDecoder().decode(dataUrl.substring(dataIdx))) + } + + private fun openLocalStream(assetPath: String): InputStream { + var inStream = ClassLoader.getSystemResourceAsStream(assetPath) + if (inStream == null) { + // if asset wasn't found in resources try to load it from file system + inStream = FileInputStream(assetPath) + } + return inStream + } +} diff --git a/core/src/jvmMain/kotlin/com/littlekt/file/JvmVfs.kt b/core/src/jvmMain/kotlin/com/littlekt/file/JvmVfs.kt deleted file mode 100644 index 48d8818a0..000000000 --- a/core/src/jvmMain/kotlin/com/littlekt/file/JvmVfs.kt +++ /dev/null @@ -1,160 +0,0 @@ -package com.littlekt.file - -import com.littlekt.Context -import com.littlekt.log.Logger -import java.io.* -import kotlin.concurrent.thread -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext -import kotlinx.serialization.Serializable -import kotlinx.serialization.encodeToString -import kotlinx.serialization.json.Json - -/** - * @author Colton Daily - * @date 11/6/2021 - */ -class JvmVfs(context: Context, logger: Logger, storageBaseDir: String, assetsBaseDir: String) : - Vfs(context, logger, assetsBaseDir) { - - private val storageDir = File(storageBaseDir) - private val keyValueStore = mutableMapOf() - - init { - HttpCache.initCache(File(".httpCache")) - if (!storageDir.exists() && !storageDir.mkdirs()) { - logger.error { "Failed to create storage directory at $storageBaseDir" } - } - - val persistentKvStorage = File(storageDir, KEY_VALUE_STORAGE_NAME) - if (persistentKvStorage.canRead()) { - try { - val kvStore = Json.decodeFromString(persistentKvStorage.readText()) - kvStore.keyValues.forEach { (k, v) -> keyValueStore[k] = v } - } catch (e: Exception) { - logger.error { "Failed loading key value store: $e" } - e.printStackTrace() - } - } - Runtime.getRuntime() - .addShutdownHook( - thread(false) { - val kvStore = KeyValueStore(keyValueStore.map { (k, v) -> KeyValueEntry(k, v) }) - File(storageDir, KEY_VALUE_STORAGE_NAME).writeText(Json.encodeToString(kvStore)) - } - ) - } - - override suspend fun loadRawAsset(rawRef: RawAssetRef): LoadedRawAsset { - return if (rawRef.isLocal) { - loadLocalRaw(rawRef) - } else { - loadHttpRaw(rawRef) - } - } - - override suspend fun loadSequenceStreamAsset( - sequenceRef: SequenceAssetRef - ): SequenceStreamCreatedAsset { - var sequence: ByteSequenceStream? = null - - withContext(Dispatchers.IO) { - try { - openLocalStream(sequenceRef.url).let { sequence = JvmByteSequenceStream(it) } - } catch (e: Exception) { - logger.error { - "Failed loading creating buffered sequence of ${sequenceRef.url}: $e" - } - } - } - return SequenceStreamCreatedAsset(sequenceRef, sequence) - } - - private suspend fun loadLocalRaw(localRawRef: RawAssetRef): LoadedRawAsset { - var data: ByteBufferImpl? = null - withContext(Dispatchers.IO) { - try { - openLocalStream(localRawRef.url).use { data = ByteBufferImpl(it.readBytes()) } - } catch (e: Exception) { - logger.error { "Failed loading asset ${localRawRef.url}: $e" } - } - } - return LoadedRawAsset(localRawRef, data) - } - - private suspend fun loadHttpRaw(httpRawRef: RawAssetRef): LoadedRawAsset { - var data: ByteBufferImpl? = null - - if (httpRawRef.url.startsWith("data:", true)) { - data = decodeDataUrl(httpRawRef.url) - } else { - withContext(Dispatchers.IO) { - try { - val f = - HttpCache.loadHttpResource(httpRawRef.url) - ?: throw IOException("Failed downloading ${httpRawRef.url}") - runCatching { FileInputStream(f).use { data = ByteBufferImpl(it.readBytes()) } } - } catch (e: Exception) { - logger.error { "Failed loading asset ${httpRawRef.url}: $e" } - } - } - } - return LoadedRawAsset(httpRawRef, data) - } - - private fun decodeDataUrl(dataUrl: String): ByteBufferImpl { - val dataIdx = dataUrl.indexOf(";base64,") + 8 - return ByteBufferImpl(java.util.Base64.getDecoder().decode(dataUrl.substring(dataIdx))) - } - - private fun openLocalStream(assetPath: String): InputStream { - var inStream = ClassLoader.getSystemResourceAsStream(assetPath) - if (inStream == null) { - // if asset wasn't found in resources try to load it from file system - inStream = FileInputStream(assetPath) - } - return inStream - } - - override fun store(key: String, data: ByteArray): Boolean { - return try { - val file = File(storageDir, key) - FileOutputStream(file).use { it.write(data) } - logger.debug { "Wrote to ${file.absolutePath}" } - true - } catch (e: IOException) { - e.printStackTrace() - false - } - } - - override fun store(key: String, data: String): Boolean { - keyValueStore[key] = data - return true - } - - override fun load(key: String): ByteBuffer? { - val file = File(storageDir, key) - if (!file.canRead()) { - return null - } - return try { - ByteBufferImpl(file.readBytes()) - } catch (e: IOException) { - e.printStackTrace() - null - } - } - - override fun loadString(key: String): String? { - return keyValueStore[key] - } - - companion object { - private const val KEY_VALUE_STORAGE_NAME = ".keyValueStorage.json" - } - - @Serializable data class KeyValueEntry(val k: String, val v: String) - - @Serializable data class KeyValueStore(val keyValues: List) -}