diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index f8e0763a..6851a009 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -85,8 +85,8 @@ jobs: name: test-results-ios path: '**/build/test-results/**/TEST-*.xml' - wasmjs-tests: - name: WasmJs Tests + js-tests: + name: JS Tests runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 @@ -98,20 +98,20 @@ jobs: - uses: gradle/actions/setup-gradle@v5 - - name: Run WasmJs tests - run: ./gradlew wasmJsBrowserTest wasmJsNodeTest + - name: Run JS tests + run: ./gradlew jsNodeTest - name: Upload test results if: always() uses: actions/upload-artifact@v6 with: - name: test-results-wasmjs + name: test-results-js path: '**/build/test-results/**/TEST-*.xml' publish-results: name: Publish Test Results runs-on: ubuntu-latest - needs: [ jvm-tests, android-tests, ios-tests, wasmjs-tests ] + needs: [ jvm-tests, android-tests, ios-tests, js-tests ] if: always() steps: - name: Download test results diff --git a/app/android/build.gradle.kts b/app/android/build.gradle.kts index d59772b9..0436fe5a 100644 --- a/app/android/build.gradle.kts +++ b/app/android/build.gradle.kts @@ -84,6 +84,9 @@ dependencies { implementation(projects.app.shared) implementation(projects.ai.discover) implementation(projects.library.core) + implementation(projects.library.ktor) + implementation(projects.library.ftp) + implementation(projects.library.torrent) implementation(projects.library.sqlite) implementation(projects.library.server) implementation(libs.androidx.activity.compose) diff --git a/app/android/src/main/kotlin/com/linroid/ketch/app/android/KetchService.kt b/app/android/src/main/kotlin/com/linroid/ketch/app/android/KetchService.kt index 74ef46c8..3d7a1992 100644 --- a/app/android/src/main/kotlin/com/linroid/ketch/app/android/KetchService.kt +++ b/app/android/src/main/kotlin/com/linroid/ketch/app/android/KetchService.kt @@ -16,6 +16,7 @@ import com.linroid.ketch.ai.AiConfig import com.linroid.ketch.ai.AiModule import com.linroid.ketch.ai.LlmConfig import com.linroid.ketch.api.log.KetchLogger +import com.linroid.ketch.api.log.Logger import com.linroid.ketch.app.instance.InstanceFactory import com.linroid.ketch.app.instance.InstanceManager import com.linroid.ketch.app.instance.LocalServerHandle @@ -23,9 +24,13 @@ import com.linroid.ketch.app.instance.ServerState import com.linroid.ketch.app.state.AiDiscoveryProvider import com.linroid.ketch.app.state.EmbeddedAiDiscoveryProvider import com.linroid.ketch.config.FileConfigStore +import com.linroid.ketch.core.Ketch +import com.linroid.ketch.engine.KtorHttpEngine +import com.linroid.ketch.ftp.FtpDownloadSource import com.linroid.ketch.server.KetchServer import com.linroid.ketch.sqlite.DriverFactory import com.linroid.ketch.sqlite.createSqliteTaskStore +import com.linroid.ketch.torrent.TorrentDownloadSource import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -86,9 +91,20 @@ class KetchService : Service() { ?: android.os.Build.MODEL instanceManager = InstanceManager( factory = InstanceFactory( - taskStore = taskStore, - downloadConfig = downloadConfig, deviceName = instanceName, + embeddedFactory = { + Ketch( + httpEngine = KtorHttpEngine(), + taskStore = taskStore, + config = downloadConfig, + name = instanceName, + logger = Logger.console(), + additionalSources = listOf( + FtpDownloadSource(), + TorrentDownloadSource(), + ), + ) + }, localServerFactory = { ketchApi -> val serverConfig = config.server log.i { "Starting local server on port ${serverConfig.port}" } diff --git a/app/desktop/build.gradle.kts b/app/desktop/build.gradle.kts index f86b2492..6cf9c2ff 100644 --- a/app/desktop/build.gradle.kts +++ b/app/desktop/build.gradle.kts @@ -11,8 +11,11 @@ dependencies { implementation(projects.config) implementation(projects.app.shared) implementation(projects.ai.discover) - implementation(projects.library.server) + implementation(projects.library.core) implementation(projects.library.ktor) + implementation(projects.library.ftp) + implementation(projects.library.torrent) + implementation(projects.library.server) implementation(projects.library.sqlite) implementation(compose.desktop.currentOs) implementation(libs.kotlinx.coroutinesSwing) diff --git a/app/desktop/src/main/kotlin/com/linroid/ketch/app/desktop/main.kt b/app/desktop/src/main/kotlin/com/linroid/ketch/app/desktop/main.kt index c2690f44..53fe10af 100644 --- a/app/desktop/src/main/kotlin/com/linroid/ketch/app/desktop/main.kt +++ b/app/desktop/src/main/kotlin/com/linroid/ketch/app/desktop/main.kt @@ -8,6 +8,7 @@ import androidx.compose.ui.window.application import com.linroid.ketch.ai.AiConfig import com.linroid.ketch.ai.AiModule import com.linroid.ketch.ai.LlmConfig +import com.linroid.ketch.api.log.Logger import com.linroid.ketch.app.App import com.linroid.ketch.app.instance.InstanceFactory import com.linroid.ketch.app.instance.InstanceManager @@ -15,9 +16,13 @@ import com.linroid.ketch.app.instance.LocalServerHandle import com.linroid.ketch.app.state.EmbeddedAiDiscoveryProvider import com.linroid.ketch.config.FileConfigStore import com.linroid.ketch.config.defaultConfigDir +import com.linroid.ketch.core.Ketch +import com.linroid.ketch.engine.KtorHttpEngine +import com.linroid.ketch.ftp.FtpDownloadSource import com.linroid.ketch.server.KetchServer import com.linroid.ketch.sqlite.DriverFactory import com.linroid.ketch.sqlite.createSqliteTaskStore +import com.linroid.ketch.torrent.TorrentDownloadSource import java.io.File import java.net.InetAddress @@ -40,9 +45,20 @@ fun main() = application { ?: InetAddress.getLocalHost().hostName.removeSuffix(".local") InstanceManager( factory = InstanceFactory( - taskStore = taskStore, - downloadConfig = downloadConfig, deviceName = instanceName, + embeddedFactory = { + Ketch( + httpEngine = KtorHttpEngine(), + taskStore = taskStore, + config = downloadConfig, + name = instanceName, + logger = Logger.console(), + additionalSources = listOf( + FtpDownloadSource(), + TorrentDownloadSource(), + ), + ) + }, localServerFactory = { ketchApi -> val serverConfig = config.server val server = KetchServer( diff --git a/app/shared/build.gradle.kts b/app/shared/build.gradle.kts index be9d4a28..2dcb48fa 100644 --- a/app/shared/build.gradle.kts +++ b/app/shared/build.gradle.kts @@ -40,9 +40,8 @@ kotlin { sourceSets { commonMain.dependencies { implementation(projects.config) - implementation(projects.library.core) implementation(projects.library.remote) - implementation(projects.library.ktor) + implementation(libs.kotlinx.coroutines.core) implementation(libs.compose.runtime) implementation(libs.compose.foundation) @@ -61,6 +60,8 @@ kotlin { implementation(libs.kotlinx.coroutines.test) } androidMain.dependencies { + implementation(projects.library.core) + implementation(projects.library.ktor) implementation(projects.ai.discover) implementation(projects.library.ftp) implementation(projects.library.torrent) @@ -69,6 +70,8 @@ kotlin { implementation(libs.dnssd) } iosMain.dependencies { + implementation(projects.library.core) + implementation(projects.library.ktor) implementation(projects.library.ftp) implementation(projects.library.torrent) implementation(projects.library.sqlite) @@ -76,6 +79,8 @@ kotlin { implementation(libs.dnssd) } jvmMain.dependencies { + implementation(projects.library.core) + implementation(projects.library.ktor) implementation(projects.ai.discover) implementation(projects.library.ftp) implementation(projects.library.torrent) diff --git a/app/shared/src/androidMain/kotlin/com/linroid/ketch/app/instance/PlatformSources.android.kt b/app/shared/src/androidMain/kotlin/com/linroid/ketch/app/instance/PlatformSources.android.kt deleted file mode 100644 index 85853d09..00000000 --- a/app/shared/src/androidMain/kotlin/com/linroid/ketch/app/instance/PlatformSources.android.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.linroid.ketch.app.instance - -import com.linroid.ketch.core.engine.DownloadSource -import com.linroid.ketch.ftp.FtpDownloadSource -import com.linroid.ketch.torrent.TorrentDownloadSource - -internal actual fun platformAdditionalSources(): List = - listOf(FtpDownloadSource(), TorrentDownloadSource()) diff --git a/app/shared/src/commonMain/kotlin/com/linroid/ketch/app/instance/InstanceEntry.kt b/app/shared/src/commonMain/kotlin/com/linroid/ketch/app/instance/InstanceEntry.kt index 7ccc9e95..5757ba4f 100644 --- a/app/shared/src/commonMain/kotlin/com/linroid/ketch/app/instance/InstanceEntry.kt +++ b/app/shared/src/commonMain/kotlin/com/linroid/ketch/app/instance/InstanceEntry.kt @@ -2,7 +2,6 @@ package com.linroid.ketch.app.instance import com.linroid.ketch.api.KetchApi import com.linroid.ketch.config.RemoteConfig -import com.linroid.ketch.core.Ketch import com.linroid.ketch.remote.ConnectionState import com.linroid.ketch.remote.RemoteKetch import kotlinx.coroutines.flow.StateFlow @@ -13,7 +12,7 @@ interface InstanceEntry { } data class EmbeddedInstance( - override val instance: Ketch, + override val instance: KetchApi, override val label: String, ) : InstanceEntry diff --git a/app/shared/src/commonMain/kotlin/com/linroid/ketch/app/instance/InstanceFactory.kt b/app/shared/src/commonMain/kotlin/com/linroid/ketch/app/instance/InstanceFactory.kt index 8db1e98b..5320aea4 100644 --- a/app/shared/src/commonMain/kotlin/com/linroid/ketch/app/instance/InstanceFactory.kt +++ b/app/shared/src/commonMain/kotlin/com/linroid/ketch/app/instance/InstanceFactory.kt @@ -1,48 +1,27 @@ package com.linroid.ketch.app.instance import com.linroid.ketch.api.KetchApi -import com.linroid.ketch.api.DownloadConfig -import com.linroid.ketch.api.log.Logger import com.linroid.ketch.config.RemoteConfig -import com.linroid.ketch.core.Ketch -import com.linroid.ketch.core.engine.DownloadSource -import com.linroid.ketch.core.task.TaskStore -import com.linroid.ketch.engine.KtorHttpEngine import com.linroid.ketch.remote.RemoteKetch /** * Creates [KetchApi] instances for each instance type. * - * @param taskStore persistent storage for download task records. - * Required when using the default embedded instance. Pass `null` - * for remote-only mode (e.g. wasmJs/web). * @param deviceName label for the embedded instance (e.g. device * model on Android, hostname on desktop). - * @param embeddedFactory factory for creating the embedded Ketch + * @param embeddedFactory factory for creating the embedded [KetchApi] * instance. When `null`, no embedded instance is available and - * [InstanceManager] starts in remote-only mode. - * Override in tests to inject fakes. + * [InstanceManager] starts in remote-only mode (e.g. wasmJs/web). + * Each platform provides its own factory that wires up the core + * engine with platform-specific dependencies. * @param localServerFactory optional factory that starts an HTTP - * server exposing the embedded [KetchApi]. Receives port, - * optional API token, and the embedded KetchApi instance. - * When non-null, server controls appear in the Embedded - * instance entry. Provided by Android and JVM/Desktop. + * server exposing the embedded [KetchApi]. Receives the embedded + * KetchApi instance. When non-null, server controls appear in + * the Embedded instance entry. Provided by Android and JVM/Desktop. */ class InstanceFactory( - taskStore: TaskStore? = null, - defaultDirectory: String? = null, - downloadConfig: DownloadConfig = DownloadConfig( - defaultDirectory = defaultDirectory, - ), val deviceName: String = "Embedded", - additionalSources: List = platformAdditionalSources(), - private val embeddedFactory: (() -> Ketch)? = taskStore?.let { ts -> - { - createDefaultEmbeddedKetch( - ts, downloadConfig, deviceName, additionalSources, - ) - } - }, + private val embeddedFactory: (() -> KetchApi)? = null, private val localServerFactory: ((KetchApi) -> LocalServerHandle)? = null, ) { /** Whether an embedded instance is available. */ @@ -54,7 +33,7 @@ class InstanceFactory( private var localServer: LocalServerHandle? = null - /** Create the embedded Ketch instance. */ + /** Create the embedded [KetchApi] instance. */ fun createEmbedded(): EmbeddedInstance { val ketch = embeddedFactory?.invoke() ?: throw UnsupportedOperationException( @@ -106,21 +85,3 @@ class InstanceFactory( localServer = null } } - -internal expect fun platformAdditionalSources(): List - -private fun createDefaultEmbeddedKetch( - taskStore: TaskStore, - config: DownloadConfig, - name: String, - additionalSources: List, -): Ketch { - return Ketch( - httpEngine = KtorHttpEngine(), - taskStore = taskStore, - config = config, - name = name, - logger = Logger.console(), - additionalSources = additionalSources, - ) -} diff --git a/app/shared/src/commonMain/kotlin/com/linroid/ketch/app/instance/InstanceManager.kt b/app/shared/src/commonMain/kotlin/com/linroid/ketch/app/instance/InstanceManager.kt index dc294631..fb37cce8 100644 --- a/app/shared/src/commonMain/kotlin/com/linroid/ketch/app/instance/InstanceManager.kt +++ b/app/shared/src/commonMain/kotlin/com/linroid/ketch/app/instance/InstanceManager.kt @@ -8,7 +8,6 @@ import com.linroid.ketch.api.ResolvedSource import com.linroid.ketch.api.DownloadConfig import com.linroid.ketch.config.ConfigStore import com.linroid.ketch.config.RemoteConfig -import com.linroid.ketch.core.Ketch import com.linroid.ketch.remote.RemoteKetch import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -24,7 +23,7 @@ import kotlinx.coroutines.launch * instance, and lifecycle transitions. * * When [InstanceFactory.hasEmbedded] is `true` (Android, iOS, - * JVM/Desktop), an embedded [Ketch] instance is created once + * JVM/Desktop), an embedded [KetchApi] instance is created once * and reused. An optional HTTP server can be started/stopped * to expose the same instance over the network. * diff --git a/app/shared/src/commonTest/kotlin/com/linroid/ketch/app/FakeInstanceFactory.kt b/app/shared/src/commonTest/kotlin/com/linroid/ketch/app/FakeInstanceFactory.kt index 7f13ae37..daf412f9 100644 --- a/app/shared/src/commonTest/kotlin/com/linroid/ketch/app/FakeInstanceFactory.kt +++ b/app/shared/src/commonTest/kotlin/com/linroid/ketch/app/FakeInstanceFactory.kt @@ -4,7 +4,6 @@ import com.linroid.ketch.api.KetchApi import com.linroid.ketch.config.RemoteConfig import com.linroid.ketch.app.instance.EmbeddedInstance import com.linroid.ketch.app.instance.RemoteInstance -import com.linroid.ketch.core.Ketch import com.linroid.ketch.remote.RemoteKetch /** @@ -50,12 +49,8 @@ class FakeInstanceFactory( ) } val api = embeddedFactory() - // In tests we use FakeKetchApi which is not a real Ketch, - // so we cast unsafely. Real tests needing Ketch type - // should provide a real Ketch instance. - @Suppress("UNCHECKED_CAST") return EmbeddedInstance( - instance = api as Ketch, + instance = api, label = label, ) } diff --git a/app/shared/src/iosMain/kotlin/com/linroid/ketch/app/MainViewController.kt b/app/shared/src/iosMain/kotlin/com/linroid/ketch/app/MainViewController.kt index 7c79c923..713e065e 100644 --- a/app/shared/src/iosMain/kotlin/com/linroid/ketch/app/MainViewController.kt +++ b/app/shared/src/iosMain/kotlin/com/linroid/ketch/app/MainViewController.kt @@ -3,11 +3,16 @@ package com.linroid.ketch.app import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.remember import androidx.compose.ui.window.ComposeUIViewController -import com.linroid.ketch.config.FileConfigStore +import com.linroid.ketch.api.log.Logger import com.linroid.ketch.app.instance.InstanceFactory import com.linroid.ketch.app.instance.InstanceManager +import com.linroid.ketch.config.FileConfigStore +import com.linroid.ketch.core.Ketch +import com.linroid.ketch.engine.KtorHttpEngine +import com.linroid.ketch.ftp.FtpDownloadSource import com.linroid.ketch.sqlite.DriverFactory import com.linroid.ketch.sqlite.createSqliteTaskStore +import com.linroid.ketch.torrent.TorrentDownloadSource import platform.Foundation.NSDocumentDirectory import platform.Foundation.NSSearchPathForDirectoriesInDomains import platform.Foundation.NSUserDomainMask @@ -31,9 +36,20 @@ fun MainViewController() = ComposeUIViewController { ?: UIDevice.currentDevice.name InstanceManager( factory = InstanceFactory( - taskStore = taskStore, - downloadConfig = downloadConfig, deviceName = instanceName, + embeddedFactory = { + Ketch( + httpEngine = KtorHttpEngine(), + taskStore = taskStore, + config = downloadConfig, + name = instanceName, + logger = Logger.console(), + additionalSources = listOf( + FtpDownloadSource(), + TorrentDownloadSource(), + ), + ) + }, ), initialRemotes = config.remotes, configStore = configStore, diff --git a/app/shared/src/iosMain/kotlin/com/linroid/ketch/app/instance/PlatformSources.ios.kt b/app/shared/src/iosMain/kotlin/com/linroid/ketch/app/instance/PlatformSources.ios.kt deleted file mode 100644 index 85853d09..00000000 --- a/app/shared/src/iosMain/kotlin/com/linroid/ketch/app/instance/PlatformSources.ios.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.linroid.ketch.app.instance - -import com.linroid.ketch.core.engine.DownloadSource -import com.linroid.ketch.ftp.FtpDownloadSource -import com.linroid.ketch.torrent.TorrentDownloadSource - -internal actual fun platformAdditionalSources(): List = - listOf(FtpDownloadSource(), TorrentDownloadSource()) diff --git a/app/shared/src/jvmMain/kotlin/com/linroid/ketch/app/instance/PlatformSources.jvm.kt b/app/shared/src/jvmMain/kotlin/com/linroid/ketch/app/instance/PlatformSources.jvm.kt deleted file mode 100644 index 85853d09..00000000 --- a/app/shared/src/jvmMain/kotlin/com/linroid/ketch/app/instance/PlatformSources.jvm.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.linroid.ketch.app.instance - -import com.linroid.ketch.core.engine.DownloadSource -import com.linroid.ketch.ftp.FtpDownloadSource -import com.linroid.ketch.torrent.TorrentDownloadSource - -internal actual fun platformAdditionalSources(): List = - listOf(FtpDownloadSource(), TorrentDownloadSource()) diff --git a/app/shared/src/wasmJsMain/kotlin/com/linroid/ketch/app/instance/PlatformSources.wasmJs.kt b/app/shared/src/wasmJsMain/kotlin/com/linroid/ketch/app/instance/PlatformSources.wasmJs.kt deleted file mode 100644 index 88e8d5b8..00000000 --- a/app/shared/src/wasmJsMain/kotlin/com/linroid/ketch/app/instance/PlatformSources.wasmJs.kt +++ /dev/null @@ -1,6 +0,0 @@ -package com.linroid.ketch.app.instance - -import com.linroid.ketch.core.engine.DownloadSource - -internal actual fun platformAdditionalSources(): List = - emptyList() diff --git a/cli/build.gradle.kts b/cli/build.gradle.kts index 880786ee..82253d1e 100644 --- a/cli/build.gradle.kts +++ b/cli/build.gradle.kts @@ -27,7 +27,7 @@ graalvmNative { "--no-fallback", "-Ob", "-H:+ReportExceptionStackTraces", - "--initialize-at-build-time=io.ktor,kotlin,kotlinx.coroutines,kotlinx.serialization,kotlinx.io", + "--initialize-at-build-time=io.ktor,kotlin,kotlinx.coroutines,kotlinx.serialization,okio", "--initialize-at-build-time=ch.qos.logback", "--initialize-at-build-time=org.slf4j", "--initialize-at-run-time=kotlin.uuid.SecureRandomHolder", diff --git a/config/build.gradle.kts b/config/build.gradle.kts index faec3e87..515c0109 100644 --- a/config/build.gradle.kts +++ b/config/build.gradle.kts @@ -29,7 +29,7 @@ kotlin { sourceSets { commonMain.dependencies { api(projects.library.api) - implementation(libs.kotlinx.io.core) + implementation(libs.okio) implementation(libs.ktoml.core) } commonTest.dependencies { diff --git a/config/src/androidMain/kotlin/com/linroid/ketch/config/PlatformFileSystem.android.kt b/config/src/androidMain/kotlin/com/linroid/ketch/config/PlatformFileSystem.android.kt new file mode 100644 index 00000000..332842af --- /dev/null +++ b/config/src/androidMain/kotlin/com/linroid/ketch/config/PlatformFileSystem.android.kt @@ -0,0 +1,5 @@ +package com.linroid.ketch.config + +import okio.FileSystem + +internal actual val platformFileSystem: FileSystem = FileSystem.SYSTEM diff --git a/config/src/commonMain/kotlin/com/linroid/ketch/config/FileConfigStore.kt b/config/src/commonMain/kotlin/com/linroid/ketch/config/FileConfigStore.kt index 2e6da058..918f75f5 100644 --- a/config/src/commonMain/kotlin/com/linroid/ketch/config/FileConfigStore.kt +++ b/config/src/commonMain/kotlin/com/linroid/ketch/config/FileConfigStore.kt @@ -1,10 +1,7 @@ package com.linroid.ketch.config -import kotlinx.io.buffered -import kotlinx.io.files.Path -import kotlinx.io.files.SystemFileSystem -import kotlinx.io.readString -import kotlinx.io.writeString +import okio.Path.Companion.toPath +import okio.buffer /** * File-based [ConfigStore] that persists config as TOML. @@ -18,34 +15,41 @@ class FileConfigStore(private val path: String) : ConfigStore { private val tmpPath = "$path.tmp" override fun load(): KetchConfig { - val file = Path(path) - val tmp = Path(tmpPath) - if (!SystemFileSystem.exists(file) && - SystemFileSystem.exists(tmp) + val file = path.toPath() + val tmp = tmpPath.toPath() + if (!platformFileSystem.exists(file) && + platformFileSystem.exists(tmp) ) { // Previous save wrote .tmp but didn't rename — recover - SystemFileSystem.atomicMove(tmp, file) - } else if (SystemFileSystem.exists(tmp)) { + platformFileSystem.atomicMove(tmp, file) + } else if (platformFileSystem.exists(tmp)) { // Main file exists, leftover .tmp is stale — remove - SystemFileSystem.delete(tmp) + platformFileSystem.delete(tmp) + } + if (!platformFileSystem.exists(file)) return KetchConfig() + val source = platformFileSystem.source(file).buffer() + val content = try { + source.readUtf8() + } finally { + source.close() } - if (!SystemFileSystem.exists(file)) return KetchConfig() - val content = SystemFileSystem.source(file).buffered() - .use { it.readString() } return ConfigStore.toml.decodeFromString( KetchConfig.serializer(), content, ) } override fun save(config: KetchConfig) { - val file = Path(path) - val tmp = Path(tmpPath) - file.parent?.let { SystemFileSystem.createDirectories(it) } - SystemFileSystem.sink(tmp).buffered().use { sink -> + val file = path.toPath() + val tmp = tmpPath.toPath() + file.parent?.let { platformFileSystem.createDirectories(it) } + val sink = platformFileSystem.sink(tmp).buffer() + try { val encoded = ConfigStore.toml .encodeToString(KetchConfig.serializer(), config) - sink.writeString(encoded) + sink.writeUtf8(encoded) + } finally { + sink.close() } - SystemFileSystem.atomicMove(tmp, file) + platformFileSystem.atomicMove(tmp, file) } } diff --git a/config/src/commonMain/kotlin/com/linroid/ketch/config/PlatformFileSystem.kt b/config/src/commonMain/kotlin/com/linroid/ketch/config/PlatformFileSystem.kt new file mode 100644 index 00000000..6975e09d --- /dev/null +++ b/config/src/commonMain/kotlin/com/linroid/ketch/config/PlatformFileSystem.kt @@ -0,0 +1,5 @@ +package com.linroid.ketch.config + +import okio.FileSystem + +internal expect val platformFileSystem: FileSystem diff --git a/config/src/iosMain/kotlin/com/linroid/ketch/config/PlatformFileSystem.ios.kt b/config/src/iosMain/kotlin/com/linroid/ketch/config/PlatformFileSystem.ios.kt new file mode 100644 index 00000000..332842af --- /dev/null +++ b/config/src/iosMain/kotlin/com/linroid/ketch/config/PlatformFileSystem.ios.kt @@ -0,0 +1,5 @@ +package com.linroid.ketch.config + +import okio.FileSystem + +internal actual val platformFileSystem: FileSystem = FileSystem.SYSTEM diff --git a/config/src/jvmMain/kotlin/com/linroid/ketch/config/PlatformFileSystem.jvm.kt b/config/src/jvmMain/kotlin/com/linroid/ketch/config/PlatformFileSystem.jvm.kt new file mode 100644 index 00000000..332842af --- /dev/null +++ b/config/src/jvmMain/kotlin/com/linroid/ketch/config/PlatformFileSystem.jvm.kt @@ -0,0 +1,5 @@ +package com.linroid.ketch.config + +import okio.FileSystem + +internal actual val platformFileSystem: FileSystem = FileSystem.SYSTEM diff --git a/config/src/wasmJsMain/kotlin/com/linroid/ketch/config/PlatformFileSystem.wasmJs.kt b/config/src/wasmJsMain/kotlin/com/linroid/ketch/config/PlatformFileSystem.wasmJs.kt new file mode 100644 index 00000000..1bd85943 --- /dev/null +++ b/config/src/wasmJsMain/kotlin/com/linroid/ketch/config/PlatformFileSystem.wasmJs.kt @@ -0,0 +1,8 @@ +package com.linroid.ketch.config + +import okio.FileSystem + +internal actual val platformFileSystem: FileSystem + get() = throw UnsupportedOperationException( + "FileSystem is not supported on Wasm/JS platform" + ) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 1dfe1922..5fae8b71 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -17,7 +17,7 @@ junit = "4.13.2" kotlin = "2.3.10" kotlinx-coroutines = "1.10.2" kotlinx-serialization = "1.10.0" -kotlinx-io = "0.9.0" +okio = "3.16.4" kotlinx-datetime = "0.7.1" kermit = "2.0.8" koog = "0.6.2" @@ -56,7 +56,9 @@ compose-uiToolingPreview = { module = "org.jetbrains.compose.ui:ui-tooling-previ kotlinx-browser = { module = "org.jetbrains.kotlinx:kotlinx-browser", version.ref = "kotlinx-browser" } kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinx-coroutines" } kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization" } -kotlinx-io-core = { module = "org.jetbrains.kotlinx:kotlinx-io-core", version.ref = "kotlinx-io" } +okio = { module = "com.squareup.okio:okio", version.ref = "okio" } +okio-wasifilesystem = { module = "com.squareup.okio:okio-wasifilesystem", version.ref = "okio" } +okio-nodefilesystem = { module = "com.squareup.okio:okio-nodefilesystem", version.ref = "okio" } kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "kotlinx-datetime" } kermit = { module = "co.touchlab:kermit", version.ref = "kermit" } koog-agents = { module = "ai.koog:koog-agents", version.ref = "koog" } diff --git a/library/api/build.gradle.kts b/library/api/build.gradle.kts index 9f4827e7..92295a10 100644 --- a/library/api/build.gradle.kts +++ b/library/api/build.gradle.kts @@ -57,9 +57,17 @@ kotlin { iosSimulatorArm64() jvm() + js { + browser() + nodejs() + } + @OptIn(ExperimentalWasmDsl::class) wasmJs { browser() } + @OptIn(ExperimentalWasmDsl::class) + wasmWasi { nodejs() } + sourceSets { commonMain { kotlin.srcDir(generateVersion) diff --git a/library/api/src/jsMain/kotlin/com/linroid/ketch/api/Destination.js.kt b/library/api/src/jsMain/kotlin/com/linroid/ketch/api/Destination.js.kt new file mode 100644 index 00000000..ca8fd198 --- /dev/null +++ b/library/api/src/jsMain/kotlin/com/linroid/ketch/api/Destination.js.kt @@ -0,0 +1,10 @@ +package com.linroid.ketch.api + +actual fun Destination.isFile(): Boolean = + !isName() && !isDirectory() + +actual fun Destination.isDirectory(): Boolean = + value.endsWith('/') + +actual fun Destination.isName(): Boolean = + !value.contains('/') diff --git a/library/api/src/jsMain/kotlin/com/linroid/ketch/api/log/Logger.js.kt b/library/api/src/jsMain/kotlin/com/linroid/ketch/api/log/Logger.js.kt new file mode 100644 index 00000000..bd792d2f --- /dev/null +++ b/library/api/src/jsMain/kotlin/com/linroid/ketch/api/log/Logger.js.kt @@ -0,0 +1,36 @@ +package com.linroid.ketch.api.log + +internal actual fun consoleLogger(minLevel: LogLevel): Logger = + object : Logger { + override fun v(message: String) { + if (minLevel <= LogLevel.VERBOSE) println("[VERBOSE] $message") + } + + override fun d(message: String) { + if (minLevel <= LogLevel.DEBUG) println("[DEBUG] $message") + } + + override fun i(message: String) { + if (minLevel <= LogLevel.INFO) println("[INFO] $message") + } + + override fun w(message: String, throwable: Throwable?) { + if (minLevel <= LogLevel.WARN) { + println("[WARN] $message") + throwable?.let { + println(" Exception: ${it.message}") + println(" ${it.stackTraceToString()}") + } + } + } + + override fun e(message: String, throwable: Throwable?) { + if (minLevel <= LogLevel.ERROR) { + println("[ERROR] $message") + throwable?.let { + println(" Exception: ${it.message}") + println(" ${it.stackTraceToString()}") + } + } + } + } diff --git a/library/api/src/wasmJsMain/kotlin/com/linroid/ketch/api/log/Logger.wasmJs.kt b/library/api/src/wasmJsMain/kotlin/com/linroid/ketch/api/log/Logger.wasmJs.kt index 1c8eed8f..bd792d2f 100644 --- a/library/api/src/wasmJsMain/kotlin/com/linroid/ketch/api/log/Logger.wasmJs.kt +++ b/library/api/src/wasmJsMain/kotlin/com/linroid/ketch/api/log/Logger.wasmJs.kt @@ -1,8 +1,5 @@ package com.linroid.ketch.api.log -import com.linroid.ketch.api.log.LogLevel -import com.linroid.ketch.api.log.Logger - internal actual fun consoleLogger(minLevel: LogLevel): Logger = object : Logger { override fun v(message: String) { diff --git a/library/api/src/wasmWasiMain/kotlin/com/linroid/ketch/api/Destination.wasmWasi.kt b/library/api/src/wasmWasiMain/kotlin/com/linroid/ketch/api/Destination.wasmWasi.kt new file mode 100644 index 00000000..ca8fd198 --- /dev/null +++ b/library/api/src/wasmWasiMain/kotlin/com/linroid/ketch/api/Destination.wasmWasi.kt @@ -0,0 +1,10 @@ +package com.linroid.ketch.api + +actual fun Destination.isFile(): Boolean = + !isName() && !isDirectory() + +actual fun Destination.isDirectory(): Boolean = + value.endsWith('/') + +actual fun Destination.isName(): Boolean = + !value.contains('/') diff --git a/library/api/src/wasmWasiMain/kotlin/com/linroid/ketch/api/log/Logger.wasmWasi.kt b/library/api/src/wasmWasiMain/kotlin/com/linroid/ketch/api/log/Logger.wasmWasi.kt new file mode 100644 index 00000000..bd792d2f --- /dev/null +++ b/library/api/src/wasmWasiMain/kotlin/com/linroid/ketch/api/log/Logger.wasmWasi.kt @@ -0,0 +1,36 @@ +package com.linroid.ketch.api.log + +internal actual fun consoleLogger(minLevel: LogLevel): Logger = + object : Logger { + override fun v(message: String) { + if (minLevel <= LogLevel.VERBOSE) println("[VERBOSE] $message") + } + + override fun d(message: String) { + if (minLevel <= LogLevel.DEBUG) println("[DEBUG] $message") + } + + override fun i(message: String) { + if (minLevel <= LogLevel.INFO) println("[INFO] $message") + } + + override fun w(message: String, throwable: Throwable?) { + if (minLevel <= LogLevel.WARN) { + println("[WARN] $message") + throwable?.let { + println(" Exception: ${it.message}") + println(" ${it.stackTraceToString()}") + } + } + } + + override fun e(message: String, throwable: Throwable?) { + if (minLevel <= LogLevel.ERROR) { + println("[ERROR] $message") + throwable?.let { + println(" Exception: ${it.message}") + println(" ${it.stackTraceToString()}") + } + } + } + } diff --git a/library/core/build.gradle.kts b/library/core/build.gradle.kts index 4dbcdaa8..2c1e9c43 100644 --- a/library/core/build.gradle.kts +++ b/library/core/build.gradle.kts @@ -41,8 +41,12 @@ kotlin { jvm() + js { + nodejs() + } + @OptIn(ExperimentalWasmDsl::class) - wasmJs { + wasmWasi { nodejs() } @@ -52,7 +56,7 @@ kotlin { implementation(libs.kotlinx.coroutines.core) implementation(libs.kotlinx.serialization.json) implementation(libs.kotlinx.datetime) - implementation(libs.kotlinx.io.core) + implementation(libs.okio) } commonTest.dependencies { implementation(libs.kotlin.test) @@ -61,6 +65,12 @@ kotlin { androidMain.dependencies { implementation(libs.androidx.startup) } + jsMain.dependencies { + implementation(libs.okio.nodefilesystem) + } + wasmWasiMain.dependencies { + implementation(libs.okio.wasifilesystem) + } } } diff --git a/library/core/src/androidMain/kotlin/com/linroid/ketch/core/file/JvmRandomAccessHandle.android.kt b/library/core/src/androidMain/kotlin/com/linroid/ketch/core/file/JvmRandomAccessHandle.android.kt deleted file mode 100644 index fd158da4..00000000 --- a/library/core/src/androidMain/kotlin/com/linroid/ketch/core/file/JvmRandomAccessHandle.android.kt +++ /dev/null @@ -1,25 +0,0 @@ -package com.linroid.ketch.core.file - -import java.io.RandomAccessFile - -internal class JvmRandomAccessHandle( - private val raf: RandomAccessFile, -) : RandomAccessHandle { - - override fun writeAt(offset: Long, data: ByteArray) { - raf.seek(offset) - raf.write(data) - } - - override fun flush() { - raf.fd.sync() - } - - override fun close() { - raf.close() - } - - override fun preallocate(size: Long) { - raf.setLength(size) - } -} diff --git a/library/core/src/androidMain/kotlin/com/linroid/ketch/core/file/PlatformFileSystem.android.kt b/library/core/src/androidMain/kotlin/com/linroid/ketch/core/file/PlatformFileSystem.android.kt new file mode 100644 index 00000000..d7f08292 --- /dev/null +++ b/library/core/src/androidMain/kotlin/com/linroid/ketch/core/file/PlatformFileSystem.android.kt @@ -0,0 +1,5 @@ +package com.linroid.ketch.core.file + +import okio.FileSystem + +internal actual val platformFileSystem: FileSystem = FileSystem.SYSTEM diff --git a/library/core/src/androidMain/kotlin/com/linroid/ketch/core/file/createFileAccessor.android.kt b/library/core/src/androidMain/kotlin/com/linroid/ketch/core/file/createFileAccessor.android.kt index 30ddd36b..a1a3d9c0 100644 --- a/library/core/src/androidMain/kotlin/com/linroid/ketch/core/file/createFileAccessor.android.kt +++ b/library/core/src/androidMain/kotlin/com/linroid/ketch/core/file/createFileAccessor.android.kt @@ -5,7 +5,6 @@ package com.linroid.ketch.core.file import android.net.Uri import com.linroid.ketch.core.AndroidContext import kotlinx.coroutines.CoroutineDispatcher -import java.io.RandomAccessFile /** * Creates a [FileAccessor] for the given [path], supporting both @@ -21,9 +20,7 @@ actual fun createFileAccessor( ): FileAccessor { val uri = Uri.parse(path) return if (uri.isRelative) { - PathFileAccessor(path, ioDispatcher) { realPath -> - JvmRandomAccessHandle(RandomAccessFile(realPath, "rw")) - } + PathFileAccessor(path, ioDispatcher) } else { ContentUriFileAccessor(AndroidContext.get(), uri, ioDispatcher) } diff --git a/library/core/src/androidMain/kotlin/com/linroid/ketch/core/file/resolveChildPath.android.kt b/library/core/src/androidMain/kotlin/com/linroid/ketch/core/file/resolveChildPath.android.kt index 6c8c25aa..e628adf7 100644 --- a/library/core/src/androidMain/kotlin/com/linroid/ketch/core/file/resolveChildPath.android.kt +++ b/library/core/src/androidMain/kotlin/com/linroid/ketch/core/file/resolveChildPath.android.kt @@ -5,7 +5,7 @@ package com.linroid.ketch.core.file import android.net.Uri import android.provider.DocumentsContract import com.linroid.ketch.core.AndroidContext -import kotlinx.io.files.Path +import okio.Path.Companion.toPath internal actual fun resolveChildPath( directory: String, @@ -31,5 +31,5 @@ internal actual fun resolveChildPath( ) return docUri.toString() } - return Path(directory, fileName).toString() + return (directory.toPath() / fileName).toString() } diff --git a/library/core/src/commonMain/kotlin/com/linroid/ketch/core/engine/DownloadExecution.kt b/library/core/src/commonMain/kotlin/com/linroid/ketch/core/engine/DownloadExecution.kt index ef593e94..c65a8186 100644 --- a/library/core/src/commonMain/kotlin/com/linroid/ketch/core/engine/DownloadExecution.kt +++ b/library/core/src/commonMain/kotlin/com/linroid/ketch/core/engine/DownloadExecution.kt @@ -16,6 +16,7 @@ import com.linroid.ketch.core.file.FileAccessor import com.linroid.ketch.core.file.FileNameResolver import com.linroid.ketch.core.file.NoOpFileAccessor import com.linroid.ketch.core.file.createFileAccessor +import com.linroid.ketch.core.file.platformFileSystem import com.linroid.ketch.core.file.resolveChildPath import com.linroid.ketch.core.task.TaskHandle import com.linroid.ketch.core.task.TaskRecord @@ -27,8 +28,8 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import kotlinx.io.files.Path -import kotlinx.io.files.SystemFileSystem +import okio.Path +import okio.Path.Companion.toPath import kotlin.time.Clock import kotlin.time.TimeSource @@ -511,7 +512,7 @@ internal class DownloadExecution( if (fileName == null) return directory val outputPath = resolveChildPath(directory, fileName) return if (deduplicate && !directory.contains("://")) { - deduplicatePath(Path(outputPath)).toString() + deduplicatePath(outputPath.toPath()).toString() } else { outputPath } @@ -529,7 +530,7 @@ internal class DownloadExecution( internal fun deduplicatePath(candidate: Path): Path { val fileName = candidate.name val directory = candidate.parent ?: return candidate - if (!SystemFileSystem.exists(candidate)) return candidate + if (!platformFileSystem.exists(candidate)) return candidate val dotIndex = fileName.lastIndexOf('.') val baseName: String @@ -544,8 +545,8 @@ internal class DownloadExecution( var seq = 1 while (true) { - val path = Path(directory, "$baseName ($seq)$extension") - if (!SystemFileSystem.exists(path)) return path + val path = directory / "$baseName ($seq)$extension" + if (!platformFileSystem.exists(path)) return path seq++ } } diff --git a/library/core/src/commonMain/kotlin/com/linroid/ketch/core/file/FileAccessor.kt b/library/core/src/commonMain/kotlin/com/linroid/ketch/core/file/FileAccessor.kt index d82e52fb..31fbe955 100644 --- a/library/core/src/commonMain/kotlin/com/linroid/ketch/core/file/FileAccessor.kt +++ b/library/core/src/commonMain/kotlin/com/linroid/ketch/core/file/FileAccessor.kt @@ -4,11 +4,11 @@ package com.linroid.ketch.core.file * Platform-specific random-access file writer. * * Each platform provides an implementation via [createFileAccessor]: - * - **Android/JVM**: `RandomAccessFile` with `Dispatchers.IO` - * - **iOS**: Foundation `NSFileHandle` / `NSFileManager` with `Dispatchers.IO` + * - **Android/JVM/iOS**: okio `FileHandle` via [PathFileAccessor] with `Dispatchers.IO` + * - **Android content URIs**: `ContentUriFileAccessor` for SAF-backed storage * - **WasmJs**: Stub that throws `UnsupportedOperationException` (no file I/O) * - * Android, JVM, and iOS implementations are thread-safe (protected by a `Mutex`). + * Android, JVM, and iOS implementations are thread-safe (serialized dispatcher). */ interface FileAccessor { /** Writes [data] starting at the given byte [offset]. */ diff --git a/library/core/src/commonMain/kotlin/com/linroid/ketch/core/file/PathFileAccessor.kt b/library/core/src/commonMain/kotlin/com/linroid/ketch/core/file/PathFileAccessor.kt index b41974da..38a812fe 100644 --- a/library/core/src/commonMain/kotlin/com/linroid/ketch/core/file/PathFileAccessor.kt +++ b/library/core/src/commonMain/kotlin/com/linroid/ketch/core/file/PathFileAccessor.kt @@ -3,45 +3,43 @@ package com.linroid.ketch.core.file import com.linroid.ketch.api.log.KetchLogger import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.withContext -import kotlinx.io.files.Path -import kotlinx.io.files.SystemFileSystem +import okio.FileHandle +import okio.Path.Companion.toPath /** * Shared [FileAccessor] for path-based file systems. * - * All directory creation, deletion, and size queries use `kotlinx-io`'s - * [SystemFileSystem]. The only platform-specific piece is the - * [RandomAccessHandle] created by [handleFactory]. + * All directory creation, deletion, and size queries use okio's + * [platformFileSystem]. Random-access writes use okio's [FileHandle], + * which provides built-in positional read/write support on all platforms. * * @param path file system path to write to * @param dispatcher dispatcher for blocking I/O operations - * @param handleFactory creates a platform-specific [RandomAccessHandle] */ internal class PathFileAccessor( path: String, dispatcher: CoroutineDispatcher, - private val handleFactory: (String) -> RandomAccessHandle, ) : FileAccessor { private val log = KetchLogger("FileAccessor") - private val realPath = Path(path) - private var handle: RandomAccessHandle? = null + private val realPath = path.toPath() + private var handle: FileHandle? = null private val dispatcher = dispatcher.limitedParallelism(1) - private fun getOrCreateHandle(): RandomAccessHandle { + private fun getOrCreateHandle(): FileHandle { return handle ?: run { val parent = realPath.parent - if (parent != null && !SystemFileSystem.exists(parent)) { + if (parent != null && !platformFileSystem.exists(parent)) { log.d { "Creating directories: $parent" } - SystemFileSystem.createDirectories(parent) + platformFileSystem.createDirectories(parent) } log.d { "Opening file: $realPath" } - handleFactory(realPath.toString()).also { handle = it } + platformFileSystem.openReadWrite(realPath).also { handle = it } } } override suspend fun writeAt(offset: Long, data: ByteArray) { withContext(dispatcher) { - getOrCreateHandle().writeAt(offset, data) + getOrCreateHandle().write(offset, data, 0, data.size) } } @@ -61,21 +59,21 @@ internal class PathFileAccessor( withContext(dispatcher) { handle?.close() handle = null - if (SystemFileSystem.exists(realPath)) { + if (platformFileSystem.exists(realPath)) { log.d { "Deleting file: $realPath" } - SystemFileSystem.delete(realPath) + platformFileSystem.delete(realPath) } } } override suspend fun size(): Long = withContext(dispatcher) { - SystemFileSystem.metadataOrNull(realPath)?.size ?: 0L + platformFileSystem.metadata(realPath).size ?: 0L } override suspend fun preallocate(size: Long) { log.d { "Preallocating $size bytes: $realPath" } withContext(dispatcher) { - getOrCreateHandle().preallocate(size) + getOrCreateHandle().resize(size) } } } diff --git a/library/core/src/commonMain/kotlin/com/linroid/ketch/core/file/PlatformFileSystem.kt b/library/core/src/commonMain/kotlin/com/linroid/ketch/core/file/PlatformFileSystem.kt new file mode 100644 index 00000000..a9952415 --- /dev/null +++ b/library/core/src/commonMain/kotlin/com/linroid/ketch/core/file/PlatformFileSystem.kt @@ -0,0 +1,11 @@ +package com.linroid.ketch.core.file + +import okio.FileSystem + +/** + * Platform-specific [FileSystem] instance. + * + * Maps to [FileSystem.SYSTEM] on JVM, Android, and iOS, + * [okio.NodeJsFileSystem] on JS, and [okio.WasiFileSystem] on WasmWasi. + */ +internal expect val platformFileSystem: FileSystem diff --git a/library/core/src/commonMain/kotlin/com/linroid/ketch/core/file/RandomAccessHandle.kt b/library/core/src/commonMain/kotlin/com/linroid/ketch/core/file/RandomAccessHandle.kt deleted file mode 100644 index 41bc2d80..00000000 --- a/library/core/src/commonMain/kotlin/com/linroid/ketch/core/file/RandomAccessHandle.kt +++ /dev/null @@ -1,21 +0,0 @@ -package com.linroid.ketch.core.file - -/** - * Low-level random-access write handle. - * - * Implementations are called within `Dispatchers.IO` + Mutex by - * [PathFileAccessor], so they need not be thread-safe themselves. - */ -internal interface RandomAccessHandle { - /** Writes [data] starting at the given byte [offset]. */ - fun writeAt(offset: Long, data: ByteArray) - - /** Flushes buffered writes to disk. */ - fun flush() - - /** Closes the underlying handle. */ - fun close() - - /** Pre-allocates [size] bytes on disk to avoid fragmentation. */ - fun preallocate(size: Long) -} diff --git a/library/core/src/commonMain/kotlin/com/linroid/ketch/core/file/createFileAccessor.kt b/library/core/src/commonMain/kotlin/com/linroid/ketch/core/file/createFileAccessor.kt index e8cabf0b..0329c01e 100644 --- a/library/core/src/commonMain/kotlin/com/linroid/ketch/core/file/createFileAccessor.kt +++ b/library/core/src/commonMain/kotlin/com/linroid/ketch/core/file/createFileAccessor.kt @@ -5,10 +5,9 @@ import kotlinx.coroutines.CoroutineDispatcher /** * Creates a platform-appropriate [FileAccessor] for the given [path]. * - * On JVM/iOS this returns a [PathFileAccessor] backed by a - * platform-specific [RandomAccessHandle]. On Android, content URIs - * are routed to a dedicated `ContentUriFileAccessor`. On WasmJs, - * a stub that throws [UnsupportedOperationException] is returned. + * On JVM, iOS, JS, and WasmWasi this returns a [PathFileAccessor] + * backed by okio's `FileHandle`. On Android, content URIs are routed + * to a dedicated `ContentUriFileAccessor`. * * @param path file system path (or content URI on Android) * @param ioDispatcher dispatcher for blocking file I/O operations diff --git a/library/core/src/iosMain/kotlin/com/linroid/ketch/core/file/IosRandomAccessHandle.kt b/library/core/src/iosMain/kotlin/com/linroid/ketch/core/file/IosRandomAccessHandle.kt deleted file mode 100644 index 135dbef7..00000000 --- a/library/core/src/iosMain/kotlin/com/linroid/ketch/core/file/IosRandomAccessHandle.kt +++ /dev/null @@ -1,43 +0,0 @@ -package com.linroid.ketch.core.file - -import kotlinx.cinterop.BetaInteropApi -import kotlinx.cinterop.ExperimentalForeignApi -import kotlinx.cinterop.addressOf -import kotlinx.cinterop.usePinned -import platform.Foundation.NSData -import platform.Foundation.NSFileHandle -import platform.Foundation.create -import platform.Foundation.seekToFileOffset -import platform.Foundation.synchronizeFile -import platform.Foundation.closeFile -import platform.Foundation.truncateFileAtOffset -import platform.Foundation.writeData - -@OptIn(ExperimentalForeignApi::class, BetaInteropApi::class) -internal class IosRandomAccessHandle( - private val fileHandle: NSFileHandle, -) : RandomAccessHandle { - - override fun writeAt(offset: Long, data: ByteArray) { - fileHandle.seekToFileOffset(offset.toULong()) - data.usePinned { pinned -> - val nsData = NSData.create( - bytes = pinned.addressOf(0), - length = data.size.toULong(), - ) - fileHandle.writeData(nsData) - } - } - - override fun flush() { - fileHandle.synchronizeFile() - } - - override fun close() { - fileHandle.closeFile() - } - - override fun preallocate(size: Long) { - fileHandle.truncateFileAtOffset(size.toULong()) - } -} diff --git a/library/core/src/iosMain/kotlin/com/linroid/ketch/core/file/PlatformFileSystem.ios.kt b/library/core/src/iosMain/kotlin/com/linroid/ketch/core/file/PlatformFileSystem.ios.kt new file mode 100644 index 00000000..d7f08292 --- /dev/null +++ b/library/core/src/iosMain/kotlin/com/linroid/ketch/core/file/PlatformFileSystem.ios.kt @@ -0,0 +1,5 @@ +package com.linroid.ketch.core.file + +import okio.FileSystem + +internal actual val platformFileSystem: FileSystem = FileSystem.SYSTEM diff --git a/library/core/src/iosMain/kotlin/com/linroid/ketch/core/file/createFileAccessor.ios.kt b/library/core/src/iosMain/kotlin/com/linroid/ketch/core/file/createFileAccessor.ios.kt index b0dbe0ff..eebdf595 100644 --- a/library/core/src/iosMain/kotlin/com/linroid/ketch/core/file/createFileAccessor.ios.kt +++ b/library/core/src/iosMain/kotlin/com/linroid/ketch/core/file/createFileAccessor.ios.kt @@ -1,23 +1,10 @@ package com.linroid.ketch.core.file import kotlinx.coroutines.CoroutineDispatcher -import platform.Foundation.NSFileHandle -import platform.Foundation.NSFileManager -import platform.Foundation.fileHandleForWritingAtPath actual fun createFileAccessor( path: String, ioDispatcher: CoroutineDispatcher, ): FileAccessor { - return PathFileAccessor(path, ioDispatcher) { realPath -> - val fileManager = NSFileManager.defaultManager - if (!fileManager.fileExistsAtPath(realPath)) { - fileManager.createFileAtPath(realPath, null, null) - } - val handle = NSFileHandle.fileHandleForWritingAtPath(realPath) - ?: throw IllegalStateException( - "Cannot open file for writing: $realPath" - ) - IosRandomAccessHandle(handle) - } + return PathFileAccessor(path, ioDispatcher) } diff --git a/library/core/src/iosMain/kotlin/com/linroid/ketch/core/file/resolveChildPath.ios.kt b/library/core/src/iosMain/kotlin/com/linroid/ketch/core/file/resolveChildPath.ios.kt index 1b6f01a7..12e50d4f 100644 --- a/library/core/src/iosMain/kotlin/com/linroid/ketch/core/file/resolveChildPath.ios.kt +++ b/library/core/src/iosMain/kotlin/com/linroid/ketch/core/file/resolveChildPath.ios.kt @@ -1,8 +1,8 @@ package com.linroid.ketch.core.file -import kotlinx.io.files.Path +import okio.Path.Companion.toPath internal actual fun resolveChildPath( directory: String, fileName: String, -): String = Path(directory, fileName).toString() +): String = (directory.toPath() / fileName).toString() diff --git a/library/core/src/jsMain/kotlin/com/linroid/ketch/core/PlatformInfo.js.kt b/library/core/src/jsMain/kotlin/com/linroid/ketch/core/PlatformInfo.js.kt new file mode 100644 index 00000000..15428d03 --- /dev/null +++ b/library/core/src/jsMain/kotlin/com/linroid/ketch/core/PlatformInfo.js.kt @@ -0,0 +1,20 @@ +package com.linroid.ketch.core + +import com.linroid.ketch.api.SystemInfo + +internal actual fun currentSystemInfo(directory: String): SystemInfo { + return SystemInfo( + os = "Node.js", + arch = "js", + separator = "/", + javaVersion = "N/A", + availableProcessors = 1, + maxMemory = 0L, + totalMemory = 0L, + freeMemory = 0L, + downloadDirectory = directory, + totalSpace = 0L, + freeSpace = 0L, + usableSpace = 0L, + ) +} diff --git a/library/core/src/wasmJsMain/kotlin/com/linroid/ketch/core/createDefaultDispatchers.wasmJs.kt b/library/core/src/jsMain/kotlin/com/linroid/ketch/core/createDefaultDispatchers.js.kt similarity index 100% rename from library/core/src/wasmJsMain/kotlin/com/linroid/ketch/core/createDefaultDispatchers.wasmJs.kt rename to library/core/src/jsMain/kotlin/com/linroid/ketch/core/createDefaultDispatchers.js.kt diff --git a/library/core/src/jsMain/kotlin/com/linroid/ketch/core/file/PlatformFileSystem.js.kt b/library/core/src/jsMain/kotlin/com/linroid/ketch/core/file/PlatformFileSystem.js.kt new file mode 100644 index 00000000..f158af9a --- /dev/null +++ b/library/core/src/jsMain/kotlin/com/linroid/ketch/core/file/PlatformFileSystem.js.kt @@ -0,0 +1,6 @@ +package com.linroid.ketch.core.file + +import okio.FileSystem +import okio.NodeJsFileSystem + +internal actual val platformFileSystem: FileSystem = NodeJsFileSystem diff --git a/library/core/src/jsMain/kotlin/com/linroid/ketch/core/file/createFileAccessor.js.kt b/library/core/src/jsMain/kotlin/com/linroid/ketch/core/file/createFileAccessor.js.kt new file mode 100644 index 00000000..60034bf6 --- /dev/null +++ b/library/core/src/jsMain/kotlin/com/linroid/ketch/core/file/createFileAccessor.js.kt @@ -0,0 +1,8 @@ +package com.linroid.ketch.core.file + +import kotlinx.coroutines.CoroutineDispatcher + +actual fun createFileAccessor( + path: String, + ioDispatcher: CoroutineDispatcher, +): FileAccessor = PathFileAccessor(path, ioDispatcher) diff --git a/library/core/src/wasmJsMain/kotlin/com/linroid/ketch/core/file/resolveChildPath.wasmJs.kt b/library/core/src/jsMain/kotlin/com/linroid/ketch/core/file/resolveChildPath.js.kt similarity index 56% rename from library/core/src/wasmJsMain/kotlin/com/linroid/ketch/core/file/resolveChildPath.wasmJs.kt rename to library/core/src/jsMain/kotlin/com/linroid/ketch/core/file/resolveChildPath.js.kt index 1b6f01a7..12e50d4f 100644 --- a/library/core/src/wasmJsMain/kotlin/com/linroid/ketch/core/file/resolveChildPath.wasmJs.kt +++ b/library/core/src/jsMain/kotlin/com/linroid/ketch/core/file/resolveChildPath.js.kt @@ -1,8 +1,8 @@ package com.linroid.ketch.core.file -import kotlinx.io.files.Path +import okio.Path.Companion.toPath internal actual fun resolveChildPath( directory: String, fileName: String, -): String = Path(directory, fileName).toString() +): String = (directory.toPath() / fileName).toString() diff --git a/library/core/src/jvmMain/kotlin/com/linroid/ketch/core/file/JvmRandomAccessHandle.kt b/library/core/src/jvmMain/kotlin/com/linroid/ketch/core/file/JvmRandomAccessHandle.kt deleted file mode 100644 index fd158da4..00000000 --- a/library/core/src/jvmMain/kotlin/com/linroid/ketch/core/file/JvmRandomAccessHandle.kt +++ /dev/null @@ -1,25 +0,0 @@ -package com.linroid.ketch.core.file - -import java.io.RandomAccessFile - -internal class JvmRandomAccessHandle( - private val raf: RandomAccessFile, -) : RandomAccessHandle { - - override fun writeAt(offset: Long, data: ByteArray) { - raf.seek(offset) - raf.write(data) - } - - override fun flush() { - raf.fd.sync() - } - - override fun close() { - raf.close() - } - - override fun preallocate(size: Long) { - raf.setLength(size) - } -} diff --git a/library/core/src/jvmMain/kotlin/com/linroid/ketch/core/file/PlatformFileSystem.jvm.kt b/library/core/src/jvmMain/kotlin/com/linroid/ketch/core/file/PlatformFileSystem.jvm.kt new file mode 100644 index 00000000..d7f08292 --- /dev/null +++ b/library/core/src/jvmMain/kotlin/com/linroid/ketch/core/file/PlatformFileSystem.jvm.kt @@ -0,0 +1,5 @@ +package com.linroid.ketch.core.file + +import okio.FileSystem + +internal actual val platformFileSystem: FileSystem = FileSystem.SYSTEM diff --git a/library/core/src/jvmMain/kotlin/com/linroid/ketch/core/file/createFileAccessor.jvm.kt b/library/core/src/jvmMain/kotlin/com/linroid/ketch/core/file/createFileAccessor.jvm.kt index 49bedac2..eebdf595 100644 --- a/library/core/src/jvmMain/kotlin/com/linroid/ketch/core/file/createFileAccessor.jvm.kt +++ b/library/core/src/jvmMain/kotlin/com/linroid/ketch/core/file/createFileAccessor.jvm.kt @@ -1,13 +1,10 @@ package com.linroid.ketch.core.file import kotlinx.coroutines.CoroutineDispatcher -import java.io.RandomAccessFile actual fun createFileAccessor( path: String, ioDispatcher: CoroutineDispatcher, ): FileAccessor { - return PathFileAccessor(path, ioDispatcher) { realPath -> - JvmRandomAccessHandle(RandomAccessFile(realPath, "rw")) - } + return PathFileAccessor(path, ioDispatcher) } diff --git a/library/core/src/jvmMain/kotlin/com/linroid/ketch/core/file/resolveChildPath.jvm.kt b/library/core/src/jvmMain/kotlin/com/linroid/ketch/core/file/resolveChildPath.jvm.kt index 1b6f01a7..12e50d4f 100644 --- a/library/core/src/jvmMain/kotlin/com/linroid/ketch/core/file/resolveChildPath.jvm.kt +++ b/library/core/src/jvmMain/kotlin/com/linroid/ketch/core/file/resolveChildPath.jvm.kt @@ -1,8 +1,8 @@ package com.linroid.ketch.core.file -import kotlinx.io.files.Path +import okio.Path.Companion.toPath internal actual fun resolveChildPath( directory: String, fileName: String, -): String = Path(directory, fileName).toString() +): String = (directory.toPath() / fileName).toString() diff --git a/library/core/src/wasmJsMain/kotlin/com/linroid/ketch/core/file/createFileAccessor.wasmJs.kt b/library/core/src/wasmJsMain/kotlin/com/linroid/ketch/core/file/createFileAccessor.wasmJs.kt deleted file mode 100644 index 081f654f..00000000 --- a/library/core/src/wasmJsMain/kotlin/com/linroid/ketch/core/file/createFileAccessor.wasmJs.kt +++ /dev/null @@ -1,48 +0,0 @@ -package com.linroid.ketch.core.file - -import kotlinx.coroutines.CoroutineDispatcher - -@Suppress("UNUSED_PARAMETER") -actual fun createFileAccessor( - path: String, - ioDispatcher: CoroutineDispatcher, -): FileAccessor { - return WasmFileAccessor() -} - -private class WasmFileAccessor : FileAccessor { - - override suspend fun writeAt(offset: Long, data: ByteArray) { - throw UnsupportedOperationException( - "FileAccessor is not supported on Wasm/JS platform" - ) - } - - override suspend fun flush() { - throw UnsupportedOperationException( - "FileAccessor is not supported on Wasm/JS platform" - ) - } - - override fun close() { - // No-op for Wasm/JS - } - - override suspend fun delete() { - throw UnsupportedOperationException( - "FileAccessor is not supported on Wasm/JS platform" - ) - } - - override suspend fun size(): Long { - throw UnsupportedOperationException( - "FileAccessor is not supported on Wasm/JS platform" - ) - } - - override suspend fun preallocate(size: Long) { - throw UnsupportedOperationException( - "FileAccessor is not supported on Wasm/JS platform" - ) - } -} diff --git a/library/core/src/wasmJsMain/kotlin/com/linroid/ketch/core/PlatformInfo.wasmJs.kt b/library/core/src/wasmWasiMain/kotlin/com/linroid/ketch/core/PlatformInfo.wasmWasi.kt similarity index 95% rename from library/core/src/wasmJsMain/kotlin/com/linroid/ketch/core/PlatformInfo.wasmJs.kt rename to library/core/src/wasmWasiMain/kotlin/com/linroid/ketch/core/PlatformInfo.wasmWasi.kt index 81b95a63..333dcfe1 100644 --- a/library/core/src/wasmJsMain/kotlin/com/linroid/ketch/core/PlatformInfo.wasmJs.kt +++ b/library/core/src/wasmWasiMain/kotlin/com/linroid/ketch/core/PlatformInfo.wasmWasi.kt @@ -4,7 +4,7 @@ import com.linroid.ketch.api.SystemInfo internal actual fun currentSystemInfo(directory: String): SystemInfo { return SystemInfo( - os = "Browser", + os = "WASI", arch = "wasm", separator = "/", javaVersion = "N/A", diff --git a/library/core/src/wasmWasiMain/kotlin/com/linroid/ketch/core/createDefaultDispatchers.wasmWasi.kt b/library/core/src/wasmWasiMain/kotlin/com/linroid/ketch/core/createDefaultDispatchers.wasmWasi.kt new file mode 100644 index 00000000..cdb6455d --- /dev/null +++ b/library/core/src/wasmWasiMain/kotlin/com/linroid/ketch/core/createDefaultDispatchers.wasmWasi.kt @@ -0,0 +1,15 @@ +package com.linroid.ketch.core + +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers + +internal actual fun createMainDispatcher(): CoroutineDispatcher = + Dispatchers.Default + +@Suppress("UNUSED_PARAMETER") +internal actual fun createNetworkDispatcher(poolSize: Int): CoroutineDispatcher = + Dispatchers.Default + +@Suppress("UNUSED_PARAMETER") +internal actual fun createIoDispatcher(poolSize: Int): CoroutineDispatcher = + Dispatchers.Default diff --git a/library/core/src/wasmWasiMain/kotlin/com/linroid/ketch/core/file/PlatformFileSystem.wasmWasi.kt b/library/core/src/wasmWasiMain/kotlin/com/linroid/ketch/core/file/PlatformFileSystem.wasmWasi.kt new file mode 100644 index 00000000..546d970d --- /dev/null +++ b/library/core/src/wasmWasiMain/kotlin/com/linroid/ketch/core/file/PlatformFileSystem.wasmWasi.kt @@ -0,0 +1,6 @@ +package com.linroid.ketch.core.file + +import okio.FileSystem +import okio.WasiFileSystem + +internal actual val platformFileSystem: FileSystem = WasiFileSystem diff --git a/library/core/src/wasmWasiMain/kotlin/com/linroid/ketch/core/file/createFileAccessor.wasmWasi.kt b/library/core/src/wasmWasiMain/kotlin/com/linroid/ketch/core/file/createFileAccessor.wasmWasi.kt new file mode 100644 index 00000000..60034bf6 --- /dev/null +++ b/library/core/src/wasmWasiMain/kotlin/com/linroid/ketch/core/file/createFileAccessor.wasmWasi.kt @@ -0,0 +1,8 @@ +package com.linroid.ketch.core.file + +import kotlinx.coroutines.CoroutineDispatcher + +actual fun createFileAccessor( + path: String, + ioDispatcher: CoroutineDispatcher, +): FileAccessor = PathFileAccessor(path, ioDispatcher) diff --git a/library/core/src/wasmWasiMain/kotlin/com/linroid/ketch/core/file/resolveChildPath.wasmWasi.kt b/library/core/src/wasmWasiMain/kotlin/com/linroid/ketch/core/file/resolveChildPath.wasmWasi.kt new file mode 100644 index 00000000..12e50d4f --- /dev/null +++ b/library/core/src/wasmWasiMain/kotlin/com/linroid/ketch/core/file/resolveChildPath.wasmWasi.kt @@ -0,0 +1,8 @@ +package com.linroid.ketch.core.file + +import okio.Path.Companion.toPath + +internal actual fun resolveChildPath( + directory: String, + fileName: String, +): String = (directory.toPath() / fileName).toString() diff --git a/library/ftp/build.gradle.kts b/library/ftp/build.gradle.kts index d41bb54f..385b546d 100644 --- a/library/ftp/build.gradle.kts +++ b/library/ftp/build.gradle.kts @@ -1,5 +1,6 @@ @file:Suppress("UnstableApiUsage") +import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl import org.jetbrains.kotlin.gradle.dsl.JvmTarget import org.jetbrains.kotlin.gradle.targets.native.tasks.KotlinNativeSimulatorTest diff --git a/library/kermit/build.gradle.kts b/library/kermit/build.gradle.kts index e58b0f74..91032aea 100644 --- a/library/kermit/build.gradle.kts +++ b/library/kermit/build.gradle.kts @@ -25,20 +25,25 @@ kotlin { } } } - iosArm64() iosSimulatorArm64() jvm() - @OptIn(ExperimentalWasmDsl::class) - wasmJs { + js { browser() + nodejs() } + @OptIn(ExperimentalWasmDsl::class) + wasmJs { browser() } + + @OptIn(ExperimentalWasmDsl::class) + wasmWasi { nodejs() } + sourceSets { commonMain.dependencies { - api(projects.library.core) + api(projects.library.api) implementation(libs.kermit) } commonTest.dependencies { diff --git a/library/ktor/build.gradle.kts b/library/ktor/build.gradle.kts index ad4982d2..3912019c 100644 --- a/library/ktor/build.gradle.kts +++ b/library/ktor/build.gradle.kts @@ -31,10 +31,10 @@ kotlin { jvm() - @OptIn(ExperimentalWasmDsl::class) - wasmJs { - browser() - } + iosArm64() + iosSimulatorArm64() + + jvm() sourceSets { commonMain.dependencies { diff --git a/library/sqlite/build.gradle.kts b/library/sqlite/build.gradle.kts index 9efa6749..0166a70a 100644 --- a/library/sqlite/build.gradle.kts +++ b/library/sqlite/build.gradle.kts @@ -38,7 +38,6 @@ kotlin { implementation(libs.sqldelight.coroutines) implementation(libs.kotlinx.coroutines.core) implementation(libs.kotlinx.serialization.json) - implementation(libs.kotlinx.io.core) } androidMain.dependencies { implementation(libs.sqldelight.android.driver)