0
# Multipart Data
1
2
Multipart form data processing with support for file uploads, form fields, and streaming multipart content with JVM-specific stream providers.
3
4
## Capabilities
5
6
### MultiPartData Interface
7
8
Interface for reading multipart data streams.
9
10
```kotlin { .api }
11
/**
12
* Interface for reading multipart data
13
*/
14
interface MultiPartData {
15
/**
16
* Read next part from multipart stream
17
* @return PartData instance or null if no more parts
18
*/
19
suspend fun readPart(): PartData?
20
21
object Empty : MultiPartData {
22
override suspend fun readPart(): PartData? = null
23
}
24
}
25
```
26
27
### PartData Base Class
28
29
Base class for individual parts in multipart data.
30
31
```kotlin { .api }
32
/**
33
* Base class for multipart data parts
34
*/
35
abstract class PartData {
36
/**
37
* Part headers
38
*/
39
abstract val headers: Headers
40
41
/**
42
* Function to dispose/cleanup part resources
43
*/
44
abstract val dispose: () -> Unit
45
46
/**
47
* Content-Disposition header parsed
48
*/
49
val contentDisposition: ContentDisposition?
50
51
/**
52
* Content-Type header parsed
53
*/
54
val contentType: ContentType?
55
56
/**
57
* Part name from Content-Disposition
58
*/
59
val name: String?
60
}
61
```
62
63
### Form Item Part
64
65
Text form field part data.
66
67
```kotlin { .api }
68
/**
69
* Form field part with text value
70
*/
71
class PartData.FormItem(
72
val value: String,
73
dispose: () -> Unit,
74
partHeaders: Headers
75
) : PartData {
76
77
override val headers: Headers = partHeaders
78
override val dispose: () -> Unit = dispose
79
}
80
```
81
82
### Binary Item Part
83
84
Binary data part with input stream provider.
85
86
```kotlin { .api }
87
/**
88
* Binary data part with input stream
89
*/
90
class PartData.BinaryItem(
91
val provider: () -> InputStream,
92
dispose: () -> Unit,
93
partHeaders: Headers
94
) : PartData {
95
96
override val headers: Headers = partHeaders
97
override val dispose: () -> Unit = dispose
98
}
99
```
100
101
### File Item Part
102
103
File upload part with original filename and JVM-specific stream provider.
104
105
```kotlin { .api }
106
/**
107
* File upload part with filename
108
*/
109
class PartData.FileItem(
110
val provider: () -> InputStream,
111
dispose: () -> Unit,
112
partHeaders: Headers
113
) : PartData {
114
115
override val headers: Headers = partHeaders
116
override val dispose: () -> Unit = dispose
117
118
/**
119
* Original filename from Content-Disposition
120
*/
121
val originalFileName: String?
122
123
/**
124
* JVM-specific stream provider
125
*/
126
val streamProvider: () -> InputStream
127
}
128
```
129
130
### Binary Channel Item Part
131
132
Binary data part with channel-based access.
133
134
```kotlin { .api }
135
/**
136
* Binary data part with channel access
137
*/
138
class PartData.BinaryChannelItem(
139
val provider: () -> ByteReadChannel,
140
dispose: () -> Unit,
141
partHeaders: Headers
142
) : PartData {
143
144
override val headers: Headers = partHeaders
145
override val dispose: () -> Unit = dispose
146
}
147
```
148
149
### Content Disposition
150
151
Content-Disposition header parsing and representation.
152
153
```kotlin { .api }
154
/**
155
* Content-Disposition header representation
156
*/
157
class ContentDisposition private constructor(
158
val disposition: String,
159
parameters: List<HeaderValueParam> = emptyList()
160
) : HeaderValueWithParameters {
161
162
/**
163
* Disposition name parameter
164
*/
165
val name: String?
166
167
/**
168
* Add parameter to Content-Disposition
169
* @param name parameter name
170
* @param value parameter value
171
* @param escapeValue whether to escape the value
172
* @return new ContentDisposition with added parameter
173
*/
174
fun withParameter(name: String, value: String, escapeValue: Boolean = false): ContentDisposition
175
176
/**
177
* Replace parameters
178
* @param parameters new parameter list
179
* @return new ContentDisposition with replaced parameters
180
*/
181
fun withParameters(parameters: List<HeaderValueParam>): ContentDisposition
182
183
companion object {
184
val Inline: ContentDisposition
185
val Attachment: ContentDisposition
186
val File: ContentDisposition
187
val Mixed: ContentDisposition
188
189
/**
190
* Parse Content-Disposition header
191
* @param value header value
192
* @return ContentDisposition instance
193
*/
194
fun parse(value: String): ContentDisposition
195
}
196
197
object Parameters {
198
const val Name: String = "name"
199
const val FileName: String = "filename"
200
const val FileNameAsterisk: String = "filename*"
201
const val CreationDate: String = "creation-date"
202
const val ModificationDate: String = "modification-date"
203
const val ReadDate: String = "read-date"
204
const val Size: String = "size"
205
const val Handling: String = "handling"
206
}
207
}
208
```
209
210
### Multipart Processing Utilities
211
212
Utility functions for processing multipart data streams.
213
214
```kotlin { .api }
215
/**
216
* Convert MultiPartData to Flow
217
* @return Flow of PartData instances
218
*/
219
fun MultiPartData.asFlow(): Flow<PartData>
220
221
/**
222
* Process each part with a handler function
223
* @param handler function to process each part
224
*/
225
suspend fun MultiPartData.forEachPart(handler: suspend (PartData) -> Unit)
226
227
/**
228
* Read all parts into a list
229
* @return List of all PartData instances
230
*/
231
suspend fun MultiPartData.readAllParts(): List<PartData>
232
```
233
234
**Usage Examples:**
235
236
```kotlin
237
import io.ktor.http.*
238
import io.ktor.http.content.*
239
import kotlinx.coroutines.flow.*
240
import java.io.*
241
242
// Processing multipart data
243
suspend fun processMultipartData(multipart: MultiPartData) {
244
multipart.forEachPart { part ->
245
when (part) {
246
is PartData.FormItem -> {
247
println("Form field '${part.name}': ${part.value}")
248
}
249
250
is PartData.FileItem -> {
251
println("File upload '${part.name}': ${part.originalFileName}")
252
println("Content-Type: ${part.contentType}")
253
254
// JVM-specific: Access file content via InputStream
255
part.streamProvider().use { inputStream ->
256
val bytes = inputStream.readBytes()
257
println("File size: ${bytes.size} bytes")
258
259
// Process file content
260
saveUploadedFile(part.originalFileName ?: "unknown", bytes)
261
}
262
}
263
264
is PartData.BinaryItem -> {
265
println("Binary data '${part.name}'")
266
part.provider().use { inputStream ->
267
// Process binary data
268
val content = inputStream.readBytes()
269
processBinaryContent(content)
270
}
271
}
272
273
is PartData.BinaryChannelItem -> {
274
println("Binary channel data '${part.name}'")
275
val channel = part.provider()
276
// Read from channel
277
val content = channel.readRemaining().readBytes()
278
processBinaryContent(content)
279
}
280
}
281
282
// Always dispose part resources
283
part.dispose()
284
}
285
}
286
287
// Using Flow API for streaming processing
288
suspend fun processMultipartStream(multipart: MultiPartData) {
289
multipart.asFlow()
290
.collect { part ->
291
when (part) {
292
is PartData.FileItem -> {
293
// Stream process large files
294
part.streamProvider().use { stream ->
295
val buffered = stream.buffered()
296
val buffer = ByteArray(8192)
297
var totalBytes = 0L
298
299
while (true) {
300
val bytesRead = buffered.read(buffer)
301
if (bytesRead == -1) break
302
303
totalBytes += bytesRead
304
// Process chunk
305
processFileChunk(buffer, bytesRead)
306
}
307
308
println("Processed $totalBytes bytes from ${part.originalFileName}")
309
}
310
}
311
else -> {
312
// Handle other part types
313
}
314
}
315
part.dispose()
316
}
317
}
318
319
// Reading all parts at once (for smaller datasets)
320
suspend fun getAllParts(multipart: MultiPartData): List<PartData> {
321
return multipart.readAllParts()
322
}
323
324
// Working with Content-Disposition
325
fun parseContentDisposition() {
326
val dispositionValue = "form-data; name=\"file\"; filename=\"document.pdf\""
327
val disposition = ContentDisposition.parse(dispositionValue)
328
329
println("Disposition: ${disposition.disposition}") // form-data
330
println("Name: ${disposition.name}") // file
331
println("Parameter: ${disposition.parameter(ContentDisposition.Parameters.FileName)}") // document.pdf
332
333
// Create Content-Disposition
334
val fileDisposition = ContentDisposition.Attachment
335
.withParameter(ContentDisposition.Parameters.FileName, "report.xlsx")
336
.withParameter(ContentDisposition.Parameters.Size, "1024")
337
338
println(fileDisposition.toString()) // attachment; filename="report.xlsx"; size="1024"
339
}
340
341
// Form data validation and processing
342
suspend fun validateAndProcessForm(multipart: MultiPartData): Map<String, Any> {
343
val formData = mutableMapOf<String, Any>()
344
val uploadedFiles = mutableListOf<UploadedFile>()
345
346
multipart.forEachPart { part ->
347
when (part) {
348
is PartData.FormItem -> {
349
val fieldName = part.name ?: "unknown"
350
351
// Validate form fields
352
when (fieldName) {
353
"email" -> {
354
if (part.value.contains("@")) {
355
formData[fieldName] = part.value
356
} else {
357
throw IllegalArgumentException("Invalid email format")
358
}
359
}
360
"age" -> {
361
val age = part.value.toIntOrNull()
362
if (age != null && age in 1..120) {
363
formData[fieldName] = age
364
} else {
365
throw IllegalArgumentException("Invalid age")
366
}
367
}
368
else -> {
369
formData[fieldName] = part.value
370
}
371
}
372
}
373
374
is PartData.FileItem -> {
375
val fileName = part.originalFileName
376
val contentType = part.contentType
377
378
// Validate file upload
379
if (fileName.isNullOrBlank()) {
380
throw IllegalArgumentException("Filename is required")
381
}
382
383
if (contentType?.match(ContentType.Image.Any) != true) {
384
throw IllegalArgumentException("Only image files allowed")
385
}
386
387
// Save file
388
val tempFile = createTempFile(prefix = "upload_", suffix = ".tmp")
389
part.streamProvider().use { input ->
390
tempFile.outputStream().use { output ->
391
input.copyTo(output)
392
}
393
}
394
395
uploadedFiles.add(UploadedFile(fileName, tempFile, contentType))
396
}
397
}
398
part.dispose()
399
}
400
401
formData["uploadedFiles"] = uploadedFiles
402
return formData
403
}
404
405
// Data classes for structured processing
406
data class UploadedFile(
407
val originalName: String,
408
val tempFile: File,
409
val contentType: ContentType?
410
)
411
412
// Helper functions
413
fun saveUploadedFile(filename: String, content: ByteArray) {
414
val file = File("uploads", filename)
415
file.parentFile.mkdirs()
416
file.writeBytes(content)
417
}
418
419
fun processBinaryContent(content: ByteArray) {
420
println("Processing ${content.size} bytes of binary data")
421
// Process binary content
422
}
423
424
fun processFileChunk(buffer: ByteArray, size: Int) {
425
// Process file chunk
426
println("Processing chunk of $size bytes")
427
}
428
429
// Error handling in multipart processing
430
suspend fun safeProcessMultipart(multipart: MultiPartData) {
431
try {
432
var partCount = 0
433
multipart.forEachPart { part ->
434
partCount++
435
436
try {
437
when (part) {
438
is PartData.FileItem -> {
439
if (part.originalFileName?.endsWith(".exe") == true) {
440
throw SecurityException("Executable files not allowed")
441
}
442
// Process file safely
443
}
444
is PartData.FormItem -> {
445
if (part.value.length > 10_000) {
446
throw IllegalArgumentException("Form field too large")
447
}
448
// Process form field
449
}
450
}
451
} catch (e: Exception) {
452
println("Error processing part $partCount: ${e.message}")
453
// Continue with next part
454
} finally {
455
// Always dispose resources
456
part.dispose()
457
}
458
}
459
} catch (e: Exception) {
460
println("Error reading multipart data: ${e.message}")
461
}
462
}
463
464
// Async processing with limits
465
suspend fun processMultipartWithLimits(
466
multipart: MultiPartData,
467
maxParts: Int = 100,
468
maxPartSize: Long = 10 * 1024 * 1024 // 10MB
469
) {
470
var partCount = 0
471
472
multipart.asFlow()
473
.take(maxParts)
474
.collect { part ->
475
partCount++
476
477
when (part) {
478
is PartData.FileItem -> {
479
part.streamProvider().use { stream ->
480
val limitedStream = stream.buffered()
481
var totalRead = 0L
482
val buffer = ByteArray(8192)
483
484
while (totalRead < maxPartSize) {
485
val bytesRead = limitedStream.read(buffer)
486
if (bytesRead == -1) break
487
488
totalRead += bytesRead
489
490
if (totalRead > maxPartSize) {
491
throw IllegalArgumentException("Part exceeds size limit")
492
}
493
494
// Process chunk
495
}
496
}
497
}
498
}
499
500
part.dispose()
501
}
502
}
503
```
504
505
## Types
506
507
All types are defined above in their respective capability sections.