diff --git a/.changes/9a48ae92-96dc-48f4-995b-5faa00f890cd.json b/.changes/9a48ae92-96dc-48f4-995b-5faa00f890cd.json new file mode 100644 index 000000000..a296571f4 --- /dev/null +++ b/.changes/9a48ae92-96dc-48f4-995b-5faa00f890cd.json @@ -0,0 +1,8 @@ +{ + "id": "9a48ae92-96dc-48f4-995b-5faa00f890cd", + "type": "feature", + "description": "Add new Kotlin/JVM methods for converting `InputStream` to `ByteStream` and for writing `ByteStream` to `OutputStream`", + "issues": [ + "awslabs/aws-sdk-kotlin#1352" + ] +} \ No newline at end of file diff --git a/runtime/runtime-core/api/runtime-core.api b/runtime/runtime-core/api/runtime-core.api index 53f4a9a18..33b6c8e63 100644 --- a/runtime/runtime-core/api/runtime-core.api +++ b/runtime/runtime-core/api/runtime-core.api @@ -418,14 +418,19 @@ public abstract class aws/smithy/kotlin/runtime/content/ByteStream$SourceStream public final class aws/smithy/kotlin/runtime/content/ByteStreamJVMKt { public static final fun asByteStream (Ljava/io/File;JJ)Laws/smithy/kotlin/runtime/content/ByteStream; public static final fun asByteStream (Ljava/io/File;Lkotlin/ranges/LongRange;)Laws/smithy/kotlin/runtime/content/ByteStream; + public static final fun asByteStream (Ljava/io/InputStream;Ljava/lang/Long;)Laws/smithy/kotlin/runtime/content/ByteStream$SourceStream; public static final fun asByteStream (Ljava/nio/file/Path;JJ)Laws/smithy/kotlin/runtime/content/ByteStream; public static final fun asByteStream (Ljava/nio/file/Path;Lkotlin/ranges/LongRange;)Laws/smithy/kotlin/runtime/content/ByteStream; public static synthetic fun asByteStream$default (Ljava/io/File;JJILjava/lang/Object;)Laws/smithy/kotlin/runtime/content/ByteStream; + public static synthetic fun asByteStream$default (Ljava/io/InputStream;Ljava/lang/Long;ILjava/lang/Object;)Laws/smithy/kotlin/runtime/content/ByteStream$SourceStream; public static synthetic fun asByteStream$default (Ljava/nio/file/Path;JJILjava/lang/Object;)Laws/smithy/kotlin/runtime/content/ByteStream; public static final fun fromFile (Laws/smithy/kotlin/runtime/content/ByteStream$Companion;Ljava/io/File;)Laws/smithy/kotlin/runtime/content/ByteStream; + public static final fun fromInputStream (Laws/smithy/kotlin/runtime/content/ByteStream$Companion;Ljava/io/InputStream;Ljava/lang/Long;)Laws/smithy/kotlin/runtime/content/ByteStream$SourceStream; + public static synthetic fun fromInputStream$default (Laws/smithy/kotlin/runtime/content/ByteStream$Companion;Ljava/io/InputStream;Ljava/lang/Long;ILjava/lang/Object;)Laws/smithy/kotlin/runtime/content/ByteStream$SourceStream; public static final fun toInputStream (Laws/smithy/kotlin/runtime/content/ByteStream;)Ljava/io/InputStream; public static final fun writeToFile (Laws/smithy/kotlin/runtime/content/ByteStream;Ljava/io/File;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public static final fun writeToFile (Laws/smithy/kotlin/runtime/content/ByteStream;Ljava/nio/file/Path;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static final fun writeToOutputStream (Laws/smithy/kotlin/runtime/content/ByteStream;Ljava/io/OutputStream;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; } public final class aws/smithy/kotlin/runtime/content/ByteStreamKt { diff --git a/runtime/runtime-core/jvm/src/aws/smithy/kotlin/runtime/content/ByteStreamJVM.kt b/runtime/runtime-core/jvm/src/aws/smithy/kotlin/runtime/content/ByteStreamJVM.kt index 20fe3510e..af48850d0 100644 --- a/runtime/runtime-core/jvm/src/aws/smithy/kotlin/runtime/content/ByteStreamJVM.kt +++ b/runtime/runtime-core/jvm/src/aws/smithy/kotlin/runtime/content/ByteStreamJVM.kt @@ -11,6 +11,7 @@ import kotlinx.coroutines.withContext import java.io.ByteArrayInputStream import java.io.File import java.io.InputStream +import java.io.OutputStream import java.nio.file.Path import kotlin.io.use @@ -97,3 +98,49 @@ public fun ByteStream.toInputStream(): InputStream = when (this) { is ByteStream.ChannelStream -> readFrom().toInputStream() is ByteStream.SourceStream -> readFrom().buffer().inputStream() } + +/** + * Create a [ByteStream.SourceStream] that reads from the given [InputStream] + * @param inputStream The [InputStream] from which to create a [ByteStream] + * @param contentLength If specified, indicates how many bytes remain in the input stream. Defaults to `null`. + */ +public fun ByteStream.Companion.fromInputStream( + inputStream: InputStream, + contentLength: Long? = null, +): ByteStream.SourceStream = inputStream.asByteStream(contentLength) + +/** + * Create a [ByteStream.SourceStream] that reads from this [InputStream] + * @param contentLength If specified, indicates how many bytes remain in this stream. Defaults to `null`. + */ +public fun InputStream.asByteStream(contentLength: Long? = null): ByteStream.SourceStream { + val source = source() + return object : ByteStream.SourceStream() { + override val contentLength: Long? = contentLength + override val isOneShot: Boolean = true + override fun readFrom(): SdkSource = source + } +} + +/** + * Writes this stream to the given [OutputStream]. This method does not flush or close the given [OutputStream]. + * @param outputStream The [OutputStream] to which the contents of this stream will be written + */ +public suspend fun ByteStream.writeToOutputStream(outputStream: OutputStream): Long = withContext(Dispatchers.IO) { + val src = when (val stream = this@writeToOutputStream) { + is ByteStream.ChannelStream -> return@withContext outputStream.writeAll(stream.readFrom()) + is ByteStream.Buffer -> stream.bytes().source() + is ByteStream.SourceStream -> stream.readFrom() + } + + outputStream.sink().use { + it.buffer().use { bufferedSink -> + bufferedSink.writeAll(src) + } + } +} + +private suspend fun OutputStream.writeAll(chan: SdkByteReadChannel): Long = + sink().use { + chan.readAll(it) + } diff --git a/runtime/runtime-core/jvm/test/aws/smithy/kotlin/runtime/content/ByteStreamJVMTest.kt b/runtime/runtime-core/jvm/test/aws/smithy/kotlin/runtime/content/ByteStreamJVMTest.kt index daa3c3809..2c7e571eb 100644 --- a/runtime/runtime-core/jvm/test/aws/smithy/kotlin/runtime/content/ByteStreamJVMTest.kt +++ b/runtime/runtime-core/jvm/test/aws/smithy/kotlin/runtime/content/ByteStreamJVMTest.kt @@ -7,9 +7,12 @@ package aws.smithy.kotlin.runtime.content import aws.smithy.kotlin.runtime.testing.RandomTempFile import kotlinx.coroutines.test.runTest +import java.io.ByteArrayOutputStream import java.nio.file.Files import kotlin.test.* +private val binaryData = ByteArray(1024) { it.toByte() } + class ByteStreamJVMTest { @Test fun testFileAsByteStreamValidatesStart() = runTest { @@ -149,4 +152,38 @@ class ByteStreamJVMTest { val byteStreamFromPath = file.toPath().asByteStream() assertEquals(0, byteStreamFromPath.contentLength) } + + @Test + fun testInputStreamAsByteStream() = runTest { + binaryData.inputStream().use { inputStream -> + val byteStream = inputStream.asByteStream() + assertNull(byteStream.contentLength) + assertTrue(byteStream.isOneShot) + + val output = byteStream.toByteArray() + assertContentEquals(binaryData, output) + } + } + + @Test + fun testInputStreamAsByteStreamWithLength() = runTest { + binaryData.inputStream().use { inputStream -> + val byteStream = inputStream.asByteStream(binaryData.size.toLong()) + assertEquals(binaryData.size.toLong(), byteStream.contentLength) + assertTrue(byteStream.isOneShot) + + val output = byteStream.toByteArray() + assertContentEquals(binaryData, output) + } + } + + @Test + fun testByteStreamToOutputStream() = runTest { + val byteStream = ByteStream.fromBytes(binaryData) + ByteArrayOutputStream().use { outputStream -> + byteStream.writeToOutputStream(outputStream) + val output = outputStream.toByteArray() + assertContentEquals(binaryData, output) + } + } }