這幾天以來,我們看過了 Ktor 怎麼發送 Get,Post,也看了怎麼傳送 Form Request
今天我們來看看 Ktor 怎麼傳輸檔案,以及背後實作是什麼
官方教學用 Ktor 傳輸檔案的寫法如下
val response: HttpResponse = client.post("http://localhost:8080/upload") {
setBody(MultiPartFormDataContent(
formData {
append("description", "Ktor logo")
append("image", File("ktor_logo.png").readBytes(), Headers.build {
append(HttpHeaders.ContentType, "image/png")
append(HttpHeaders.ContentDisposition, "filename=\"ktor_logo.png\"")
})
},
boundary = "WebAppBoundary"
)
)
onUpload { bytesSentTotal, contentLength ->
println("Sent $bytesSentTotal bytes from $contentLength")
}
}
這邊的 MultiPartFormDataContent
類別的簽名如下
public class MultiPartFormDataContent(
parts: List<PartData>,
public val boundary: String = generateBoundary(),
override val contentType: ContentType = ContentType.MultiPart.FormData.withParameter("boundary", boundary)
) : OutgoingContent.WriteChannelContent()
這邊的 boundary
被設置為 "WebAppBoundary"
contentType
設置為 ContentType.MultiPart.FormData.withParameter("boundary", boundary)
,也就是 multipart/form-data
,並加上 boundary=WebAppBoundary
/**
* Provides a list of standard subtypes of a `multipart` content type.
*/
@Suppress("KDocMissingDocumentation", "unused")
public object MultiPart {
public val Any: ContentType = ContentType("multipart", "*")
public val Mixed: ContentType = ContentType("multipart", "mixed")
public val Alternative: ContentType = ContentType("multipart", "alternative")
public val Related: ContentType = ContentType("multipart", "related")
public val FormData: ContentType = ContentType("multipart", "form-data")
public val Signed: ContentType = ContentType("multipart", "signed")
public val Encrypted: ContentType = ContentType("multipart", "encrypted")
public val ByteRanges: ContentType = ContentType("multipart", "byteranges")
}
parts
由 formData()
組成,實作如下
/**
* Build multipart form using [block] function.
*/
public fun formData(block: FormBuilder.() -> Unit): List<PartData> =
formData(*FormBuilder().apply(block).build().toTypedArray())
這邊會實作出一個 FormBuilder
協助我們建立內容
也就是我們前面看到的 append
,FormBuilder.append
實作如下
/**
* Appends a pair [key]:[value] with optional [headers].
*/
public fun append(key: String, value: String, headers: Headers = Headers.Empty) {
parts += FormPart(key, value, headers)
}
FormPart
是一個 data class
/**
* A multipart form item. Use it to build a form in client.
*
* @param key multipart name
* @param value content, could be [String], [Number], [ByteArray], [ByteReadPacket] or [InputProvider]
* @param headers part headers, note that some servers may fail if an unknown header provided
*/
public data class FormPart<T : Any>(val key: String, val value: T, val headers: Headers = Headers.Empty)
在這邊我們可以看到,程式內的
append("image", File("ktor_logo.png").readBytes(), Headers.build {
append(HttpHeaders.ContentType, "image/png")
append(HttpHeaders.ContentDisposition, "filename=\"ktor_logo.png\"")
})
就會變成其中一個 FormPart
,等待轉換成請求
組合出許多 FormPart
之後,就換成下一段 formData
邏輯了
/**
* Builds a multipart form from [values].
*
* Example: [Upload a file](https://ktor.io/docs/request.html#upload_file).
*/
@Suppress("DEPRECATION")
public fun formData(vararg values: FormPart<*>): List<PartData> {
val result = mutableListOf<PartData>()
values.forEach { (key, value, headers) ->
val partHeaders = HeadersBuilder().apply {
append(HttpHeaders.ContentDisposition, "form-data; name=${key.escapeIfNeeded()}")
appendAll(headers)
}
val part = when (value) {
is String -> PartData.FormItem(value, {}, partHeaders.build())
is Number -> PartData.FormItem(value.toString(), {}, partHeaders.build())
is Boolean -> PartData.FormItem(value.toString(), {}, partHeaders.build())
is ByteArray -> {
partHeaders.append(HttpHeaders.ContentLength, value.size.toString())
PartData.BinaryItem({ ByteReadPacket(value) }, {}, partHeaders.build())
}
is ByteReadPacket -> {
partHeaders.append(HttpHeaders.ContentLength, value.remaining.toString())
PartData.BinaryItem({ value.copy() }, { value.close() }, partHeaders.build())
}
is InputProvider -> {
val size = value.size
if (size != null) {
partHeaders.append(HttpHeaders.ContentLength, size.toString())
}
PartData.BinaryItem(value.block, {}, partHeaders.build())
}
is ChannelProvider -> {
val size = value.size
if (size != null) {
partHeaders.append(HttpHeaders.ContentLength, size.toString())
}
PartData.BinaryChannelItem(value.block, partHeaders.build())
}
is Input -> error("Can't use [Input] as part of form: $value. Consider using [InputProvider] instead.")
else -> error("Unknown form content type: $value")
}
result += part
}
return result
}
根據輸入的數值,以我們範例內
File("ktor_logo.png").readBytes()
來說是 is ByteArray
最後切成 List<PartData>
接著我們可以看到,WriteChannelContent
定義了一個 writeTo
函數
在 MultiPartFormDataContent
裡面實作如下
private val BOUNDARY_BYTES = "--$boundary\r\n".toByteArray()
private val LAST_BOUNDARY_BYTES = "--$boundary--\r\n".toByteArray()
private val BODY_OVERHEAD_SIZE = LAST_BOUNDARY_BYTES.size
private val PART_OVERHEAD_SIZE = RN_BYTES.size * 2 + BOUNDARY_BYTES.size
override suspend fun writeTo(channel: ByteWriteChannel) {
try {
for (part in rawParts) {
channel.writeFully(BOUNDARY_BYTES)
channel.writeFully(part.headers)
channel.writeFully(RN_BYTES)
when (part) {
is PreparedPart.InputPart -> {
part.provider().use { input ->
input.copyTo(channel)
}
}
is PreparedPart.ChannelPart -> {
part.provider().copyTo(channel)
}
}
channel.writeFully(RN_BYTES)
}
channel.writeFully(LAST_BOUNDARY_BYTES)
} catch (cause: Throwable) {
channel.close(cause)
} finally {
channel.close()
}
}
這裡面會將 rawParts
分段送出,rawParts
實作則是從 parts
產生出來
private val rawParts: List<PreparedPart> = parts.map { part ->
val headersBuilder = BytePacketBuilder()
for ((key, values) in part.headers.entries()) {
headersBuilder.writeText("$key: ${values.joinToString("; ")}")
headersBuilder.writeFully(RN_BYTES)
}
val bodySize = part.headers[HttpHeaders.ContentLength]?.toLong()
when (part) {
is PartData.FileItem -> {
val headers = headersBuilder.build().readBytes()
val size = bodySize?.plus(PART_OVERHEAD_SIZE)?.plus(headers.size)
PreparedPart.InputPart(headers, part.provider, size)
}
is PartData.BinaryItem -> {
val headers = headersBuilder.build().readBytes()
val size = bodySize?.plus(PART_OVERHEAD_SIZE)?.plus(headers.size)
PreparedPart.InputPart(headers, part.provider, size)
}
is PartData.FormItem -> {
val bytes = buildPacket { writeText(part.value) }.readBytes()
val provider = { buildPacket { writeFully(bytes) } }
if (bodySize == null) {
headersBuilder.writeText("${HttpHeaders.ContentLength}: ${bytes.size}")
headersBuilder.writeFully(RN_BYTES)
}
val headers = headersBuilder.build().readBytes()
val size = bytes.size + PART_OVERHEAD_SIZE + headers.size
PreparedPart.InputPart(headers, provider, size.toLong())
}
is PartData.BinaryChannelItem -> {
val headers = headersBuilder.build().readBytes()
val size = bodySize?.plus(PART_OVERHEAD_SIZE)?.plus(headers.size)
PreparedPart.ChannelPart(headers, part.provider, size)
}
}
}
這邊會將前面所生成的 PartData.BinaryItem
變成一個一個的 PreparedPart.InputPart
然後透過 channel 送出去。
最後我們看到
onUpload { bytesSentTotal, contentLength ->
println("Sent $bytesSentTotal bytes from $contentLength")
}
這段會在 listener
上面設置進度,每次事件發生時印出進度
/**
* Registers listener to observe upload progress.
*/
public fun HttpRequestBuilder.onUpload(listener: ProgressListener?) {
if (listener == null) {
attributes.remove(UploadProgressListenerAttributeKey)
} else {
attributes.put(UploadProgressListenerAttributeKey, listener)
}
}
到這邊已經過了 20 天,我們看完了 Ktor 如何上傳多媒體檔案的作法!