1. Overview
In this tutorial, we’re going to get familiar with a few ways to copy the contents of an InputStream to a file in Kotlin.
Surely, there is an overlap between Java and Kotlin in this regard in terms of standard and third-party libraries. However, we’ll only cover the most idiomatic approaches in the standard libraries here, as they’re pretty mature nowadays.
2. The (Usually) Wrong Way
One simple solution is to first read all the bytes from the InputStream and then write it to the file:
val file: File = // from somewhere
val bytes = inputStream.readBytes()
file.writeBytes(bytes)
We might get tempted or consulted to use this approach. However, with this approach, we can easily put ourselves in big trouble, especially when the input size is unknown or large. This is because of the fact that we’re loading the whole content into memory before starting to write it to a file. Basically, we’re acting like the memory is unlimited, when it’s clearly not.
Additionally, other approaches are usually using an internal buffer to read from an InputStream. Therefore, this approach is not that superior even for small-size streams.
So we’re better off avoiding this temptation and let’s get familiar with better ways.
3. The copyTo() Extension Function
The first approach is to use the copyTo(output) extension function on InputStream in Kotlin. This function copies everything from the receiving InputStream to the given OutputStream:
val content = "Hello World".repeat(1000)
val file: File = createTempFile()
val inputStream = ByteArrayInputStream(content.toByteArray())
inputStream.use { input ->
file.outputStream().use { output ->
input.copyTo(output)
}
}
assertThat(file).hasContent(content)
In the above example, first, we’re creating a temporary file and then copying everything from an InputStream to that file. In the end, we’re also verifying that the content is being copied as expected. We can also perform the same operation on any File instance, such as the ones obtained from a path:
val file = File("path/to/file")
Please note that, the File.outputStream() extension function converts a File instance to an OutputStream. This is necessary as the copyTo() function expects an instance of OutputStream as the destination.
Obviously, the most important part in the above example is input.copyTo(output) which handles the heavy-lifting of copying.
Moreover, we should act responsibly and close the underlying resources when we’re done with them. Therefore, we’re using the use() function pretty extensively to automatically manage those resources. This technique is also known as try-with-resources in the JVM community.
3.1. Buffering
Under the hood, by default, the copyTo() function buffers 8 kilobytes each time it reads from the InputStream. This is a pretty common trick to improve the performance of copying, especially when it’s involving the kernel (e.g. reading from an open file or socket).
Quite interestingly, this extension function makes it possible to customize the buffer size through a second argument:
input.copyTo(output, 16 * 1024)
As shown above, we’re buffering 16 kilobytes, instead of the default 8.
4. The Files.copy()
In addition to the Kotlin extension function, we can use the Files.copy(stream, path) utility to copy from an InputStream to the given Path instance:
inputStream.use { input ->
Files.copy(input, Paths.get("./copied"))
}
assertThat(File("./copied")).hasContent(content)
Here, we’re copying the stream content to a file named copied in the current directory. Similarly, if we already have a File instance, we can use the toPath() method on it, to still be able to use the Files.copy() method:
val file: File = // from somewhere
Files.copy(input, file.toPath())
Similarly, the Files.copy() method is also using the same buffering technique as the previous approach.
5. The transferTo() in Java 9+
Java 9 introduced a new method called InputStream.transferTo(output) to transfer the content of an InputStream to an OutputStream:
val file = createTempFile()
val inputStream = ByteArrayInputStream(content.toByteArray())
inputStream.use { input ->
file.outputStream().use { output ->
input.transferTo(output)
}
}
assertThat(file).hasContent(content)
Since this method needs an OutputStream for the destination file, we’re using the same File.outputStream() extension function again. Quite interestingly, the Files.copy() method is using this method under the hood.
6. Conclusion
In this tutorial, we learned a few approaches to copy the contents of an InputStream to a file in Kotlin. Even though they were different at the API level, they had pretty similar implementation details, as all of them were using the same buffering approach.
As usual, all the examples are available over on GitHub.