From d0b1d14efd9614a38953fa6509f1aa6a3411e0ac Mon Sep 17 00:00:00 2001 From: Timo Drick Date: Mon, 14 Sep 2026 10:33:51 +0200 Subject: [PATCH 1/5] Add support for the web origin private file system (OPFS). So it supports temporary files on the web target. --- README.md | 8 +- .../io/github/vinceglb/filekit/FileKit.kt | 23 +++ .../vinceglb/filekit/PlatformFileAsync.kt | 44 ++++ .../filekit/OriginPrivateFileSystem.js.kt | 10 + .../vinceglb/filekit/FileKitLinuxTest.kt | 9 + .../github/vinceglb/filekit/FileKit.nonWeb.kt | 6 + .../filekit/PlatformFileAsync.nonWeb.kt | 52 +++++ .../filekit/OriginPrivateFileSystem.wasmJs.kt | 10 + .../filekit/OriginPrivateFileSystem.web.kt | 189 ++++++++++++++++++ .../vinceglb/filekit/PlatformFile.web.kt | 87 +++++--- .../vinceglb/filekit/PlatformFileAsync.web.kt | 118 +++++++++++ .../io/github/vinceglb/filekit/WebFile.web.kt | 22 +- .../vinceglb/filekit/PlatformFileWebTest.kt | 63 ++++++ 13 files changed, 606 insertions(+), 35 deletions(-) create mode 100644 filekit-core/src/commonMain/kotlin/io/github/vinceglb/filekit/PlatformFileAsync.kt create mode 100644 filekit-core/src/jsMain/kotlin/io/github/vinceglb/filekit/OriginPrivateFileSystem.js.kt create mode 100644 filekit-core/src/nonWebMain/kotlin/io/github/vinceglb/filekit/PlatformFileAsync.nonWeb.kt create mode 100644 filekit-core/src/wasmJsMain/kotlin/io/github/vinceglb/filekit/OriginPrivateFileSystem.wasmJs.kt create mode 100644 filekit-core/src/webMain/kotlin/io/github/vinceglb/filekit/OriginPrivateFileSystem.web.kt create mode 100644 filekit-core/src/webMain/kotlin/io/github/vinceglb/filekit/PlatformFileAsync.web.kt diff --git a/README.md b/README.md index 70358663..b31326d2 100644 --- a/README.md +++ b/README.md @@ -105,11 +105,11 @@ val file = FileKit.openFileSaver( ) file?.writeString(contentToSave) -// Work with files -val myFile = FileKit.filesDir / "document.pdf" +// Work with application storage on every platform, including the web. +val myFile = FileKit.filesDirectory().file(name = "document.pdf", create = true) println(myFile.name) -println(myFile.size()) -myFile.writeString("Hello, World!") +println(myFile.sizeAsync()) +myFile.writeStringAsync("Hello, World!") // Image operations val compressedBytes = FileKit.compressImage( diff --git a/filekit-core/src/commonMain/kotlin/io/github/vinceglb/filekit/FileKit.kt b/filekit-core/src/commonMain/kotlin/io/github/vinceglb/filekit/FileKit.kt index 40fe95c5..7fd53db5 100644 --- a/filekit-core/src/commonMain/kotlin/io/github/vinceglb/filekit/FileKit.kt +++ b/filekit-core/src/commonMain/kotlin/io/github/vinceglb/filekit/FileKit.kt @@ -1,3 +1,26 @@ package io.github.vinceglb.filekit public expect object FileKit + +/** + * Returns the directory for persistent application files. + * + * On web targets this is the root of the browser's origin private file system. + */ +public expect suspend fun FileKit.filesDirectory(): PlatformFile + +/** + * Returns the directory for application cache files. + * + * On web targets this is the `cache` directory in the browser's origin private + * file system. + */ +public expect suspend fun FileKit.cacheDirectory(): PlatformFile + +/** + * Returns the directory for application databases. + * + * On web targets this is the `databases` directory in the browser's origin + * private file system. + */ +public expect suspend fun FileKit.databasesDirectory(): PlatformFile diff --git a/filekit-core/src/commonMain/kotlin/io/github/vinceglb/filekit/PlatformFileAsync.kt b/filekit-core/src/commonMain/kotlin/io/github/vinceglb/filekit/PlatformFileAsync.kt new file mode 100644 index 00000000..08f3eb8e --- /dev/null +++ b/filekit-core/src/commonMain/kotlin/io/github/vinceglb/filekit/PlatformFileAsync.kt @@ -0,0 +1,44 @@ +package io.github.vinceglb.filekit + +import io.github.vinceglb.filekit.mimeType.MimeType +import kotlin.time.ExperimentalTime +import kotlin.time.Instant + +/** Lists this directory's current children. */ +public expect suspend fun PlatformFile.listAsync(): List + +/** Returns a child file, creating it when [create] is true. */ +public expect suspend fun PlatformFile.file( + name: String, + create: Boolean = false, +): PlatformFile + +/** Returns a child directory, creating it when [create] is true. */ +public expect suspend fun PlatformFile.directory( + name: String, + create: Boolean = false, +): PlatformFile + +/** Replaces this file's content with [bytes]. */ +public expect suspend fun PlatformFile.writeBytes(bytes: ByteArray) + +/** Replaces this file's content with [string]. */ +public suspend fun PlatformFile.writeStringAsync(string: String) { + writeBytes(string.encodeToByteArray()) +} + +/** Deletes this file or empty directory. */ +public expect suspend fun PlatformFile.deleteAsync(mustExist: Boolean = true) + +/** Returns whether this file currently exists. */ +public expect suspend fun PlatformFile.existsAsync(): Boolean + +/** Returns this file's current size in bytes. */ +public expect suspend fun PlatformFile.sizeAsync(): Long + +/** Returns this file's current MIME type, if available. */ +public expect suspend fun PlatformFile.mimeTypeAsync(): MimeType? + +/** Returns this file's current modification time. */ +@OptIn(ExperimentalTime::class) +public expect suspend fun PlatformFile.lastModifiedAsync(): Instant diff --git a/filekit-core/src/jsMain/kotlin/io/github/vinceglb/filekit/OriginPrivateFileSystem.js.kt b/filekit-core/src/jsMain/kotlin/io/github/vinceglb/filekit/OriginPrivateFileSystem.js.kt new file mode 100644 index 00000000..d96fdbbb --- /dev/null +++ b/filekit-core/src/jsMain/kotlin/io/github/vinceglb/filekit/OriginPrivateFileSystem.js.kt @@ -0,0 +1,10 @@ +package io.github.vinceglb.filekit + +import org.khronos.webgl.Uint8Array +import kotlin.js.ExperimentalWasmJsInterop +import kotlin.js.JsAny +import kotlin.js.unsafeCast + +@OptIn(ExperimentalWasmJsInterop::class) +internal actual fun ByteArray.toWebBytes(): JsAny = + Uint8Array(toTypedArray()).unsafeCast() diff --git a/filekit-core/src/linuxTest/kotlin/io/github/vinceglb/filekit/FileKitLinuxTest.kt b/filekit-core/src/linuxTest/kotlin/io/github/vinceglb/filekit/FileKitLinuxTest.kt index 5bc39d6c..7a0d1ee8 100644 --- a/filekit-core/src/linuxTest/kotlin/io/github/vinceglb/filekit/FileKitLinuxTest.kt +++ b/filekit-core/src/linuxTest/kotlin/io/github/vinceglb/filekit/FileKitLinuxTest.kt @@ -57,6 +57,15 @@ class FileKitLinuxTest { ) } + @Test + fun FileKit_asyncStorageDirectories_matchPlatformDirectories() = runTest { + FileKit.init(appId = APP_ID) + + assertEquals(FileKit.filesDir.path, FileKit.filesDirectory().path) + assertEquals(FileKit.cacheDir.path, FileKit.cacheDirectory().path) + assertEquals(FileKit.databasesDir.path, FileKit.databasesDirectory().path) + } + @Test fun FileKit_filesDir_withoutCustomDirectory_resolvesUnderXdgDataHome() { FileKit.init(appId = APP_ID) diff --git a/filekit-core/src/nonWebMain/kotlin/io/github/vinceglb/filekit/FileKit.nonWeb.kt b/filekit-core/src/nonWebMain/kotlin/io/github/vinceglb/filekit/FileKit.nonWeb.kt index 930ec1b0..ff420f9f 100644 --- a/filekit-core/src/nonWebMain/kotlin/io/github/vinceglb/filekit/FileKit.nonWeb.kt +++ b/filekit-core/src/nonWebMain/kotlin/io/github/vinceglb/filekit/FileKit.nonWeb.kt @@ -36,6 +36,12 @@ public expect val FileKit.databasesDir: PlatformFile */ public expect val FileKit.projectDir: PlatformFile +public actual suspend fun FileKit.filesDirectory(): PlatformFile = filesDir + +public actual suspend fun FileKit.cacheDirectory(): PlatformFile = cacheDir + +public actual suspend fun FileKit.databasesDirectory(): PlatformFile = databasesDir + /** * Saves an image to the platform's gallery or photo album. * diff --git a/filekit-core/src/nonWebMain/kotlin/io/github/vinceglb/filekit/PlatformFileAsync.nonWeb.kt b/filekit-core/src/nonWebMain/kotlin/io/github/vinceglb/filekit/PlatformFileAsync.nonWeb.kt new file mode 100644 index 00000000..6e45a6c0 --- /dev/null +++ b/filekit-core/src/nonWebMain/kotlin/io/github/vinceglb/filekit/PlatformFileAsync.nonWeb.kt @@ -0,0 +1,52 @@ +package io.github.vinceglb.filekit + +import io.github.vinceglb.filekit.exceptions.FileKitException +import io.github.vinceglb.filekit.mimeType.MimeType +import kotlin.time.ExperimentalTime +import kotlin.time.Instant + +public actual suspend fun PlatformFile.listAsync(): List = list() + +public actual suspend fun PlatformFile.file( + name: String, + create: Boolean, +): PlatformFile { + val file = this / name + if (create && !file.exists()) { + file write ByteArray(0) + } + if (!file.exists() || !file.isRegularFile()) { + throw FileKitException("File does not exist: $file") + } + return file +} + +public actual suspend fun PlatformFile.directory( + name: String, + create: Boolean, +): PlatformFile { + val directory = this / name + if (create) { + directory.createDirectories() + } else if (!directory.exists() || !directory.isDirectory()) { + throw FileKitException("Directory does not exist: $directory") + } + return directory +} + +public actual suspend fun PlatformFile.writeBytes(bytes: ByteArray) { + this write bytes +} + +public actual suspend fun PlatformFile.deleteAsync(mustExist: Boolean) { + delete(mustExist) +} + +public actual suspend fun PlatformFile.existsAsync(): Boolean = exists() + +public actual suspend fun PlatformFile.sizeAsync(): Long = size() + +public actual suspend fun PlatformFile.mimeTypeAsync(): MimeType? = mimeType() + +@OptIn(ExperimentalTime::class) +public actual suspend fun PlatformFile.lastModifiedAsync(): Instant = lastModified() diff --git a/filekit-core/src/wasmJsMain/kotlin/io/github/vinceglb/filekit/OriginPrivateFileSystem.wasmJs.kt b/filekit-core/src/wasmJsMain/kotlin/io/github/vinceglb/filekit/OriginPrivateFileSystem.wasmJs.kt new file mode 100644 index 00000000..170b63d0 --- /dev/null +++ b/filekit-core/src/wasmJsMain/kotlin/io/github/vinceglb/filekit/OriginPrivateFileSystem.wasmJs.kt @@ -0,0 +1,10 @@ +package io.github.vinceglb.filekit + +import org.khronos.webgl.toInt8Array +import kotlin.js.ExperimentalWasmJsInterop +import kotlin.js.JsAny +import kotlin.js.unsafeCast + +@OptIn(ExperimentalWasmJsInterop::class) +internal actual fun ByteArray.toWebBytes(): JsAny = + toInt8Array().unsafeCast() diff --git a/filekit-core/src/webMain/kotlin/io/github/vinceglb/filekit/OriginPrivateFileSystem.web.kt b/filekit-core/src/webMain/kotlin/io/github/vinceglb/filekit/OriginPrivateFileSystem.web.kt new file mode 100644 index 00000000..dea0121e --- /dev/null +++ b/filekit-core/src/webMain/kotlin/io/github/vinceglb/filekit/OriginPrivateFileSystem.web.kt @@ -0,0 +1,189 @@ +package io.github.vinceglb.filekit + +import io.github.vinceglb.filekit.exceptions.FileKitException +import kotlinx.coroutines.await +import kotlin.js.ExperimentalWasmJsInterop +import kotlin.js.JsAny +import kotlin.js.JsName +import kotlin.js.Promise + +/** + * Reads the root directory of the browser's origin private file system (OPFS). + * + * The returned [PlatformFile] retains a live OPFS directory handle. Use + * [listOriginPrivateFileSystemEntries], [originPrivateFileSystemFile], and + * [originPrivateFileSystemDirectory] to interact with its entries + * asynchronously. + * + * OPFS is available only in secure contexts in browsers that implement the File + * System API. + */ +@OptIn(ExperimentalWasmJsInterop::class) +public suspend fun PlatformFile.Companion.fromOriginPrivateFileSystem(): PlatformFile { + val root = browserNavigator.storage.getDirectory().await() + return PlatformFile( + WebFile.OriginPrivateDirectory( + handle = root, + path = "", + parent = null, + ), + ) +} + +@OptIn(ExperimentalWasmJsInterop::class) +public suspend fun PlatformFile.listOriginPrivateFileSystemEntries(): List { + val directory = webFile as? WebFile.OriginPrivateDirectory + ?: throw FileKitException("This file is not an origin private file system directory") + val iterator = directory.handle.values() + val entries = mutableListOf() + + while (true) { + val entry = iterator.next().await() + if (entry.done) break + + val handle = entry.value + ?: throw FileKitException("Could not read origin private file system entry") + val path = directory.path.appendOriginPrivateFileSystemPath(handle.name) + entries += PlatformFile( + when (handle.kind) { + "file" -> WebFile.OriginPrivateFile( + handle = handle.unsafeCast(), + path = path, + parent = directory, + ) + + "directory" -> WebFile.OriginPrivateDirectory( + handle = handle.unsafeCast(), + path = path, + parent = directory, + ) + + else -> throw FileKitException("Unsupported origin private file system entry type: ${handle.kind}") + }, + ) + } + + return entries +} + +/** Returns an OPFS file in this directory, creating it when [create] is true. */ +@OptIn(ExperimentalWasmJsInterop::class) +public suspend fun PlatformFile.originPrivateFileSystemFile( + name: String, + create: Boolean = false, +): PlatformFile { + val directory = webFile as? WebFile.OriginPrivateDirectory + ?: throw FileKitException("This file is not an origin private file system directory") + return PlatformFile( + WebFile.OriginPrivateFile( + handle = directory.handle.getFileHandle(name, FileSystemGetHandleOptions(create)).await(), + path = directory.path.appendOriginPrivateFileSystemPath(name), + parent = directory, + ), + ) +} + +/** Returns an OPFS directory in this directory, creating it when [create] is true. */ +@OptIn(ExperimentalWasmJsInterop::class) +public suspend fun PlatformFile.originPrivateFileSystemDirectory( + name: String, + create: Boolean = false, +): PlatformFile { + val directory = webFile as? WebFile.OriginPrivateDirectory + ?: throw FileKitException("This file is not an origin private file system directory") + return PlatformFile( + WebFile.OriginPrivateDirectory( + handle = directory.handle.getDirectoryHandle(name, FileSystemGetHandleOptions(create)).await(), + path = directory.path.appendOriginPrivateFileSystemPath(name), + parent = directory, + ), + ) +} + +/** Replaces the contents of this live OPFS file with [bytes]. */ +@OptIn(ExperimentalWasmJsInterop::class) +public suspend fun PlatformFile.writeToOriginPrivateFileSystem(bytes: ByteArray) { + val file = webFile as? WebFile.OriginPrivateFile + ?: throw FileKitException("This file is not a writable origin private file system file") + val writable = file.handle.createWritable().await() + try { + writable.write(bytes.toWebBytes()).await() + } finally { + writable.close().await() + } +} + +public actual suspend fun FileKit.filesDirectory(): PlatformFile = + PlatformFile.fromOriginPrivateFileSystem() + +public actual suspend fun FileKit.cacheDirectory(): PlatformFile = + filesDirectory().originPrivateFileSystemDirectory(name = "cache", create = true) + +public actual suspend fun FileKit.databasesDirectory(): PlatformFile = + filesDirectory().originPrivateFileSystemDirectory(name = "databases", create = true) + +internal fun String.appendOriginPrivateFileSystemPath(child: String): String = + if (isEmpty()) child else "$this/$child" + +@OptIn(ExperimentalWasmJsInterop::class) +@JsName("navigator") +private external val browserNavigator: BrowserNavigator + +@OptIn(ExperimentalWasmJsInterop::class) +private external interface BrowserNavigator : JsAny { + val storage: BrowserStorageManager +} + +@OptIn(ExperimentalWasmJsInterop::class) +private external interface BrowserStorageManager : JsAny { + fun getDirectory(): Promise +} + +@OptIn(ExperimentalWasmJsInterop::class) +internal external interface FileSystemHandle : JsAny { + val kind: String + val name: String +} + +@OptIn(ExperimentalWasmJsInterop::class) +internal external interface FileSystemFileHandle : FileSystemHandle { + fun getFile(): Promise + fun createWritable(): Promise +} + +@OptIn(ExperimentalWasmJsInterop::class) +internal external interface FileSystemDirectoryHandle : FileSystemHandle { + fun values(): FileSystemDirectoryHandleIterator + fun getFileHandle(name: String, options: FileSystemGetHandleOptions): Promise + fun getDirectoryHandle(name: String, options: FileSystemGetHandleOptions): Promise + fun removeEntry(name: String): Promise +} + +@OptIn(ExperimentalWasmJsInterop::class) +internal external interface FileSystemDirectoryHandleIterator : JsAny { + fun next(): Promise +} + +@OptIn(ExperimentalWasmJsInterop::class) +internal external interface FileSystemDirectoryHandleIteratorResult : JsAny { + val done: Boolean + val value: FileSystemHandle? +} + +@OptIn(ExperimentalWasmJsInterop::class) +internal external interface FileSystemGetHandleOptions : JsAny { + var create: Boolean +} + +@OptIn(ExperimentalWasmJsInterop::class) +internal fun FileSystemGetHandleOptions(create: Boolean): FileSystemGetHandleOptions = + js("({ create })") + +@OptIn(ExperimentalWasmJsInterop::class) +internal external interface FileSystemWritableFileStream : JsAny { + fun write(data: JsAny): Promise + fun close(): Promise +} + +@OptIn(ExperimentalWasmJsInterop::class) +internal expect fun ByteArray.toWebBytes(): JsAny diff --git a/filekit-core/src/webMain/kotlin/io/github/vinceglb/filekit/PlatformFile.web.kt b/filekit-core/src/webMain/kotlin/io/github/vinceglb/filekit/PlatformFile.web.kt index d509fd38..9b66164f 100644 --- a/filekit-core/src/webMain/kotlin/io/github/vinceglb/filekit/PlatformFile.web.kt +++ b/filekit-core/src/webMain/kotlin/io/github/vinceglb/filekit/PlatformFile.web.kt @@ -2,6 +2,7 @@ package io.github.vinceglb.filekit import io.github.vinceglb.filekit.exceptions.FileKitException import io.github.vinceglb.filekit.mimeType.MimeType +import kotlinx.coroutines.await import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.withContext @@ -35,29 +36,39 @@ public actual val PlatformFile.name: String get() = when (val file = webFile) { is WebFile.FileWrapper -> file.name is WebFile.DirectoryWrapper -> file.name + is WebFile.OriginPrivateFile -> file.name + is WebFile.OriginPrivateDirectory -> file.name } public actual val PlatformFile.extension: String get() = when (webFile) { is WebFile.FileWrapper -> name.substringAfterLast(".", "") is WebFile.DirectoryWrapper -> "" + is WebFile.OriginPrivateFile -> name.substringAfterLast(".", "") + is WebFile.OriginPrivateDirectory -> "" } public actual val PlatformFile.nameWithoutExtension: String get() = when (webFile) { is WebFile.FileWrapper -> name.substringBeforeLast(".", name) is WebFile.DirectoryWrapper -> name + is WebFile.OriginPrivateFile -> name.substringBeforeLast(".", name) + is WebFile.OriginPrivateDirectory -> name } public actual fun PlatformFile.size(): Long = when (val file = webFile) { is WebFile.FileWrapper -> file.size is WebFile.DirectoryWrapper -> 0 + is WebFile.OriginPrivateFile -> 0 + is WebFile.OriginPrivateDirectory -> 0 } public actual val PlatformFile.path: String get() = when (val file = webFile) { is WebFile.FileWrapper -> file.path is WebFile.DirectoryWrapper -> file.path + is WebFile.OriginPrivateFile -> file.path + is WebFile.OriginPrivateDirectory -> file.path } public actual fun PlatformFile.mimeType(): MimeType? = when (val file = webFile) { @@ -70,23 +81,32 @@ public actual fun PlatformFile.mimeType(): MimeType? = when (val file = webFile) is WebFile.DirectoryWrapper -> { null } + + is WebFile.OriginPrivateFile, + is WebFile.OriginPrivateDirectory, + -> null } public actual fun PlatformFile.lastModified(): Instant = when (val file = webFile) { is WebFile.FileWrapper -> file.lastModified is WebFile.DirectoryWrapper -> file.lastModified + is WebFile.OriginPrivateFile, + is WebFile.OriginPrivateDirectory, + -> WEB_DIRECTORY_LAST_MODIFIED } public actual fun PlatformFile.parent(): PlatformFile? = when (val file = webFile) { is WebFile.FileWrapper -> file.parent?.toPlatformFile() is WebFile.DirectoryWrapper -> file.parent?.toPlatformFile() + is WebFile.OriginPrivateFile -> file.parent?.toPlatformFile() + is WebFile.OriginPrivateDirectory -> file.parent?.toPlatformFile() } public actual fun PlatformFile.isRegularFile(): Boolean = - webFile is WebFile.FileWrapper + webFile is WebFile.FileWrapper || webFile is WebFile.OriginPrivateFile public actual fun PlatformFile.isDirectory(): Boolean = - webFile is WebFile.DirectoryWrapper + webFile is WebFile.DirectoryWrapper || webFile is WebFile.OriginPrivateDirectory public actual inline fun PlatformFile.list(block: (List) -> Unit) { block(list()) @@ -95,6 +115,10 @@ public actual inline fun PlatformFile.list(block: (List) -> Unit) public actual fun PlatformFile.list(): List = when (val file = webFile) { is WebFile.FileWrapper -> throw FileKitException("Cannot list a regular file") is WebFile.DirectoryWrapper -> file.children.map { it.toPlatformFile() } + is WebFile.OriginPrivateFile -> throw FileKitException("Cannot list a regular file") + is WebFile.OriginPrivateDirectory -> { + throw FileKitException("Origin private file system directories must be listed asynchronously") + } } public actual fun PlatformFile.startAccessingSecurityScopedResource(): Boolean = true @@ -103,36 +127,41 @@ public actual fun PlatformFile.stopAccessingSecurityScopedResource() {} @OptIn(ExperimentalWasmJsInterop::class) public actual suspend fun PlatformFile.readBytes(): ByteArray = when (val file = webFile) { - is WebFile.FileWrapper -> withContext(Dispatchers.Main) { - suspendCancellableCoroutine { continuation -> - val reader = FileReader() - reader.onload = { event -> - try { - val arrayBuffer = event - .target - ?.unsafeCast() - ?.result - ?.unsafeCast() - ?: throw FileKitException("Could not read file") - - val bytes = Uint8Array(arrayBuffer) - val byteArray = ByteArray(bytes.length) - for (i in 0 until bytes.length) { - byteArray[i] = bytes[i] - } - - continuation.resume(byteArray) - } catch (e: Exception) { - continuation.resumeWithException(e) - } - } - - reader.readAsArrayBuffer(file.file) - } - } + is WebFile.FileWrapper -> file.file.readBytes() is WebFile.DirectoryWrapper -> throw FileKitException("Cannot read bytes from a directory") + is WebFile.OriginPrivateFile -> file.handle.getFile().await().readBytes() + is WebFile.OriginPrivateDirectory -> throw FileKitException("Cannot read bytes from a directory") } public actual suspend fun PlatformFile.readString(): String = readBytes().decodeToString() + +@OptIn(ExperimentalWasmJsInterop::class) +private suspend fun BrowserFile.readBytes(): ByteArray = withContext(Dispatchers.Main) { + suspendCancellableCoroutine { continuation -> + val reader = FileReader() + reader.onload = { event -> + try { + val arrayBuffer = event + .target + ?.unsafeCast() + ?.result + ?.unsafeCast() + ?: throw FileKitException("Could not read file") + + val bytes = Uint8Array(arrayBuffer) + val byteArray = ByteArray(bytes.length) + for (i in 0 until bytes.length) { + byteArray[i] = bytes[i] + } + + continuation.resume(byteArray) + } catch (e: Exception) { + continuation.resumeWithException(e) + } + } + + reader.readAsArrayBuffer(this@readBytes) + } +} diff --git a/filekit-core/src/webMain/kotlin/io/github/vinceglb/filekit/PlatformFileAsync.web.kt b/filekit-core/src/webMain/kotlin/io/github/vinceglb/filekit/PlatformFileAsync.web.kt new file mode 100644 index 00000000..ab66a633 --- /dev/null +++ b/filekit-core/src/webMain/kotlin/io/github/vinceglb/filekit/PlatformFileAsync.web.kt @@ -0,0 +1,118 @@ +package io.github.vinceglb.filekit + +import io.github.vinceglb.filekit.exceptions.FileKitException +import io.github.vinceglb.filekit.mimeType.MimeType +import kotlinx.coroutines.await +import kotlin.js.ExperimentalWasmJsInterop +import kotlin.js.toDouble +import kotlin.time.ExperimentalTime +import kotlin.time.Instant + +public actual suspend fun PlatformFile.listAsync(): List = when (val file = webFile) { + is WebFile.FileWrapper, + is WebFile.OriginPrivateFile, + -> throw FileKitException("Cannot list a regular file") + + is WebFile.DirectoryWrapper -> file.children.map(WebFile::toPlatformFile) + is WebFile.OriginPrivateDirectory -> listOriginPrivateFileSystemEntries() +} + +public actual suspend fun PlatformFile.file( + name: String, + create: Boolean, +): PlatformFile = when (webFile) { + is WebFile.OriginPrivateDirectory -> originPrivateFileSystemFile(name, create) + else -> throw FileKitException("This file is not a writable origin private file system directory") +} + +public actual suspend fun PlatformFile.directory( + name: String, + create: Boolean, +): PlatformFile = when (webFile) { + is WebFile.OriginPrivateDirectory -> originPrivateFileSystemDirectory(name, create) + else -> throw FileKitException("This file is not a writable origin private file system directory") +} + +public actual suspend fun PlatformFile.writeBytes(bytes: ByteArray) { + when (webFile) { + is WebFile.OriginPrivateFile -> writeToOriginPrivateFileSystem(bytes) + else -> throw FileKitException("This file is not a writable origin private file system file") + } +} + +@OptIn(ExperimentalWasmJsInterop::class) +public actual suspend fun PlatformFile.deleteAsync(mustExist: Boolean) { + when (val file = webFile) { + is WebFile.OriginPrivateFile -> { + val parent = file.parent + ?: throw FileKitException("Cannot delete the origin private file system root") + if (mustExist) { + parent.handle.removeEntry(file.name).await() + } else { + runCatching { parent.handle.removeEntry(file.name).await() } + } + } + + is WebFile.OriginPrivateDirectory -> { + val parent = file.parent + ?: throw FileKitException("Cannot delete the origin private file system root") + if (mustExist) { + parent.handle.removeEntry(file.name).await() + } else { + runCatching { parent.handle.removeEntry(file.name).await() } + } + } + + else -> throw FileKitException("This file is not a writable origin private file system entry") + } +} + +@OptIn(ExperimentalWasmJsInterop::class) +public actual suspend fun PlatformFile.existsAsync(): Boolean = when (val file = webFile) { + is WebFile.FileWrapper, + is WebFile.DirectoryWrapper, + -> true + + is WebFile.OriginPrivateFile -> file.parent?.let { parent -> + runCatching { parent.handle.getFileHandle(file.name, FileSystemGetHandleOptions(false)).await() }.isSuccess + } ?: true + + is WebFile.OriginPrivateDirectory -> file.parent?.let { parent -> + runCatching { parent.handle.getDirectoryHandle(file.name, FileSystemGetHandleOptions(false)).await() }.isSuccess + } ?: true +} + +@OptIn(ExperimentalWasmJsInterop::class) +public actual suspend fun PlatformFile.sizeAsync(): Long = when (val file = webFile) { + is WebFile.FileWrapper -> file.size + is WebFile.DirectoryWrapper, + is WebFile.OriginPrivateDirectory, + -> 0 + + is WebFile.OriginPrivateFile -> file.handle.getFile().await().size.toDouble().toLong() +} + +@OptIn(ExperimentalWasmJsInterop::class) +public actual suspend fun PlatformFile.mimeTypeAsync(): MimeType? = when (val file = webFile) { + is WebFile.FileWrapper -> file.type.takeIf(String::isNotBlank)?.let(MimeType::parse) + is WebFile.DirectoryWrapper, + is WebFile.OriginPrivateDirectory, + -> null + + is WebFile.OriginPrivateFile -> file.handle.getFile().await().type + .takeIf(String::isNotBlank) + ?.let(MimeType::parse) +} + +@Suppress("REDUNDANT_CALL_OF_CONVERSION_METHOD") +@OptIn(ExperimentalTime::class, ExperimentalWasmJsInterop::class) +public actual suspend fun PlatformFile.lastModifiedAsync(): Instant = when (val file = webFile) { + is WebFile.FileWrapper -> file.lastModified + is WebFile.DirectoryWrapper, + is WebFile.OriginPrivateDirectory, + -> WEB_DIRECTORY_LAST_MODIFIED + + is WebFile.OriginPrivateFile -> Instant.fromEpochMilliseconds( + file.handle.getFile().await().lastModified.toDouble().toLong(), + ) +} diff --git a/filekit-core/src/webMain/kotlin/io/github/vinceglb/filekit/WebFile.web.kt b/filekit-core/src/webMain/kotlin/io/github/vinceglb/filekit/WebFile.web.kt index 327b390e..60acfff3 100644 --- a/filekit-core/src/webMain/kotlin/io/github/vinceglb/filekit/WebFile.web.kt +++ b/filekit-core/src/webMain/kotlin/io/github/vinceglb/filekit/WebFile.web.kt @@ -7,8 +7,8 @@ import kotlin.time.Instant /** * FileKit's web backing type for [PlatformFile]. * - * It wraps real browser files and virtual directories reconstructed from - * browser directory picker results. + * It wraps browser file snapshots, live origin-private file-system entries, + * and virtual directories reconstructed from browser directory picker results. */ public sealed class WebFile { @OptIn(ExperimentalWasmJsInterop::class) @@ -44,6 +44,24 @@ public sealed class WebFile { public val lastModified: Instant = WEB_DIRECTORY_LAST_MODIFIED } + + public class OriginPrivateFile internal constructor( + internal val handle: FileSystemFileHandle, + public val path: String, + public val parent: OriginPrivateDirectory?, + ) : WebFile() { + public val name: String + get() = handle.name + } + + public class OriginPrivateDirectory internal constructor( + internal val handle: FileSystemDirectoryHandle, + public val path: String, + public val parent: OriginPrivateDirectory?, + ) : WebFile() { + public val name: String + get() = handle.name + } } internal val WEB_DIRECTORY_LAST_MODIFIED: Instant = Instant.fromEpochMilliseconds(0) diff --git a/filekit-core/src/webTest/kotlin/io/github/vinceglb/filekit/PlatformFileWebTest.kt b/filekit-core/src/webTest/kotlin/io/github/vinceglb/filekit/PlatformFileWebTest.kt index 098030ec..d34a124a 100644 --- a/filekit-core/src/webTest/kotlin/io/github/vinceglb/filekit/PlatformFileWebTest.kt +++ b/filekit-core/src/webTest/kotlin/io/github/vinceglb/filekit/PlatformFileWebTest.kt @@ -173,6 +173,69 @@ class PlatformFileWebTest { ) } + @Test + fun testOriginPrivateFileSystemPathFromRoot() { + assertEquals( + expected = "hello.txt", + actual = "".appendOriginPrivateFileSystemPath("hello.txt"), + ) + } + + @Test + fun testOriginPrivateFileSystemPathFromDirectory() { + assertEquals( + expected = "folder/hello.txt", + actual = "folder".appendOriginPrivateFileSystemPath("hello.txt"), + ) + } + + @Test + fun testOriginPrivateFileSystemRoot() = runTest { + val root = PlatformFile.fromOriginPrivateFileSystem() + + assertTrue(root.isDirectory()) + assertFalse(root.isRegularFile()) + assertNull(root.parent()) + } + + @Test + fun testOriginPrivateFileSystemStorageDirectories() = runTest { + val filesDir = FileKit.filesDirectory() + val cacheDir = FileKit.cacheDirectory() + val databasesDir = FileKit.databasesDirectory() + + assertTrue(filesDir.isDirectory()) + assertTrue(cacheDir.isDirectory()) + assertTrue(databasesDir.isDirectory()) + assertEquals("cache", cacheDir.name) + assertEquals("databases", databasesDir.name) + assertEquals("", filesDir.path) + assertEquals("cache", cacheDir.path) + assertEquals("databases", databasesDir.path) + } + + @Test + fun testOriginPrivateFileSystemFileOperations() = runTest { + val cacheDir = FileKit.cacheDirectory() + val file = cacheDir.file(name = "filekit-opfs-test.txt", create = true) + + try { + file.writeStringAsync("Hello, OPFS!") + + assertTrue(file.existsAsync()) + assertEquals("Hello, OPFS!", file.readString()) + assertEquals(12L, file.sizeAsync()) + assertEquals( + listOf("filekit-opfs-test.txt"), + cacheDir.listAsync().filter { it.name == file.name }.map { it.name }, + ) + } finally { + file.deleteAsync(mustExist = false) + } + + assertFalse(file.existsAsync()) + } + private fun PlatformFile.webFileWrapper(): WebFile.FileWrapper = assertIs(webFile) } From 450262ac3e72f444002d8fbfcb82d31c5acc65ff Mon Sep 17 00:00:00 2001 From: Timo Drick Date: Wed, 16 Sep 2026 11:08:06 +0200 Subject: [PATCH 2/5] Added documentation for non web targets --- .../kotlin/io/github/vinceglb/filekit/FileKit.kt | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/filekit-core/src/commonMain/kotlin/io/github/vinceglb/filekit/FileKit.kt b/filekit-core/src/commonMain/kotlin/io/github/vinceglb/filekit/FileKit.kt index 7fd53db5..d2c10286 100644 --- a/filekit-core/src/commonMain/kotlin/io/github/vinceglb/filekit/FileKit.kt +++ b/filekit-core/src/commonMain/kotlin/io/github/vinceglb/filekit/FileKit.kt @@ -5,6 +5,9 @@ public expect object FileKit /** * Returns the directory for persistent application files. * + * On Android, this corresponds to `Context.filesDir`. + * On Apple, this corresponds to `NSApplicationSupportDirectory`. + * On JVM, this corresponds to a platform-specific application data directory. * On web targets this is the root of the browser's origin private file system. */ public expect suspend fun FileKit.filesDirectory(): PlatformFile @@ -12,6 +15,9 @@ public expect suspend fun FileKit.filesDirectory(): PlatformFile /** * Returns the directory for application cache files. * + * On Android, this corresponds to `Context.cacheDir`. + * On Apple, this corresponds to `NSCachesDirectory`. + * On JVM, this corresponds to a platform-specific cache directory. * On web targets this is the `cache` directory in the browser's origin private * file system. */ @@ -20,7 +26,7 @@ public expect suspend fun FileKit.cacheDirectory(): PlatformFile /** * Returns the directory for application databases. * - * On web targets this is the `databases` directory in the browser's origin - * private file system. + * This is primarily relevant for Android where it corresponds to the databases directory. + * On other platforms, it might fall back to `filesDir` or another appropriate location. */ public expect suspend fun FileKit.databasesDirectory(): PlatformFile From b2578272728b85301b95dd550a26a378543980e9 Mon Sep 17 00:00:00 2001 From: Timo Drick Date: Thu, 17 Sep 2026 09:47:27 +0200 Subject: [PATCH 3/5] Cleanups (only support filesDirectory() on all platforms, removed async duplicates of write and delete because they where already suspend functions so they can directly supported on web.) --- .../io/github/vinceglb/filekit/FileKit.kt | 21 +---- .../github/vinceglb/filekit/PlatformFile.kt | 28 ++++++ .../vinceglb/filekit/PlatformFileAsync.kt | 11 --- .../github/vinceglb/filekit/FileKit.nonWeb.kt | 4 - .../vinceglb/filekit/PlatformFile.nonWeb.kt | 18 +--- .../filekit/PlatformFileAsync.nonWeb.kt | 8 -- .../io/github/vinceglb/filekit/FileKit.web.kt | 11 +++ .../filekit/OriginPrivateFileSystem.web.kt | 78 +++++++--------- .../vinceglb/filekit/PlatformFile.web.kt | 89 ++++++++++++++++-- .../vinceglb/filekit/PlatformFileAsync.web.kt | 91 ++++++++++--------- .../vinceglb/filekit/PlatformFileWebTest.kt | 30 ------ 11 files changed, 199 insertions(+), 190 deletions(-) diff --git a/filekit-core/src/commonMain/kotlin/io/github/vinceglb/filekit/FileKit.kt b/filekit-core/src/commonMain/kotlin/io/github/vinceglb/filekit/FileKit.kt index d2c10286..f3728a9e 100644 --- a/filekit-core/src/commonMain/kotlin/io/github/vinceglb/filekit/FileKit.kt +++ b/filekit-core/src/commonMain/kotlin/io/github/vinceglb/filekit/FileKit.kt @@ -8,25 +8,6 @@ public expect object FileKit * On Android, this corresponds to `Context.filesDir`. * On Apple, this corresponds to `NSApplicationSupportDirectory`. * On JVM, this corresponds to a platform-specific application data directory. - * On web targets this is the root of the browser's origin private file system. + * On web targets this is the root of the browser's origin private file system (OPFS). */ public expect suspend fun FileKit.filesDirectory(): PlatformFile - -/** - * Returns the directory for application cache files. - * - * On Android, this corresponds to `Context.cacheDir`. - * On Apple, this corresponds to `NSCachesDirectory`. - * On JVM, this corresponds to a platform-specific cache directory. - * On web targets this is the `cache` directory in the browser's origin private - * file system. - */ -public expect suspend fun FileKit.cacheDirectory(): PlatformFile - -/** - * Returns the directory for application databases. - * - * This is primarily relevant for Android where it corresponds to the databases directory. - * On other platforms, it might fall back to `filesDir` or another appropriate location. - */ -public expect suspend fun FileKit.databasesDirectory(): PlatformFile diff --git a/filekit-core/src/commonMain/kotlin/io/github/vinceglb/filekit/PlatformFile.kt b/filekit-core/src/commonMain/kotlin/io/github/vinceglb/filekit/PlatformFile.kt index 28c838a8..203b77a2 100644 --- a/filekit-core/src/commonMain/kotlin/io/github/vinceglb/filekit/PlatformFile.kt +++ b/filekit-core/src/commonMain/kotlin/io/github/vinceglb/filekit/PlatformFile.kt @@ -53,6 +53,13 @@ public expect fun PlatformFile.lastModified(): Instant */ public expect suspend fun PlatformFile.readBytes(): ByteArray +/** + * Writes the given bytes to this file. + * + * @param bytes The bytes to write. + */ +public expect suspend infix fun PlatformFile.write(bytes: ByteArray): Unit + /** * Reads the content of the file as a string. * @@ -60,6 +67,13 @@ public expect suspend fun PlatformFile.readBytes(): ByteArray */ public expect suspend fun PlatformFile.readString(): String +/** + * Writes the given string to this file. + * + * @param string The string to write. + */ +public expect suspend fun PlatformFile.writeString(string: String): Unit + /** * Returns the MIME type of the file. * @@ -107,6 +121,20 @@ public expect inline fun PlatformFile.list(block: (List) -> Unit) */ public expect fun PlatformFile.list(): List +/** + * Deletes this file. + * + * @param mustExist If `true`, fails if the file does not exist. Defaults to `true`. + * @param recursively If `true`, a directory is emptied before it is removed. Defaults to `false`, + * which fails on a filesystem directory that still has contents. Symbolic links (including dangling + * links) and Windows directory junctions are unlinked, never followed. Android document URI deletion + * is handled by the document provider regardless of this flag. + */ +public expect suspend fun PlatformFile.delete( + mustExist: Boolean = true, + recursively: Boolean = false, +) + /** * Starts accessing a security-scoped resource. * diff --git a/filekit-core/src/commonMain/kotlin/io/github/vinceglb/filekit/PlatformFileAsync.kt b/filekit-core/src/commonMain/kotlin/io/github/vinceglb/filekit/PlatformFileAsync.kt index 08f3eb8e..52881ef6 100644 --- a/filekit-core/src/commonMain/kotlin/io/github/vinceglb/filekit/PlatformFileAsync.kt +++ b/filekit-core/src/commonMain/kotlin/io/github/vinceglb/filekit/PlatformFileAsync.kt @@ -19,17 +19,6 @@ public expect suspend fun PlatformFile.directory( create: Boolean = false, ): PlatformFile -/** Replaces this file's content with [bytes]. */ -public expect suspend fun PlatformFile.writeBytes(bytes: ByteArray) - -/** Replaces this file's content with [string]. */ -public suspend fun PlatformFile.writeStringAsync(string: String) { - writeBytes(string.encodeToByteArray()) -} - -/** Deletes this file or empty directory. */ -public expect suspend fun PlatformFile.deleteAsync(mustExist: Boolean = true) - /** Returns whether this file currently exists. */ public expect suspend fun PlatformFile.existsAsync(): Boolean diff --git a/filekit-core/src/nonWebMain/kotlin/io/github/vinceglb/filekit/FileKit.nonWeb.kt b/filekit-core/src/nonWebMain/kotlin/io/github/vinceglb/filekit/FileKit.nonWeb.kt index ff420f9f..6a9e67b4 100644 --- a/filekit-core/src/nonWebMain/kotlin/io/github/vinceglb/filekit/FileKit.nonWeb.kt +++ b/filekit-core/src/nonWebMain/kotlin/io/github/vinceglb/filekit/FileKit.nonWeb.kt @@ -38,10 +38,6 @@ public expect val FileKit.projectDir: PlatformFile public actual suspend fun FileKit.filesDirectory(): PlatformFile = filesDir -public actual suspend fun FileKit.cacheDirectory(): PlatformFile = cacheDir - -public actual suspend fun FileKit.databasesDirectory(): PlatformFile = databasesDir - /** * Saves an image to the platform's gallery or photo album. * diff --git a/filekit-core/src/nonWebMain/kotlin/io/github/vinceglb/filekit/PlatformFile.nonWeb.kt b/filekit-core/src/nonWebMain/kotlin/io/github/vinceglb/filekit/PlatformFile.nonWeb.kt index 9e11051b..113f2ccc 100644 --- a/filekit-core/src/nonWebMain/kotlin/io/github/vinceglb/filekit/PlatformFile.nonWeb.kt +++ b/filekit-core/src/nonWebMain/kotlin/io/github/vinceglb/filekit/PlatformFile.nonWeb.kt @@ -119,7 +119,7 @@ public actual suspend fun PlatformFile.readString(): String = * * @param bytes The bytes to write. */ -public suspend infix fun PlatformFile.write(bytes: ByteArray): Unit = +public actual suspend infix fun PlatformFile.write(bytes: ByteArray): Unit = withContext(Dispatchers.IO) { this@write .sink() @@ -163,7 +163,7 @@ public suspend infix fun PlatformFile.write(platformFile: PlatformFile): Unit = * * @param string The string to write. */ -public suspend fun PlatformFile.writeString(string: String): Unit = +public actual suspend fun PlatformFile.writeString(string: String): Unit = withContext(Dispatchers.IO) { this@writeString .sink() @@ -212,20 +212,6 @@ public expect fun PlatformFile.createDirectories(mustCreate: Boolean = false) */ public expect suspend fun PlatformFile.atomicMove(destination: PlatformFile) -/** - * Deletes this file. - * - * @param mustExist If `true`, fails if the file does not exist. Defaults to `true`. - * @param recursively If `true`, a directory is emptied before it is removed. Defaults to `false`, - * which fails on a filesystem directory that still has contents. Symbolic links (including dangling - * links) and Windows directory junctions are unlinked, never followed. Android document URI deletion - * is handled by the document provider regardless of this flag. - */ -public expect suspend fun PlatformFile.delete( - mustExist: Boolean = true, - recursively: Boolean = false, -) - /** * Empties this directory, depth first, leaving the directory itself in place. Does nothing when * this is not a directory. diff --git a/filekit-core/src/nonWebMain/kotlin/io/github/vinceglb/filekit/PlatformFileAsync.nonWeb.kt b/filekit-core/src/nonWebMain/kotlin/io/github/vinceglb/filekit/PlatformFileAsync.nonWeb.kt index 6e45a6c0..eff99a52 100644 --- a/filekit-core/src/nonWebMain/kotlin/io/github/vinceglb/filekit/PlatformFileAsync.nonWeb.kt +++ b/filekit-core/src/nonWebMain/kotlin/io/github/vinceglb/filekit/PlatformFileAsync.nonWeb.kt @@ -34,14 +34,6 @@ public actual suspend fun PlatformFile.directory( return directory } -public actual suspend fun PlatformFile.writeBytes(bytes: ByteArray) { - this write bytes -} - -public actual suspend fun PlatformFile.deleteAsync(mustExist: Boolean) { - delete(mustExist) -} - public actual suspend fun PlatformFile.existsAsync(): Boolean = exists() public actual suspend fun PlatformFile.sizeAsync(): Long = size() diff --git a/filekit-core/src/webMain/kotlin/io/github/vinceglb/filekit/FileKit.web.kt b/filekit-core/src/webMain/kotlin/io/github/vinceglb/filekit/FileKit.web.kt index 68de89f4..b23a6b1c 100644 --- a/filekit-core/src/webMain/kotlin/io/github/vinceglb/filekit/FileKit.web.kt +++ b/filekit-core/src/webMain/kotlin/io/github/vinceglb/filekit/FileKit.web.kt @@ -1,5 +1,16 @@ package io.github.vinceglb.filekit +/** + * Reads the root directory of the browser's origin private file system (OPFS). + * + * The returned [PlatformFile] retains a live OPFS directory handle. + * + * OPFS is available only in secure contexts in browsers that implement the File + * System API. + */ +public actual suspend fun FileKit.filesDirectory(): PlatformFile = + PlatformFile.fromOriginPrivateFileSystem() + /** * Downloads a file in the browser. * diff --git a/filekit-core/src/webMain/kotlin/io/github/vinceglb/filekit/OriginPrivateFileSystem.web.kt b/filekit-core/src/webMain/kotlin/io/github/vinceglb/filekit/OriginPrivateFileSystem.web.kt index dea0121e..f2f1b774 100644 --- a/filekit-core/src/webMain/kotlin/io/github/vinceglb/filekit/OriginPrivateFileSystem.web.kt +++ b/filekit-core/src/webMain/kotlin/io/github/vinceglb/filekit/OriginPrivateFileSystem.web.kt @@ -11,15 +11,15 @@ import kotlin.js.Promise * Reads the root directory of the browser's origin private file system (OPFS). * * The returned [PlatformFile] retains a live OPFS directory handle. Use - * [listOriginPrivateFileSystemEntries], [originPrivateFileSystemFile], and - * [originPrivateFileSystemDirectory] to interact with its entries + * [listAsync], [file], and + * [directory] to interact with its entries * asynchronously. * * OPFS is available only in secure contexts in browsers that implement the File * System API. */ @OptIn(ExperimentalWasmJsInterop::class) -public suspend fun PlatformFile.Companion.fromOriginPrivateFileSystem(): PlatformFile { +internal suspend fun PlatformFile.Companion.fromOriginPrivateFileSystem(): PlatformFile { val root = browserNavigator.storage.getDirectory().await() return PlatformFile( WebFile.OriginPrivateDirectory( @@ -31,10 +31,8 @@ public suspend fun PlatformFile.Companion.fromOriginPrivateFileSystem(): Platfor } @OptIn(ExperimentalWasmJsInterop::class) -public suspend fun PlatformFile.listOriginPrivateFileSystemEntries(): List { - val directory = webFile as? WebFile.OriginPrivateDirectory - ?: throw FileKitException("This file is not an origin private file system directory") - val iterator = directory.handle.values() +internal suspend fun WebFile.OriginPrivateDirectory.list(): List { + val iterator = handle.values() val entries = mutableListOf() while (true) { @@ -43,19 +41,19 @@ public suspend fun PlatformFile.listOriginPrivateFileSystemEntries(): List WebFile.OriginPrivateFile( handle = handle.unsafeCast(), path = path, - parent = directory, + parent = this, ) "directory" -> WebFile.OriginPrivateDirectory( handle = handle.unsafeCast(), path = path, - parent = directory, + parent = this, ) else -> throw FileKitException("Unsupported origin private file system entry type: ${handle.kind}") @@ -68,44 +66,34 @@ public suspend fun PlatformFile.listOriginPrivateFileSystemEntries(): List + fun createWritable(): Promise } @OptIn(ExperimentalWasmJsInterop::class) internal external interface FileSystemDirectoryHandle : FileSystemHandle { fun values(): FileSystemDirectoryHandleIterator + fun getFileHandle(name: String, options: FileSystemGetHandleOptions): Promise + fun getDirectoryHandle(name: String, options: FileSystemGetHandleOptions): Promise + fun removeEntry(name: String): Promise } @@ -182,6 +165,7 @@ internal fun FileSystemGetHandleOptions(create: Boolean): FileSystemGetHandleOpt @OptIn(ExperimentalWasmJsInterop::class) internal external interface FileSystemWritableFileStream : JsAny { fun write(data: JsAny): Promise + fun close(): Promise } diff --git a/filekit-core/src/webMain/kotlin/io/github/vinceglb/filekit/PlatformFile.web.kt b/filekit-core/src/webMain/kotlin/io/github/vinceglb/filekit/PlatformFile.web.kt index 9b66164f..fc479e04 100644 --- a/filekit-core/src/webMain/kotlin/io/github/vinceglb/filekit/PlatformFile.web.kt +++ b/filekit-core/src/webMain/kotlin/io/github/vinceglb/filekit/PlatformFile.web.kt @@ -2,8 +2,8 @@ package io.github.vinceglb.filekit import io.github.vinceglb.filekit.exceptions.FileKitException import io.github.vinceglb.filekit.mimeType.MimeType -import kotlinx.coroutines.await import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.await import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.withContext import kotlinx.serialization.Serializable @@ -58,8 +58,13 @@ public actual val PlatformFile.nameWithoutExtension: String public actual fun PlatformFile.size(): Long = when (val file = webFile) { is WebFile.FileWrapper -> file.size + is WebFile.DirectoryWrapper -> 0 + + // Unfortunately, we cannot support this synchronously :-( + // Maybe the size function should be deprecated or removed from web target is WebFile.OriginPrivateFile -> 0 + is WebFile.OriginPrivateDirectory -> 0 } @@ -84,12 +89,16 @@ public actual fun PlatformFile.mimeType(): MimeType? = when (val file = webFile) is WebFile.OriginPrivateFile, is WebFile.OriginPrivateDirectory, - -> null + -> { + null + } } public actual fun PlatformFile.lastModified(): Instant = when (val file = webFile) { is WebFile.FileWrapper -> file.lastModified + is WebFile.DirectoryWrapper -> file.lastModified + is WebFile.OriginPrivateFile, is WebFile.OriginPrivateDirectory, -> WEB_DIRECTORY_LAST_MODIFIED @@ -113,9 +122,18 @@ public actual inline fun PlatformFile.list(block: (List) -> Unit) } public actual fun PlatformFile.list(): List = when (val file = webFile) { - is WebFile.FileWrapper -> throw FileKitException("Cannot list a regular file") - is WebFile.DirectoryWrapper -> file.children.map { it.toPlatformFile() } - is WebFile.OriginPrivateFile -> throw FileKitException("Cannot list a regular file") + is WebFile.FileWrapper -> { + throw FileKitException("Cannot list a regular file") + } + + is WebFile.DirectoryWrapper -> { + file.children.map { it.toPlatformFile() } + } + + is WebFile.OriginPrivateFile -> { + throw FileKitException("Cannot list a regular file") + } + is WebFile.OriginPrivateDirectory -> { throw FileKitException("Origin private file system directories must be listed asynchronously") } @@ -127,11 +145,24 @@ public actual fun PlatformFile.stopAccessingSecurityScopedResource() {} @OptIn(ExperimentalWasmJsInterop::class) public actual suspend fun PlatformFile.readBytes(): ByteArray = when (val file = webFile) { - is WebFile.FileWrapper -> file.file.readBytes() + is WebFile.FileWrapper -> { + file.file.readBytes() + } + + is WebFile.DirectoryWrapper -> { + throw FileKitException("Cannot read bytes from a directory") + } + + is WebFile.OriginPrivateFile -> { + file.handle + .getFile() + .await() + .readBytes() + } - is WebFile.DirectoryWrapper -> throw FileKitException("Cannot read bytes from a directory") - is WebFile.OriginPrivateFile -> file.handle.getFile().await().readBytes() - is WebFile.OriginPrivateDirectory -> throw FileKitException("Cannot read bytes from a directory") + is WebFile.OriginPrivateDirectory -> { + throw FileKitException("Cannot read bytes from a directory") + } } public actual suspend fun PlatformFile.readString(): String = @@ -165,3 +196,43 @@ private suspend fun BrowserFile.readBytes(): ByteArray = withContext(Dispatchers reader.readAsArrayBuffer(this@readBytes) } } + +public actual suspend infix fun PlatformFile.write(bytes: ByteArray) { + when (webFile) { + is WebFile.OriginPrivateFile -> webFile.write(bytes) + else -> throw FileKitException("This file is not a writable origin private file system file") + } +} + +public actual suspend fun PlatformFile.writeString(string: String) { + write(string.encodeToByteArray()) +} + +@OptIn(ExperimentalWasmJsInterop::class) +public actual suspend fun PlatformFile.delete(mustExist: Boolean, recursively: Boolean) { + when (val file = webFile) { + is WebFile.OriginPrivateFile -> { + val parent = file.parent + ?: throw FileKitException("Cannot delete the origin private file system root") + if (mustExist) { + parent.handle.removeEntry(file.name).await() + } else { + runCatching { parent.handle.removeEntry(file.name).await() } + } + } + + is WebFile.OriginPrivateDirectory -> { + val parent = file.parent + ?: throw FileKitException("Cannot delete the origin private file system root") + if (mustExist) { + parent.handle.removeEntry(file.name).await() + } else { + runCatching { parent.handle.removeEntry(file.name).await() } + } + } + + else -> { + throw FileKitException("This file is not a writable origin private file system entry") + } + } +} diff --git a/filekit-core/src/webMain/kotlin/io/github/vinceglb/filekit/PlatformFileAsync.web.kt b/filekit-core/src/webMain/kotlin/io/github/vinceglb/filekit/PlatformFileAsync.web.kt index ab66a633..295282de 100644 --- a/filekit-core/src/webMain/kotlin/io/github/vinceglb/filekit/PlatformFileAsync.web.kt +++ b/filekit-core/src/webMain/kotlin/io/github/vinceglb/filekit/PlatformFileAsync.web.kt @@ -13,57 +13,30 @@ public actual suspend fun PlatformFile.listAsync(): List = when (v is WebFile.OriginPrivateFile, -> throw FileKitException("Cannot list a regular file") - is WebFile.DirectoryWrapper -> file.children.map(WebFile::toPlatformFile) - is WebFile.OriginPrivateDirectory -> listOriginPrivateFileSystemEntries() + is WebFile.DirectoryWrapper -> file.children.map { it.toPlatformFile() } + + is WebFile.OriginPrivateDirectory -> file.list() } public actual suspend fun PlatformFile.file( name: String, create: Boolean, ): PlatformFile = when (webFile) { - is WebFile.OriginPrivateDirectory -> originPrivateFileSystemFile(name, create) + is WebFile.OriginPrivateDirectory -> webFile.file(name, create) else -> throw FileKitException("This file is not a writable origin private file system directory") } +@OptIn(ExperimentalWasmJsInterop::class) public actual suspend fun PlatformFile.directory( name: String, create: Boolean, ): PlatformFile = when (webFile) { - is WebFile.OriginPrivateDirectory -> originPrivateFileSystemDirectory(name, create) - else -> throw FileKitException("This file is not a writable origin private file system directory") -} - -public actual suspend fun PlatformFile.writeBytes(bytes: ByteArray) { - when (webFile) { - is WebFile.OriginPrivateFile -> writeToOriginPrivateFileSystem(bytes) - else -> throw FileKitException("This file is not a writable origin private file system file") + is WebFile.OriginPrivateDirectory -> { + webFile.directory(name, create) } -} -@OptIn(ExperimentalWasmJsInterop::class) -public actual suspend fun PlatformFile.deleteAsync(mustExist: Boolean) { - when (val file = webFile) { - is WebFile.OriginPrivateFile -> { - val parent = file.parent - ?: throw FileKitException("Cannot delete the origin private file system root") - if (mustExist) { - parent.handle.removeEntry(file.name).await() - } else { - runCatching { parent.handle.removeEntry(file.name).await() } - } - } - - is WebFile.OriginPrivateDirectory -> { - val parent = file.parent - ?: throw FileKitException("Cannot delete the origin private file system root") - if (mustExist) { - parent.handle.removeEntry(file.name).await() - } else { - runCatching { parent.handle.removeEntry(file.name).await() } - } - } - - else -> throw FileKitException("This file is not a writable origin private file system entry") + else -> { + throw FileKitException("This file is not a writable origin private file system directory") } } @@ -84,35 +57,63 @@ public actual suspend fun PlatformFile.existsAsync(): Boolean = when (val file = @OptIn(ExperimentalWasmJsInterop::class) public actual suspend fun PlatformFile.sizeAsync(): Long = when (val file = webFile) { - is WebFile.FileWrapper -> file.size + is WebFile.FileWrapper -> { + file.size + } + is WebFile.DirectoryWrapper, is WebFile.OriginPrivateDirectory, - -> 0 + -> { + 0 + } - is WebFile.OriginPrivateFile -> file.handle.getFile().await().size.toDouble().toLong() + is WebFile.OriginPrivateFile -> { + file.handle + .getFile() + .await() + .size + .toDouble() + .toLong() + } } @OptIn(ExperimentalWasmJsInterop::class) public actual suspend fun PlatformFile.mimeTypeAsync(): MimeType? = when (val file = webFile) { - is WebFile.FileWrapper -> file.type.takeIf(String::isNotBlank)?.let(MimeType::parse) + is WebFile.FileWrapper -> { + file.type.takeIf(String::isNotBlank)?.let(MimeType::parse) + } + is WebFile.DirectoryWrapper, is WebFile.OriginPrivateDirectory, - -> null + -> { + null + } - is WebFile.OriginPrivateFile -> file.handle.getFile().await().type - .takeIf(String::isNotBlank) - ?.let(MimeType::parse) + is WebFile.OriginPrivateFile -> { + file.handle + .getFile() + .await() + .type + .takeIf(String::isNotBlank) + ?.let(MimeType::parse) + } } @Suppress("REDUNDANT_CALL_OF_CONVERSION_METHOD") @OptIn(ExperimentalTime::class, ExperimentalWasmJsInterop::class) public actual suspend fun PlatformFile.lastModifiedAsync(): Instant = when (val file = webFile) { is WebFile.FileWrapper -> file.lastModified + is WebFile.DirectoryWrapper, is WebFile.OriginPrivateDirectory, -> WEB_DIRECTORY_LAST_MODIFIED is WebFile.OriginPrivateFile -> Instant.fromEpochMilliseconds( - file.handle.getFile().await().lastModified.toDouble().toLong(), + file.handle + .getFile() + .await() + .lastModified + .toDouble() + .toLong(), ) } diff --git a/filekit-core/src/webTest/kotlin/io/github/vinceglb/filekit/PlatformFileWebTest.kt b/filekit-core/src/webTest/kotlin/io/github/vinceglb/filekit/PlatformFileWebTest.kt index d34a124a..27c49847 100644 --- a/filekit-core/src/webTest/kotlin/io/github/vinceglb/filekit/PlatformFileWebTest.kt +++ b/filekit-core/src/webTest/kotlin/io/github/vinceglb/filekit/PlatformFileWebTest.kt @@ -201,39 +201,9 @@ class PlatformFileWebTest { @Test fun testOriginPrivateFileSystemStorageDirectories() = runTest { val filesDir = FileKit.filesDirectory() - val cacheDir = FileKit.cacheDirectory() - val databasesDir = FileKit.databasesDirectory() assertTrue(filesDir.isDirectory()) - assertTrue(cacheDir.isDirectory()) - assertTrue(databasesDir.isDirectory()) - assertEquals("cache", cacheDir.name) - assertEquals("databases", databasesDir.name) assertEquals("", filesDir.path) - assertEquals("cache", cacheDir.path) - assertEquals("databases", databasesDir.path) - } - - @Test - fun testOriginPrivateFileSystemFileOperations() = runTest { - val cacheDir = FileKit.cacheDirectory() - val file = cacheDir.file(name = "filekit-opfs-test.txt", create = true) - - try { - file.writeStringAsync("Hello, OPFS!") - - assertTrue(file.existsAsync()) - assertEquals("Hello, OPFS!", file.readString()) - assertEquals(12L, file.sizeAsync()) - assertEquals( - listOf("filekit-opfs-test.txt"), - cacheDir.listAsync().filter { it.name == file.name }.map { it.name }, - ) - } finally { - file.deleteAsync(mustExist = false) - } - - assertFalse(file.existsAsync()) } private fun PlatformFile.webFileWrapper(): WebFile.FileWrapper = From f5e50a7df9674a5d8c9fd757451e53883e7895a8 Mon Sep 17 00:00:00 2001 From: Timo Drick Date: Sun, 20 Sep 2026 22:57:12 +0200 Subject: [PATCH 4/5] Implemented WebFile snapshot when creating a OriginPrivateFile to support file size and last modified time. Can be updated with PlatformFile.update() to refresh metadata. --- .../vinceglb/filekit/PlatformFileAsync.kt | 11 ++++ .../filekit/PlatformFileAsync.nonWeb.kt | 2 + .../filekit/OriginPrivateFileSystem.web.kt | 52 ++++++++++++------- .../vinceglb/filekit/PlatformFile.web.kt | 24 ++++----- .../vinceglb/filekit/PlatformFileAsync.web.kt | 8 +++ .../io/github/vinceglb/filekit/WebFile.web.kt | 23 +++++++- .../vinceglb/filekit/PlatformFileWebTest.kt | 20 +++++++ 7 files changed, 104 insertions(+), 36 deletions(-) diff --git a/filekit-core/src/commonMain/kotlin/io/github/vinceglb/filekit/PlatformFileAsync.kt b/filekit-core/src/commonMain/kotlin/io/github/vinceglb/filekit/PlatformFileAsync.kt index 52881ef6..3ddfcddc 100644 --- a/filekit-core/src/commonMain/kotlin/io/github/vinceglb/filekit/PlatformFileAsync.kt +++ b/filekit-core/src/commonMain/kotlin/io/github/vinceglb/filekit/PlatformFileAsync.kt @@ -31,3 +31,14 @@ public expect suspend fun PlatformFile.mimeTypeAsync(): MimeType? /** Returns this file's current modification time. */ @OptIn(ExperimentalTime::class) public expect suspend fun PlatformFile.lastModifiedAsync(): Instant + +/** + * Refreshes metadata cached by this platform file. + * + * Files whose metadata is read directly from their backing filesystem do not need an update. + */ +public suspend fun PlatformFile.update() { + updatePlatformData() +} + +internal expect suspend fun PlatformFile.updatePlatformData() diff --git a/filekit-core/src/nonWebMain/kotlin/io/github/vinceglb/filekit/PlatformFileAsync.nonWeb.kt b/filekit-core/src/nonWebMain/kotlin/io/github/vinceglb/filekit/PlatformFileAsync.nonWeb.kt index eff99a52..cbec36e2 100644 --- a/filekit-core/src/nonWebMain/kotlin/io/github/vinceglb/filekit/PlatformFileAsync.nonWeb.kt +++ b/filekit-core/src/nonWebMain/kotlin/io/github/vinceglb/filekit/PlatformFileAsync.nonWeb.kt @@ -42,3 +42,5 @@ public actual suspend fun PlatformFile.mimeTypeAsync(): MimeType? = mimeType() @OptIn(ExperimentalTime::class) public actual suspend fun PlatformFile.lastModifiedAsync(): Instant = lastModified() + +internal actual suspend fun PlatformFile.updatePlatformData() {} diff --git a/filekit-core/src/webMain/kotlin/io/github/vinceglb/filekit/OriginPrivateFileSystem.web.kt b/filekit-core/src/webMain/kotlin/io/github/vinceglb/filekit/OriginPrivateFileSystem.web.kt index f2f1b774..4f0393d8 100644 --- a/filekit-core/src/webMain/kotlin/io/github/vinceglb/filekit/OriginPrivateFileSystem.web.kt +++ b/filekit-core/src/webMain/kotlin/io/github/vinceglb/filekit/OriginPrivateFileSystem.web.kt @@ -44,19 +44,27 @@ internal suspend fun WebFile.OriginPrivateDirectory.list(): List { val path = path.appendOriginPrivateFileSystemPath(handle.name) entries += PlatformFile( when (handle.kind) { - "file" -> WebFile.OriginPrivateFile( - handle = handle.unsafeCast(), - path = path, - parent = this, - ) - - "directory" -> WebFile.OriginPrivateDirectory( - handle = handle.unsafeCast(), - path = path, - parent = this, - ) - - else -> throw FileKitException("Unsupported origin private file system entry type: ${handle.kind}") + "file" -> { + val fileHandle = handle.unsafeCast() + WebFile.OriginPrivateFile( + handle = fileHandle, + fileSnapshot = fileHandle.getFile().await(), + path = path, + parent = this, + ) + } + + "directory" -> { + WebFile.OriginPrivateDirectory( + handle = handle.unsafeCast(), + path = path, + parent = this, + ) + } + + else -> { + throw FileKitException("Unsupported origin private file system entry type: ${handle.kind}") + } }, ) } @@ -69,13 +77,17 @@ internal suspend fun WebFile.OriginPrivateDirectory.list(): List { internal suspend fun WebFile.OriginPrivateDirectory.file( name: String, create: Boolean = false, -): PlatformFile = PlatformFile( - WebFile.OriginPrivateFile( - handle = handle.getFileHandle(name, FileSystemGetHandleOptions(create)).await(), - path = path.appendOriginPrivateFileSystemPath(name), - parent = this, - ), -) +): PlatformFile { + val fileHandle = handle.getFileHandle(name, FileSystemGetHandleOptions(create)).await() + return PlatformFile( + WebFile.OriginPrivateFile( + handle = fileHandle, + fileSnapshot = fileHandle.getFile().await(), + path = path.appendOriginPrivateFileSystemPath(name), + parent = this, + ), + ) +} /** Returns an OPFS directory in this directory, creating it when [create] is true. */ @OptIn(ExperimentalWasmJsInterop::class) diff --git a/filekit-core/src/webMain/kotlin/io/github/vinceglb/filekit/PlatformFile.web.kt b/filekit-core/src/webMain/kotlin/io/github/vinceglb/filekit/PlatformFile.web.kt index fc479e04..cc945105 100644 --- a/filekit-core/src/webMain/kotlin/io/github/vinceglb/filekit/PlatformFile.web.kt +++ b/filekit-core/src/webMain/kotlin/io/github/vinceglb/filekit/PlatformFile.web.kt @@ -58,13 +58,8 @@ public actual val PlatformFile.nameWithoutExtension: String public actual fun PlatformFile.size(): Long = when (val file = webFile) { is WebFile.FileWrapper -> file.size - is WebFile.DirectoryWrapper -> 0 - - // Unfortunately, we cannot support this synchronously :-( - // Maybe the size function should be deprecated or removed from web target - is WebFile.OriginPrivateFile -> 0 - + is WebFile.OriginPrivateFile -> file.size is WebFile.OriginPrivateDirectory -> 0 } @@ -87,21 +82,22 @@ public actual fun PlatformFile.mimeType(): MimeType? = when (val file = webFile) null } - is WebFile.OriginPrivateFile, - is WebFile.OriginPrivateDirectory, - -> { + is WebFile.OriginPrivateFile -> { + file.type + .takeIf { it.isNotBlank() } + ?.let { MimeType.parse(it) } + } + + is WebFile.OriginPrivateDirectory -> { null } } public actual fun PlatformFile.lastModified(): Instant = when (val file = webFile) { is WebFile.FileWrapper -> file.lastModified - is WebFile.DirectoryWrapper -> file.lastModified - - is WebFile.OriginPrivateFile, - is WebFile.OriginPrivateDirectory, - -> WEB_DIRECTORY_LAST_MODIFIED + is WebFile.OriginPrivateFile -> file.lastModified + is WebFile.OriginPrivateDirectory -> WEB_DIRECTORY_LAST_MODIFIED } public actual fun PlatformFile.parent(): PlatformFile? = when (val file = webFile) { diff --git a/filekit-core/src/webMain/kotlin/io/github/vinceglb/filekit/PlatformFileAsync.web.kt b/filekit-core/src/webMain/kotlin/io/github/vinceglb/filekit/PlatformFileAsync.web.kt index 295282de..6ef61765 100644 --- a/filekit-core/src/webMain/kotlin/io/github/vinceglb/filekit/PlatformFileAsync.web.kt +++ b/filekit-core/src/webMain/kotlin/io/github/vinceglb/filekit/PlatformFileAsync.web.kt @@ -117,3 +117,11 @@ public actual suspend fun PlatformFile.lastModifiedAsync(): Instant = when (val .toLong(), ) } + +@OptIn(ExperimentalWasmJsInterop::class) +internal actual suspend fun PlatformFile.updatePlatformData() { + when (val file = webFile) { + is WebFile.OriginPrivateFile -> file.updateSnapshot(file.handle.getFile().await()) + else -> Unit + } +} diff --git a/filekit-core/src/webMain/kotlin/io/github/vinceglb/filekit/WebFile.web.kt b/filekit-core/src/webMain/kotlin/io/github/vinceglb/filekit/WebFile.web.kt index 60acfff3..8a4c07e5 100644 --- a/filekit-core/src/webMain/kotlin/io/github/vinceglb/filekit/WebFile.web.kt +++ b/filekit-core/src/webMain/kotlin/io/github/vinceglb/filekit/WebFile.web.kt @@ -13,10 +13,13 @@ import kotlin.time.Instant public sealed class WebFile { @OptIn(ExperimentalWasmJsInterop::class) public class FileWrapper( - public val file: BrowserFile, + file: BrowserFile, path: String? = file.webkitRelativePath, public val parent: DirectoryWrapper? = null, ) : WebFile() { + public var file: BrowserFile = file + internal set + public val path: String = path?.takeIf { it.isNotBlank() } ?: file.name public val name: String @@ -45,13 +48,29 @@ public sealed class WebFile { public val lastModified: Instant = WEB_DIRECTORY_LAST_MODIFIED } + @OptIn(ExperimentalWasmJsInterop::class) public class OriginPrivateFile internal constructor( internal val handle: FileSystemFileHandle, + private var fileSnapshot: BrowserFile, public val path: String, public val parent: OriginPrivateDirectory?, ) : WebFile() { public val name: String - get() = handle.name + get() = fileSnapshot.name + + public val type: String + get() = fileSnapshot.type + + public val size: Long + get() = fileSnapshot.size.toDouble().toLong() + + @Suppress("REDUNDANT_CALL_OF_CONVERSION_METHOD") + public val lastModified: Instant + get() = Instant.fromEpochMilliseconds(fileSnapshot.lastModified.toDouble().toLong()) + + internal fun updateSnapshot(fileSnapshot: BrowserFile) { + this.fileSnapshot = fileSnapshot + } } public class OriginPrivateDirectory internal constructor( diff --git a/filekit-core/src/webTest/kotlin/io/github/vinceglb/filekit/PlatformFileWebTest.kt b/filekit-core/src/webTest/kotlin/io/github/vinceglb/filekit/PlatformFileWebTest.kt index 27c49847..a6430bf8 100644 --- a/filekit-core/src/webTest/kotlin/io/github/vinceglb/filekit/PlatformFileWebTest.kt +++ b/filekit-core/src/webTest/kotlin/io/github/vinceglb/filekit/PlatformFileWebTest.kt @@ -206,6 +206,26 @@ class PlatformFileWebTest { assertEquals("", filesDir.path) } + @Test + fun testOriginPrivateFileUpdateRefreshesSnapshotAttributes() = runTest { + val root = FileKit.filesDirectory() + val name = "filekit-attributes-test.txt" + val file = root.file(name, create = true) + + try { + file.writeString("Hello") + + assertEquals(0L, file.size()) + + file.update() + + assertEquals(5L, file.size()) + assertTrue(file.lastModified() > Instant.fromEpochMilliseconds(0)) + } finally { + file.delete(mustExist = false) + } + } + private fun PlatformFile.webFileWrapper(): WebFile.FileWrapper = assertIs(webFile) } From d4a98c538efef9d732b38d77c8ec2d643a160674 Mon Sep 17 00:00:00 2001 From: Timo Drick Date: Mon, 21 Sep 2026 18:28:22 +0200 Subject: [PATCH 5/5] Fixed linux test for new FileKit.filesDirectory() function --- .../kotlin/io/github/vinceglb/filekit/FileKitLinuxTest.kt | 2 -- 1 file changed, 2 deletions(-) diff --git a/filekit-core/src/linuxTest/kotlin/io/github/vinceglb/filekit/FileKitLinuxTest.kt b/filekit-core/src/linuxTest/kotlin/io/github/vinceglb/filekit/FileKitLinuxTest.kt index 7a0d1ee8..0c17fe1d 100644 --- a/filekit-core/src/linuxTest/kotlin/io/github/vinceglb/filekit/FileKitLinuxTest.kt +++ b/filekit-core/src/linuxTest/kotlin/io/github/vinceglb/filekit/FileKitLinuxTest.kt @@ -62,8 +62,6 @@ class FileKitLinuxTest { FileKit.init(appId = APP_ID) assertEquals(FileKit.filesDir.path, FileKit.filesDirectory().path) - assertEquals(FileKit.cacheDir.path, FileKit.cacheDirectory().path) - assertEquals(FileKit.databasesDir.path, FileKit.databasesDirectory().path) } @Test