vapor / multipart-kit

🏞 Parses and serializes multipart-encoded data with Codable support.
MIT License
140 stars 41 forks source link

How to upload file on iOS? #92

Closed pzmudzinski closed 2 months ago

pzmudzinski commented 11 months ago

I was trying to encode struct like that:

struct MyStruct {
    let imageFile: Data
}
let encoder = FormDataEncoder()
let body = try encoder.encode(options, boundary: boundary)

But imageFile is not recognized as file part. Also I could not find any example.

Also sorry for bug label - don't know how to remove it 😅

0xTim commented 11 months ago

What do you mean by

But imageFile is not recognized as file part

Is that error coming from the encode function? I tried to find a test that shows Data being encoded but I can't which isn't great...

pzmudzinski commented 11 months ago

I mean it's not encoded in such format:

Content-Disposition: attachment; filename="yourfilename"

I haven't found any way to use it to encode form with file and just gave up and spin up own solution.

0xTim commented 11 months ago

Content-Disposition is a response header when used with the attachment type, not a request header so I'm confused as to why you need it when doing encoding?

pzmudzinski commented 11 months ago

Sorry, I meant "filename" part. Basically was trying to use this library to make a request with multipart form-data body containing both regular key - values and a file within. So in the end it would look like:

Content-Disposition: form-data; name=some-key
...
some value
...
Content-Disposition: form-data; name=image_input; filename=something.png
...
file content
...

But I have not found a way to encode file using it. Does it make sense?

0xTim commented 11 months ago

Yes it does make sense. Have you tried File instead of Data? That should encode correctly and is a bug if not

pzmudzinski commented 11 months ago

I am not sure what are you referring to. There is no such thing as File In Foundation framework.

0xTim commented 11 months ago

Sorry it's a Vapor thing https://github.com/vapor/vapor/blob/main/Sources/Vapor/Utilities/File.swift

If you're trying to use this outside of Vapor you can copy and paste it and I think it will work

sidepelican commented 10 months ago

When using Vapor.File, it gets encoded as follows:

-----bound5952344343153968676
Content-Disposition: form-data; name="image[data]"

<binary data>
-----bound5952344343153968676
Content-Disposition: form-data; name="image[filename]"

myfile.jpg

However, it seems like @pzmudzinski is expecting it to be encoded like this:

Content-Disposition: form-data; name=image; filename=myfile.jpg

I also agree with this, and it appears that without this format, some runtimes may not recognize the file as such.

sidepelican commented 10 months ago

Sorry, I had missed the extension in Vapor that addresses this issue. With this extension, it would work as expected

https://github.com/vapor/vapor/blob/0680f9f6bfab7100cd585b3186740ee7860c983e/Sources/Vapor/Multipart/File%2BMultipart.swift#L4

sidepelican commented 10 months ago

@pzmudzinski Here is the simple File.swift

import Foundation
import MultipartKit
import NIOFoundationCompat

public struct File: Codable, Equatable, Sendable, MultipartPartConvertible {
    public var filename: String
    public var data: Data
    public var contentType: String?

    public init(data: Data, filename: String, contentType: String? = nil) {
        self.data = data
        self.filename = filename
        self.contentType = contentType
    }

    // MARK: - MultipartPartConvertible

    public var multipart: MultipartPart? {
        var part = MultipartPart(headers: [:], body: data)
        if let contentType {
            part.headers.replaceOrAdd(name: "Content-Type", value: contentType)
        }
        part.headers.replaceOrAdd(
            name: "Content-Disposition",
            value: !filename.contains("\"")
            ? "form-data; filename=\"\(filename)\""
            : "form-data; filename='\(filename)'"
        )
        return part
    }

    public init?(multipart: MultipartPart) {
        let filenameRegex = /filename=(?:"([^"]+)"|'([^']+)'|([^\s"';]+))/
        guard let contentDisposition = multipart.headers.first(name: "Content-Disposition"),
              let output = contentDisposition.firstMatch(of: filenameRegex)?.output,
              let filename = output.1 ?? output.2 ?? output.3
        else {
            return nil
        }
        self.init(
            data: Data(buffer: multipart.body),
            filename: String(filename),
            contentType: multipart.headers.first(name: "Content-Type")
        )
    }
}
0xTim commented 10 months ago

@pzmudzinski Does this solve your issue? File probably should be part of MultipartKit but it would be a breaking change to move it as this point I think, right @gwynne ? (We'd end up with duplicated symbols due to the re-export), so if you're not pulling in Vapor as well you'll need to copy in File

TMinYou commented 3 months ago

is iOS supported ? https://github.com/vapor/multipart-kit/issues/99

0xTim commented 2 months ago

Closing due to inactivity - I believe this is solved now. Feel free to reopen if not!