multipart-kit icon indicating copy to clipboard operation
multipart-kit copied to clipboard

How to upload file on iOS?

Open pzmudzinski opened this issue 1 year ago • 11 comments

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 😅

pzmudzinski avatar Dec 27 '23 19:12 pzmudzinski

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...

0xTim avatar Jan 02 '24 16:01 0xTim

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.

pzmudzinski avatar Jan 03 '24 10:01 pzmudzinski

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?

0xTim avatar Jan 03 '24 10:01 0xTim

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?

pzmudzinski avatar Jan 03 '24 10:01 pzmudzinski

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

0xTim avatar Jan 06 '24 01:01 0xTim

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

pzmudzinski avatar Jan 07 '24 13:01 pzmudzinski

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

0xTim avatar Jan 07 '24 16:01 0xTim

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 avatar Jan 14 '24 09:01 sidepelican

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 avatar Jan 14 '24 14:01 sidepelican

@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")
        )
    }
}

sidepelican avatar Jan 15 '24 00:01 sidepelican

@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

0xTim avatar Jan 15 '24 12:01 0xTim