Skip to content

Commit b94f047

Browse files
authored
feat: add ByteStream support for partial file content (#640)
1 parent b0e3ae2 commit b94f047

File tree

5 files changed

+162
-5
lines changed

5 files changed

+162
-5
lines changed
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"id": "53fe3bd1-84b5-47b2-8d75-460ab8437dcc",
3+
"type": "feature",
4+
"description": "Add partial-file ByteStream support.",
5+
"issues": [
6+
"awslabs/aws-sdk-kotlin#530"
7+
]
8+
}

runtime/runtime-core/build.gradle.kts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ kotlin {
3434

3535
val kamlVersion: String by project
3636
implementation("com.charleskorn.kaml:kaml:$kamlVersion")
37+
38+
implementation(project(":runtime:testing"))
3739
}
3840
}
3941

runtime/runtime-core/jvm/src/aws/smithy/kotlin/runtime/content/ByteStreamJVM.kt

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,18 +21,38 @@ fun ByteStream.Companion.fromFile(file: File): ByteStream = file.asByteStream()
2121
/**
2222
* Create a [ByteStream] from a file
2323
*/
24-
fun File.asByteStream(): ByteStream = FileContent(this)
24+
fun File.asByteStream(start: Long = 0, endInclusive: Long = length() - 1): ByteStream {
25+
require(start >= 0) { "start index $start cannot be negative" }
26+
require(endInclusive == -1L || endInclusive >= start) {
27+
"end index $endInclusive must be greater than or equal to start index $start"
28+
}
29+
30+
val len = length()
31+
require(endInclusive < len) { "end index $endInclusive must be less than file size $len" }
32+
33+
return FileContent(this, start, endInclusive)
34+
}
35+
36+
/**
37+
* Create a [ByteStream] from a file with the given range
38+
*/
39+
fun File.asByteStream(range: LongRange) = asByteStream(range.first, range.last)
2540

2641
/**
2742
* Create a [ByteStream] from a path
2843
*/
29-
fun Path.asByteStream(): ByteStream {
44+
fun Path.asByteStream(start: Long = 0, endInclusive: Long = -1): ByteStream {
3045
val f = toFile()
3146
require(f.exists()) { "cannot create ByteStream, file does not exist: $this" }
3247
require(f.isFile) { "cannot create a ByteStream from a directory: $this" }
33-
return f.asByteStream()
48+
return f.asByteStream(start, endInclusive)
3449
}
3550

51+
/**
52+
* Create a [ByteStream] from a path with the given range
53+
*/
54+
fun Path.asByteStream(range: LongRange) = asByteStream(range.first, range.last)
55+
3656
/**
3757
* Write the contents of this ByteStream to file and close it
3858
* @return the number of bytes written

runtime/runtime-core/jvm/src/aws/smithy/kotlin/runtime/content/FileContent.kt

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,12 @@ import java.io.File
1414
*/
1515
public class FileContent(
1616
public val file: File,
17+
public val start: Long = 0,
18+
public val endInclusive: Long = file.length() - 1
1719
) : ByteStream.ReplayableStream() {
1820

1921
override val contentLength: Long
20-
get() = file.length()
22+
get() = endInclusive - start + 1
2123

22-
override fun newReader(): SdkByteReadChannel = file.readChannel()
24+
override fun newReader(): SdkByteReadChannel = file.readChannel(start, endInclusive)
2325
}
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0.
4+
*/
5+
6+
package aws.smithy.kotlin.runtime.content
7+
8+
import aws.smithy.kotlin.runtime.testing.RandomTempFile
9+
import kotlinx.coroutines.ExperimentalCoroutinesApi
10+
import kotlinx.coroutines.test.runTest
11+
import kotlin.test.*
12+
13+
@OptIn(ExperimentalCoroutinesApi::class)
14+
class ByteStreamJVMTest {
15+
@Test
16+
fun `file as byte stream validates start`() = runTest {
17+
val file = RandomTempFile(1024)
18+
val e = assertFailsWith<Throwable> {
19+
file.asByteStream(-1)
20+
}
21+
assertEquals("start index -1 cannot be negative", e.message)
22+
}
23+
24+
@Test
25+
fun `file as byte stream validates end`() = runTest {
26+
val file = RandomTempFile(1024)
27+
val e = assertFailsWith<Throwable> {
28+
file.asByteStream(endInclusive = 1024)
29+
}
30+
assertEquals("end index 1024 must be less than file size 1024", e.message)
31+
}
32+
33+
@Test
34+
fun `file as byte stream validates start and end`() = runTest {
35+
val file = RandomTempFile(1024)
36+
val e = assertFailsWith<Throwable> {
37+
file.asByteStream(5, 1)
38+
}
39+
assertEquals("end index 1 must be greater than or equal to start index 5", e.message)
40+
}
41+
42+
@Test
43+
fun `file as byte stream has contentLength`() = runTest {
44+
val file = RandomTempFile(1024)
45+
val stream = file.asByteStream()
46+
47+
assertEquals(1024, stream.contentLength)
48+
}
49+
50+
@Test
51+
fun `partial file as byte stream has contentLength`() = runTest {
52+
val file = RandomTempFile(1024)
53+
val stream = file.asByteStream(1, 1023)
54+
55+
assertEquals(1023, stream.contentLength)
56+
}
57+
58+
@Test
59+
fun `partial file as byte stream has contentLength with implicit end`() = runTest {
60+
val file = RandomTempFile(1024)
61+
val stream = file.asByteStream(1)
62+
63+
assertEquals(1023, stream.contentLength)
64+
}
65+
66+
@Test
67+
fun `file as byte stream matches read`() = runTest {
68+
val file = RandomTempFile(1024)
69+
70+
val expected = file.readBytes()
71+
val actual = file.asByteStream().toByteArray()
72+
73+
assertContentEquals(expected, actual)
74+
}
75+
76+
@Test
77+
fun `partial file as byte stream matches read`() = runTest {
78+
val file = RandomTempFile(1024)
79+
80+
val expected = file.readBytes()
81+
val part0 = file.asByteStream(endInclusive = 255).toByteArray()
82+
val part1 = file.asByteStream(256, 511).toByteArray()
83+
val part2 = file.asByteStream(512).toByteArray()
84+
85+
assertContentEquals(expected, part0 + part1 + part2)
86+
}
87+
88+
@Test
89+
fun `partial file as byte stream using range`() = runTest {
90+
val file = RandomTempFile(1024)
91+
92+
val expected = file.readBytes()
93+
val part0 = file.asByteStream(0L..255L).toByteArray()
94+
val part1 = file.asByteStream(256L..511L).toByteArray()
95+
val part2 = file.asByteStream(512L until file.length()).toByteArray()
96+
97+
assertContentEquals(expected, part0 + part1 + part2)
98+
}
99+
100+
@Test
101+
fun `partial path as byte stream`() = runTest {
102+
val file = RandomTempFile(1024)
103+
val path = file.toPath()
104+
105+
val expected = file.readBytes()
106+
val part0 = path.asByteStream(endInclusive = 255).toByteArray()
107+
val part1 = path.asByteStream(256, 511).toByteArray()
108+
val part2 = path.asByteStream(512).toByteArray()
109+
110+
assertContentEquals(expected, part0 + part1 + part2)
111+
}
112+
113+
@Test
114+
fun `partial path as byte stream using range`() = runTest {
115+
val file = RandomTempFile(1024)
116+
val path = file.toPath()
117+
118+
val expected = file.readBytes()
119+
val part0 = path.asByteStream(0L..255L).toByteArray()
120+
val part1 = path.asByteStream(256L..511L).toByteArray()
121+
val part2 = path.asByteStream(512L until file.length()).toByteArray()
122+
123+
assertContentEquals(expected, part0 + part1 + part2)
124+
}
125+
}

0 commit comments

Comments
 (0)