Michael Paus
07/31/2025, 10:03 AMMichael Paus
07/31/2025, 10:03 AMimport js.objects.unsafeJso
import jszip.JSZip
import kotlin.test.Test
// See: <https://github.com/JetBrains/kotlin-wrappers>
// and <https://stuk.github.io/jszip/>
class ZipWrapperTest {
private val imgData = "R0lGODdhBQAFAIACAAAAAP/eACwAAAAABQAFAAACCIwPkWerClIBADs=" // smile.gif as Base64.
@Test
fun test() {
val zip = JSZip()
zip.file("Hello.txt", "Hello World\n")
val img = checkNotNull(zip.folder("images"))
img.file("smile.gif", imgData, unsafeJso { base64 = true })
// ???
// zip.generateAsync({type:"blob"})
// .then(function(content) {
// // see FileSaver.js
// saveAs(content, "example.zip");
// });
}
}
Konstantin Tskhovrebov
07/31/2025, 10:33 AMKonstantin Tskhovrebov
07/31/2025, 10:34 AMturansky
07/31/2025, 11:03 AMval options: JSZipGeneratorOptions<Blob> = unsafeJso { type = OutputType.blob }
zip.generateAsync(options)
.then { saveAs(it, "example.zip") }
turansky
07/31/2025, 11:04 AMval options: JSZipGeneratorOptions<Blob> = unsafeJso { type = OutputType.blob }
val content = zip.generate(options)
saveAs(content, "example.zip") }
Michael Paus
07/31/2025, 11:32 AMsaveAs
come from? I can’t find it.turansky
07/31/2025, 11:32 AMturansky
07/31/2025, 11:34 AMFileSaver.js
Michael Paus
07/31/2025, 11:34 AMturansky
07/31/2025, 11:35 AMturansky
07/31/2025, 11:36 AMMichael Paus
07/31/2025, 11:37 AMturansky
07/31/2025, 11:39 AMMichael Paus
07/31/2025, 12:54 PMshowOpenFilePicker
. So currently there does not seem to be an option to get a file picker for saving a file.turansky
07/31/2025, 1:00 PMMichael Paus
07/31/2025, 1:12 PMval promise: Promise<FileSystemFileHandle> = showSaveFilePickerAsync(options)
but what have I to do next to actually write the content Blob to a file. As I said already, I don’t have a JavaScript background.Konstantin Tskhovrebov
07/31/2025, 1:16 PMMichael Paus
07/31/2025, 1:26 PMMichael Paus
07/31/2025, 1:27 PMturansky
07/31/2025, 2:45 PMMichael Paus
07/31/2025, 3:04 PMimport js.objects.unsafeJso
import js.promise.Promise
import js.promise.await
import jszip.JSZip
import jszip.JSZipGeneratorOptions
import jszip.OutputType
import jszip.blob
import jszip.generate
import web.blob.Blob
import web.experimental.ExperimentalWebApi
import web.fs.FileSystemFileHandle
import web.fs.SaveFilePickerOptions
import web.fs.createWritable
import web.fs.write
actual suspend fun createZip(imgData: String) {
println("Creating ZIP file.")
val zip = JSZip()
zip.file("Hello.txt", "Hello World\n")
val img = checkNotNull(zip.folder("images"))
img.file("smile.gif", imgData, unsafeJso { base64 = true })
val options: JSZipGeneratorOptions<Blob> = unsafeJso { type = OutputType.blob }
val content: Blob = zip.generate(options)
saveZipFileAs(content, "example.zip")
}
@OptIn(ExperimentalWebApi::class)
private suspend fun saveZipFileAs(content: Blob, fileName: String) {
println("Saving ZIP file.")
val options: SaveFilePickerOptions = unsafeJso { suggestedName = fileName }
val f: FileSystemFileHandle = showSaveFilePickerAsync(options).await()
f.createWritable().write(content)
}
/**
* [MDN Reference](<https://developer.mozilla.org/docs/Web/API/Window/showSaveFilePicker>)
*/
@ExperimentalWebApi
@JsName("showSaveFilePicker")
external fun showSaveFilePickerAsync(
options: SaveFilePickerOptions = definedExternally,
): Promise<FileSystemFileHandle>
turansky
07/31/2025, 3:28 PMval w = f.createWritable()
w.write(content)
w.close()
Close call requiredturansky
07/31/2025, 3:29 PMturansky
07/31/2025, 3:32 PMshowSaveFilePicker
, but I thought, that it will be weird if you will receive error on cancellation.
But it's what you will have now with await
:(Michael Paus
07/31/2025, 4:07 PMturansky
07/31/2025, 4:08 PMclose
? 😉Michael Paus
07/31/2025, 4:10 PMturansky
07/31/2025, 4:16 PMMichael Paus
07/31/2025, 4:16 PMturansky
07/31/2025, 4:18 PM.use
support for FileSystemWritableFileStream
Michael Paus
07/31/2025, 4:31 PMturansky
07/31/2025, 4:38 PMMichael Paus
07/31/2025, 6:02 PMimport js.objects.unsafeJso
import js.promise.Promise
import jszip.JSZip
import jszip.OutputType
import jszip.blob
import jszip.generate
import web.blob.Blob
import web.experimental.ExperimentalWebApi
import web.fs.FileSystemFileHandle
import web.fs.SaveFilePickerOptions
import web.fs.createWritable
import web.fs.write
import web.streams.close
actual suspend fun createZip(imgData: String) {
JSZip().apply {
file("Hello.txt", "Hello World\n")
file("images/smile.gif", imgData, unsafeJso { base64 = true })
saveZipFileAs(generate(unsafeJso { type = OutputType.blob }), "example.zip")
}
}
@OptIn(ExperimentalWebApi::class)
private suspend fun saveZipFileAs(content: Blob, fileName: String) {
var fh: FileSystemFileHandle? = null
showSaveFilePickerAsync(unsafeJso { suggestedName = fileName }).then( { f -> fh = f; f }, { r -> r } ).await()
fh?.let {it.createWritable().apply {
write(content)
close()
}}
}
/**
* [MDN Reference](<https://developer.mozilla.org/docs/Web/API/Window/showSaveFilePicker>)
*/
@ExperimentalWebApi
@JsName("showSaveFilePicker")
external fun showSaveFilePickerAsync(
options: SaveFilePickerOptions = definedExternally,
): Promise<FileSystemFileHandle>
turansky
07/31/2025, 6:08 PMturansky
07/31/2025, 6:10 PM@OptIn(ExperimentalWebApi::class)
private suspend fun saveZipFileAs(content: Blob, fileName: String) {
val fh = showSaveFilePickerAsync(unsafeJso { suggestedName = fileName })
.catch { _ -> null }
.await()
?: return
fh.createWritable().apply {
write(content)
close()
}
}
turansky
07/31/2025, 6:11 PMMichael Paus
07/31/2025, 6:21 PMturansky
07/31/2025, 6:31 PM.use
extension or something similar if we are talking about writable.Michael Paus
07/31/2025, 6:43 PMMichael Paus
07/31/2025, 6:56 PMturansky
07/31/2025, 6:58 PMturansky
07/31/2025, 7:01 PMkotlin-file-saver
ExampleMichael Paus
08/01/2025, 10:29 AMturansky
08/01/2025, 11:59 AMturansky
08/01/2025, 3:25 PMclose()
call is required if error will occur in write
process.
fh.createWritable().apply {
write(content)
// no close call if cancellation or error will occur
close()
}
Michael Paus
08/01/2025, 5:37 PMturansky
08/02/2025, 2:04 AMturansky
08/02/2025, 2:06 AMimport web.blob.Blob
import web.dom.document
import web.html.HtmlTagName.a
import web.url.URL
/**
* Downloads the given content as a file.
* @param content - The file content.
* @param fileName - The proposed filename.
*/
fun downloadFile(
content: Blob,
fileName: String,
) {
val data = URL.createObjectURL(content)
val anchor = document.createElement(a)
anchor.href = data
anchor.download = fileName
anchor.style.display = "none"
document.body.appendChild(anchor)
anchor.click()
document.body.removeChild(anchor)
}
OriginalMichael Paus
08/02/2025, 12:08 PMturansky
08/02/2025, 4:22 PMturansky
08/02/2025, 4:25 PMhas the advantage of avoiding an external dependencyThere is no such goal in common. In current case we have unsupported library with known problems, which can be fixed in 2 lines of code.
turansky
08/03/2025, 12:50 PMturansky
08/03/2025, 12:51 PMMichael Paus
08/03/2025, 4:52 PMdownloadFile
in order to avoid confusion.
* By default, the file is directly downloaded to the default download
* directory of the browser. If you want to select the download location via a file selector,
* then you have to configure that behaviour in your browser settings.
turansky
08/03/2025, 5:33 PMMichael Paus
08/03/2025, 5:39 PMMichael Paus
08/04/2025, 12:11 PMpackage de.mpmediasoft.kotlinwrappers.jszip
import js.buffer.ArrayBuffer
import js.core.JsPrimitives.toByte
import js.core.JsPrimitives.toJsByte
import js.objects.unsafeJso
import js.promise.await
import js.typedarrays.Uint8Array
import jszip.*
import kotlinx.coroutines.test.runTest
import web.blob.Blob
import kotlin.test.Test
import kotlin.test.assertTrue
// See: <https://github.com/JetBrains/kotlin-wrappers>
// and <https://stuk.github.io/jszip/>
private fun Uint8Array<*>.toByteArray(): ByteArray = ByteArray(length) { index -> at(index)!!.toByte() }
private fun ByteArray.toUint8Array(): Uint8Array<*> = Uint8Array<ArrayBuffer>(size).apply { forEachIndexed { index, byte -> set(index, byte.toJsByte()) } }
class KotlinWrappersJsZipTest {
private val imgData = "R0lGODdhBQAFAIACAAAAAP/eACwAAAAABQAFAAACCIwPkWerClIBADs=" // smile.gif as Base64.
private suspend fun createZipFileContent(): Blob {
JSZip().apply {
file("Hello.txt", "Hello World\nAnd this is the second line.\nAnd one more.")
file("images/smile.gif", imgData, unsafeJso { base64 = true })
return generate(unsafeJso { type = OutputType.blob })
}
}
@Test
fun testConsume() = runTest {
// Create some test data:
val zipFileContent: Blob = createZipFileContent()
println("zipFileContent.size = ${zipFileContent.size.toInt()}\n")
assertTrue(zipFileContent.size.toInt() > 0)
// Examine ZIP data and extract some values:
val js = JSZip()
js.load(zipFileContent)
js.forEach { p,f ->
// println("$p -> ${f.name}, ${f.date}, ${f.comment}\n") // Just crashes when I try to print f.comment
println("$p -> ${f.name}, ${f.date}\n")
}
val textEntry: String = js.file("Hello.txt")!!.async(OutputType.text).await().toString()
assertTrue(textEntry.length > 0)
println("The file 'Hello.txt' contains the text:\n$textEntry\n")
val gifEntry : ByteArray = js.file("images/smile.gif")!!.async(OutputType.uint8array).await().toByteArray()
assertTrue(gifEntry.size > 0)
// Just to see whether we can also convert back.
val gifEntryJs: Uint8Array<*> = gifEntry.toUint8Array()
assertTrue(gifEntryJs.length > 0)
assertTrue(gifEntry.size == gifEntryJs.length)
}
}
turansky
08/04/2025, 12:21 PMJSZipObject
turansky
08/04/2025, 12:25 PMUint8Array
-> ByteArray
?turansky
08/04/2025, 1:10 PMturansky
08/04/2025, 1:13 PMMichael Paus
08/04/2025, 2:10 PMturansky
08/04/2025, 10:02 PMturansky
08/05/2025, 9:43 AMJSZipObject
and typed array adapters available in Kotlin Wrappers 2025.8.4
! 😜Michael Paus
08/05/2025, 11:58 AMturansky
08/05/2025, 3:25 PMMichael Paus
08/05/2025, 4:55 PMturansky
08/05/2025, 6:44 PMByteArray
<-> UByteArray
conversion methods - let's add them
2. I also wasn't familiar with byte arrays before your request 😉turansky
08/05/2025, 7:18 PMturansky
08/06/2025, 4:21 PMJSZip
and ByteArray
- it will be available in next release.
Feedback is welcome!turansky
08/11/2025, 1:11 AMclose
to solve issue about .use
😉
Do you have any variants?Michael Paus
08/11/2025, 6:12 AMturansky
08/11/2025, 12:11 PMfun interface ...Closable {
suspend fun close()
}
we need name for interfaceMichael Paus
08/11/2025, 1:25 PMMichael Paus
08/12/2025, 7:57 AMval content: Blob = ...
var fh: FileSystemFileHandle? = …
I was hoping to be able to just replace
fh?.createWritable()?.apply {
write(content)
close()
}
by
fh?.createWritable()?.use {
write(content)
}
but that doesn’t compile.turansky
08/12/2025, 8:44 AMuse
signature.
Writer - first parameter of lambdaturansky
08/12/2025, 8:45 AMuse { writer -> ... }
Michael Paus
08/12/2025, 9:55 AMfh?.createWritable()?.use { w ->
w.write(content)
}
Or this.
fh?.createWritable()?.use {
it.write(content)
}
Thank you.turansky
08/12/2025, 10:07 AMuse
- "safe" let
turansky
08/12/2025, 10:31 AM