Created
January 30, 2019 23:23
-
-
Save cenkbilgen/d49428e9a4b671396287cffd8664d2cf to your computer and use it in GitHub Desktop.
"multipart/form-data" in Swift
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// URLRequestExtension.swift | |
// | |
// Created by Cenk Bilgen on 2019-01-30. | |
// Copyright © 2019 abc. All rights reserved. | |
// | |
import Foundation | |
extension URLRequest { | |
// functions to create the http body if you want to use multipart/form-data | |
// since the http connection is likely compressesd, the network inefficiency of transmitting text vs binary is moot | |
// and just base64 encoding through JSON good enough, | |
// but multipart/form-data is also straight forward if you know the spec | |
// see https://www.w3.org/Protocols/rfc1341/7_2_Multipart.html | |
///////////////// Example Usage | |
// var request = URLRequest(url: url) | |
// request.httpMethod = "POST" | |
// | |
// let boundary = "-----BOUNDARY" | |
// request.addValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type") | |
// | |
// // 1. single-part form (to send just a single image file for example) | |
// let data = try? request.formEncoded(data: data, filename: "file1.jpg", boundary: boundary) | |
// | |
// //--- OR --- | |
// | |
// // 2. multi-part form (to send an image file plus other metadata for example) | |
// let formParts: [URLRequest.FormPart] = [FormPart(data: data1, filename: nil, mimetype: "text/plain") | |
// FormPart(data: data2, filename: "file1.jpg", mimetype: "image/jpeg")] | |
// let data = try? request.formEncoded(parts: formParts, boundary: boundary) | |
// let task = URLSession.shared.uploadTask(with: request, from: data) { } | |
/////////////////////////////////// | |
struct FormPart { | |
let data: Data | |
let filename: String? // not included if nil | |
let mimetype: String? // "application/octet-stream" if nil. see https://www.w3.org/Protocols/rfc1341/4_Content-Type.html | |
} | |
// Convert an array of form structures into the proper http request body form | |
func formEncoded(parts: [FormPart], boundary: String = "-----BOUNDARY") throws -> Data { | |
var body = Data() | |
for part in parts { | |
body += try encode(formPart: part, boundary: boundary) | |
} | |
guard body.isEmpty == false else { | |
throw FormEncodingError.noParts | |
} | |
body += "\r\n--\(boundary)--\r\n".data(using: .ascii)! // if there's a bad char in boundary, would've failed earlier | |
// make this function mutating | |
// self.addValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type") | |
// self.httpBody = body | |
return body | |
} | |
// Create data from a single form part, it's not enough to send by itself | |
fileprivate func encode(formPart part: FormPart, boundary: String) throws -> Data { | |
var boundaryHead = "--\(boundary)\r\n" | |
boundaryHead += "Content-Disposition: form-data; name=\"file\"" | |
if let filename = part.filename { | |
boundaryHead += "; filename=\"\(filename)\"\r\n" | |
} | |
boundaryHead += "Content-Type: \(part.mimetype ?? "application/octet-stream")\r\n\r\n" | |
// can encode as as .utf8 if you add ,charset=UTF-8 to Content-Type | |
guard let head = boundaryHead.data(using: .ascii) else { | |
throw FormEncodingError.badboundaryValue | |
} | |
return head + part.data + "\r\n".data(using: .ascii)! | |
} | |
// This is alternative function if you just want to send a single-part form with data (like just one file) | |
// doesn't need resizing of mutable Data structure like the multi-part version does | |
func formEncoded(data: Data, filename: String, mimetype: String? = nil, boundary: String = "-----BOUNDARY") throws -> Data { | |
var boundaryHead = "" | |
boundaryHead += "--\(boundary)\r\n" | |
boundaryHead += "Content-Disposition: form-data; name=\"file\"; filename=\"\(filename)\"\r\n" | |
boundaryHead += "Content-Type: \(mimetype ?? "application/octet-stream")\r\n\r\n" | |
// boundaryHead += "Content-Transfer-Encoding: binary\n\n" // is this necessary sometimes? | |
let boundaryTail = "\r\n\r\n--\(boundary)--\r\n" | |
// put it together: head + data + tail | |
guard let head = boundaryHead.data(using: .ascii), | |
let tail = boundaryTail.data(using: .ascii) else { | |
throw FormEncodingError.badboundaryValue | |
} | |
let body = head + data + tail | |
// make this function mutating | |
// self.addValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type") | |
// self.httpBody = body | |
return body | |
} | |
} | |
// MARK: Errors | |
enum FormEncodingError: Error { | |
case badboundaryValue | |
case noParts | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment