MultipartKit

repository·main·Indexed 41 days ago

https://github.com/vapor/multipart-kit

A Swift library for encoding and decoding multipart/form-data. It provides Codable support via FormDataEncoder and FormDataDecoder, manual parsing with MultipartParser, and memory-efficient processing using StreamingMultipartParserAsyncSequence and MultipartWriter. Requires Swift 6.1 or later.

Tokens
1.8K
Snippets
5
Records
6
Agent score
2%

What's inside MultipartKit

  1. Stream multipart messages with StreamingMultipartParserAsyncSequence

    main

    To parse large multipart messages without loading the entire message into memory, use StreamingMultipartParserAsyncSequence. This wraps an AsyncSequence of body chunks and yields MultipartSection objects as soon as they are parsed.

    MultipartSection can be:

    • .headerFields(let fields): Emitted when headers for a part are parsed.
    • .bodyChunk(let chunk): Emitted for each chunk of the part's body.
    • .boundary(let end): Emitted when a boundary is reached (where end is true if the message is finished).
    let sequence = StreamingMultipartParserAsyncSequence(boundary: boundary, buffer: stream)
    
    for try await section in sequence {
        switch section {
        case .headerFields(let fields):
            print(fields[.contentDisposition] ?? "")
        case .bodyChunk(let chunk):
            try await file.write(contentsOf: chunk)
        case .boundary(let end):
            print(end ? "message finished" : "part finished")
        }
    }
  2. Encode and decode multipart/form-data using Codable

    main

    MultipartKit provides FormDataEncoder and FormDataDecoder to handle multipart/form-data using Swift's Codable protocol.

    Encoding

    Use FormDataEncoder().encode(_:boundary:) to convert a Codable instance into a multipart message. You can encode to a String, a specific body type (like [UInt8]), or append directly into an existing buffer.

    Decoding

    Use FormDataDecoder().decode(_:from:boundary:) to reconstruct a Codable instance from a multipart message. The decoder accepts a String or any MultipartPartBodyElement.

    Note: null values are not supported for encoding or decoding as there is no standard representation for null in Multipart.

    Nesting and Collections: Nested structures are represented using square bracket notation for paths (e.g., nested[0][tag]). Array elements always include their index to support complex nesting.

    struct User: Codable {
        let name: String
        let email: String
    }
    let user = User(name: "Ed", email: "ed@example.com")
    let boundary = "abc123"
    
    // Encoding to String
    let encoded = try FormDataEncoder().encode(user, boundary: boundary)
    
    // Decoding from String
    let decoded = try FormDataDecoder().decode(User.self, from: encoded, boundary: boundary)
    
    // Encoding to [UInt8]
    let bytes: [UInt8] = try FormDataEncoder().encode(user, boundary: boundary)
    
    // Appending to an existing buffer
    var buffer: [UInt8] = []
    try FormDataEncoder().encode(user, boundary: boundary, into: &buffer)
  3. Install MultipartKit via SPM

    main

    Add MultipartKit to your Swift Package Manager dependencies using the following steps:

    1. Add the package to your Package.swift dependencies:
    dependencies: [
        // ...
        .package(url: "https://github.com/vapor/multipart-kit.git", from: "5.0.0-alpha.5"),
    ]
    1. Add the product to your target's dependencies:
    targets: [
        .target(name: "MyAppTarget", dependencies: [
            // ...
            .product(name: "MultipartKit", package: "multipart-kit"),
        ])
    ]
    dependencies: [
        // ...
        .package(url: "https://github.com/vapor/multipart-kit.git", from: "5.0.0-alpha.5"),
    ]
    
    targets: [
        .target(name: "MyAppTarget", dependencies: [
            // ...
            .product(name: "MultipartKit", package: "multipart-kit"),
        ])
    ]
  4. Work with Multipart parts directly

    main

    If you do not require Codable support, you can manually parse messages using MultipartParser. This allows you to iterate over MultipartPart objects, accessing their headerFields and body directly.

    MultipartPart provides a ContentDisposition object which simplifies access to dispositionType, name, filename, and additionalParameters without manual header parsing.

    let parts = try MultipartParser<ArraySlice<UInt8>>(boundary: boundary).parse(message)
    
    for part in parts {
        // Access part.headerFields and part.body
        // Use part.contentDisposition to get name, filename, etc.
    }
  5. Serialize multipart data using MultipartWriter

    main

    Serialization is handled via the MultipartWriter protocol.

    MemoryMultipartWriter

    MemoryMultipartWriter is a built-in implementation that collects the entire message in memory. Use writePart(_:) to add parts and finish() to complete the message, then retrieve the result with getResult().

    Custom Writers

    To write to a destination other than memory (e.g., a file or socket), conform your type to MultipartWriter. You only need to implement write(bytes:); boundaries and headers are handled automatically.

    Streaming Serialization

    StreamingMultipartWriterAsyncSequence allows you to turn an AsyncSequence of MultipartSections into an AsyncSequence of serialized chunks, which is useful for streaming outbound data.

    // Using MemoryMultipartWriter
    var writer = MemoryMultipartWriter<[UInt8]>(boundary: boundary)
    
    try await writer.writePart(
        MultipartPart(
            headerFields: [.contentDisposition: #"form-data; name=\"file\"; filename=\"hello.txt\""#],
            body: Array("Hello, world!".utf8)
        )
    )
    try await writer.finish()
    
    let serialized = writer.getResult()
    
    // Using StreamingMultipartWriterAsyncSequence
    let sequence = StreamingMultipartWriterAsyncSequence(
        backingSequence: sections,
        boundary: boundary,
        outboundBody: ArraySlice<UInt8>.self
    )
    
    for try await chunk in sequence {
        try await socket.write(chunk)
    }