Skip to content

Commit ceacbe9

Browse files
committed
Use only files and byte arrays for object storage uploads
There are various problems with using InputStream as the default API for uploading files to object storage. One is that many object storage providers won't calculate a checksum for streaming uploads, increasing our chances for data corruption. Another is that many "S3-compatible" object storage providers don't properly support streaming uploads, causing either upload errors or worse: silent data corruption. We've actually experienced silent data corruption when using streaming uploads to Google Cloud Storage via their XML API which is supposed to be S3-compatible. Transparent headers in the PutObject request were _silently being appended_ to the object data, causing logic errors when we attempted to read and use those objects later (in our case, when publishing apps). Overall it seems to be a much better choice to use local files and memory buffers as upload sources for the best object storage provider compatibility and to prevent insidious bugs like the formerly described data corruption issue.
1 parent 26cd287 commit ceacbe9

File tree

4 files changed

+44
-17
lines changed

4 files changed

+44
-17
lines changed

console/src/main/kotlin/app/accrescent/parcelo/console/routes/Drafts.kt

+2-10
Original file line numberDiff line numberDiff line change
@@ -219,16 +219,8 @@ fun Route.createDraftRoute() {
219219
null
220220
}
221221

222-
val iconFileId = iconData.inputStream()
223-
.use {
224-
runBlocking {
225-
storageService.saveObject(it, iconData.size.toLong())
226-
}
227-
}
228-
val appFileId = tempApkSet.inputStream()
229-
.use {
230-
runBlocking { storageService.saveObject(it, tempApkSet.size()) }
231-
}
222+
val iconFileId = runBlocking { storageService.uploadBytes(iconData) }
223+
val appFileId = runBlocking { storageService.uploadFile(tempApkSet.path) }
232224
val icon = Icon.new { fileId = iconFileId }
233225
Draft.new {
234226
this.label = label

console/src/main/kotlin/app/accrescent/parcelo/console/routes/Updates.kt

+1-2
Original file line numberDiff line numberDiff line change
@@ -169,8 +169,7 @@ fun Route.createUpdateRoute() {
169169
return@post
170170
}
171171

172-
val apkSetFileId = tempApkSet.inputStream()
173-
.use { storageService.saveObject(it, tempApkSet.size()) }
172+
val apkSetFileId = storageService.uploadFile(tempApkSet.path)
174173

175174
// There exists:
176175
//

console/src/main/kotlin/app/accrescent/parcelo/console/storage/ObjectStorageService.kt

+13-3
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,27 @@ package app.accrescent.parcelo.console.storage
66

77
import org.jetbrains.exposed.dao.id.EntityID
88
import java.io.InputStream
9+
import java.nio.file.Path
910

1011
/**
1112
* Abstraction of object storage
1213
*/
1314
interface ObjectStorageService {
1415
/**
15-
* Save a file to the file storage service
16+
* Upload an object from a file to the object storage service
1617
*
17-
* @return the database ID of the new file
18+
* @param path the path of the file to upload
19+
* @return the database ID of the new object
1820
*/
19-
suspend fun saveObject(inputStream: InputStream, size: Long): EntityID<Int>
21+
suspend fun uploadFile(path: Path): EntityID<Int>
22+
23+
/**
24+
* Upload an object from memory to the object storage service
25+
*
26+
* @param bytes the object data to upload
27+
* @return the database ID of the new object
28+
*/
29+
suspend fun uploadBytes(bytes: ByteArray): EntityID<Int>
2030

2131
/**
2232
* Marks the given file deleted

console/src/main/kotlin/app/accrescent/parcelo/console/storage/S3ObjectStorageService.kt

+28-2
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import aws.sdk.kotlin.services.s3.model.DeleteObjectsRequest
1616
import aws.sdk.kotlin.services.s3.model.GetObjectRequest
1717
import aws.sdk.kotlin.services.s3.model.ObjectIdentifier
1818
import aws.sdk.kotlin.services.s3.model.PutObjectRequest
19+
import aws.smithy.kotlin.runtime.content.ByteStream
1920
import aws.smithy.kotlin.runtime.content.asByteStream
2021
import aws.smithy.kotlin.runtime.content.toInputStream
2122
import aws.smithy.kotlin.runtime.net.url.Url
@@ -27,6 +28,7 @@ import org.jetbrains.exposed.sql.transactions.transaction
2728
import java.io.FileNotFoundException
2829
import java.io.IOException
2930
import java.io.InputStream
31+
import java.nio.file.Path
3032
import java.util.UUID
3133

3234
/**
@@ -39,7 +41,7 @@ class S3ObjectStorageService(
3941
private val s3AccessKeyId: String,
4042
private val s3SecretAccessKey: String,
4143
) : ObjectStorageService {
42-
override suspend fun saveObject(inputStream: InputStream, size: Long): EntityID<Int> {
44+
override suspend fun uploadFile(path: Path): EntityID<Int> {
4345
S3Client {
4446
endpointUrl = s3EndpointUrl
4547
region = s3Region
@@ -53,7 +55,31 @@ class S3ObjectStorageService(
5355
val req = PutObjectRequest {
5456
bucket = s3Bucket
5557
key = objectKey
56-
body = inputStream.asByteStream(size)
58+
body = path.asByteStream()
59+
}
60+
s3Client.putObject(req)
61+
62+
val fileId = transaction { File.new { s3ObjectKey = objectKey }.id }
63+
64+
return fileId
65+
}
66+
}
67+
68+
override suspend fun uploadBytes(bytes: ByteArray): EntityID<Int> {
69+
S3Client {
70+
endpointUrl = s3EndpointUrl
71+
region = s3Region
72+
credentialsProvider = StaticCredentialsProvider {
73+
accessKeyId = s3AccessKeyId
74+
secretAccessKey = s3SecretAccessKey
75+
}
76+
}.use { s3Client ->
77+
val objectKey = UUID.randomUUID().toString()
78+
79+
val req = PutObjectRequest {
80+
bucket = s3Bucket
81+
key = objectKey
82+
body = ByteStream.fromBytes(bytes)
5783
}
5884
s3Client.putObject(req)
5985

0 commit comments

Comments
 (0)