Как загрузить файл изображения, используя Codable и URLSession.shared.uploadTask (multipart / form-data) в Swift? - PullRequest
0 голосов
/ 04 октября 2019

Я хочу загрузить файл изображения на внутренний сервер, используя определенную конечную точку URL. Я легко могу сделать это, используя запрос на загрузку Alamofire как multipartFormData. Однако я хочу избавиться от Alamofire, чтобы минимизировать зависимость от сторонних фреймворков. Вот код Alamofire, который работает:

func uploadRequestAlamofire(parameters: [String: Any], imageData: Data?, completion: @escaping(CustomError?) -> Void ) {

let url = imageUploadEndpoint!

let headers: HTTPHeaders = ["X-User-Agent": "ios",
                            "Accept-Language": "en",
                            "Accept": "application/json",
                            "Content-type": "multipart/form-data",
                            "ApiKey": KeychainService.getString(by: KeychainKey.apiKey) ?? ""]

Alamofire.upload(multipartFormData: { (multipartFormData) in
    for (key, value) in parameters {
        multipartFormData.append("\(value)".data(using: String.Encoding.utf8)!, withName: key as String)
    }

    if let data = imageData {
        multipartFormData.append(data, withName: "file", fileName: "image.png", mimeType: "image/jpg")
    }

}, usingThreshold: UInt64.init(), to: url, method: .post, headers: headers) { (result) in
    switch result {
    case .success(let upload, _, _):
        upload.responseJSON { response in

            completion(CustomError(errorCode: response.response!.statusCode))

            print("Succesfully uploaded")
        }
    case .failure(let error):
        print("Error in upload: \(error.localizedDescription)")

    }
}
}

Вот задача загрузки URLSession, которая не работает:

func requestNativeImageUpload(imageData: Data, orderExtId: String) {

var request = URLRequest(url: imageUploadEndpoint!)
request.httpMethod = "POST"
request.timeoutInterval = 10

    request.allHTTPHeaderFields = [
        "X-User-Agent": "ios",
        "Accept-Language": "en",
        "Accept": "application/json",
        "Content-type": "multipart/form-data",
        "ApiKey": KeychainService.getString(by: KeychainKey.apiKey) ?? ""
    ]

let body = OrderUpload(order_ext_id: orderExtId, file: imageData)

do {
    request.httpBody = try encoder.encode(body)
} catch let error {
    print(error.localizedDescription)
}

let session = URLSession.shared



session.uploadTask(with: request, from: imageData)  { data, response, error in
    guard let response = response as? HTTPURLResponse else { return }

    print(response)
    if error != nil {
        print(error!.localizedDescription)
    }


    }.resume()
}

Это способ, которым я вызываю методы для обоихAlamofire и URLSession:

uploadRequestAlamofire(parameters: ["order_ext_id": order_ext_id, "file": "image.jpg"], imageData: uploadImage) { [weak self] response in } 

requestNativeImageUpload(imageData: uploadImage!, orderExtId: order_ext_id)

Вот что внутренний сервер ожидает получить в теле запроса:

let order_ext_id: String
let description: String
let file: string($binary)

Это структура Codable для кодирования для httpBody запроса.

struct OrderUpload: Codable {
    let order_ext_id: String
    let description: String 
    let file: String
}

Хотя в этой демонстрации мои методы могут быть не совсем подходящими, и я не обрабатываю код состояния ответа, метод Alamofire работает хорошо.

Почему не должен работать URLSession?

Ответы [ 2 ]

0 голосов
/ 05 октября 2019

Наконец-то я смог найти решение. Источник: URLSession: многочастные запросы данных формы |Swift 3, Xcode 8 . В моем конкретном случае мне нужно предоставить orderExtId в качестве параметра для внутреннего сервера, чтобы принять мое изображение. Ваш случай может отличаться, в зависимости от требований бэкэнда.

func requestNativeImageUpload(image: UIImage, orderExtId: String) {

    guard let url = imageUploadEndpoint else { return }
    let boundary = generateBoundary()
    var request = URLRequest(url: url)

    let parameters = ["order_ext_id": orderExtId]

    guard let mediaImage = Media(withImage: image, forKey: "file") else { return }

    request.httpMethod = "POST"

    request.allHTTPHeaderFields = [
                "X-User-Agent": "ios",
                "Accept-Language": "en",
                "Accept": "application/json",
                "Content-Type": "multipart/form-data; boundary=\(boundary)",
                "ApiKey": KeychainService.getString(by: KeychainKey.apiKey) ?? ""
            ]

    let dataBody = createDataBody(withParameters: parameters, media: [mediaImage], boundary: boundary)
    request.httpBody = dataBody

    let session = URLSession.shared
    session.dataTask(with: request) { (data, response, error) in
        if let response = response {
            print(response)
        }

        if let data = data {
            do {
                let json = try JSONSerialization.jsonObject(with: data, options: [])
                print(json)
            } catch {
                print(error)
            }
        }
        }.resume()
}


func generateBoundary() -> String {
    return "Boundary-\(NSUUID().uuidString)"
}

func createDataBody(withParameters params: [String: String]?, media: [Media]?, boundary: String) -> Data {

    let lineBreak = "\r\n"
    var body = Data()

    if let parameters = params {
        for (key, value) in parameters {
            body.append("--\(boundary + lineBreak)")
            body.append("Content-Disposition: form-data; name=\"\(key)\"\(lineBreak + lineBreak)")
            body.append("\(value + lineBreak)")
        }
    }

    if let media = media {
        for photo in media {
            body.append("--\(boundary + lineBreak)")
            body.append("Content-Disposition: form-data; name=\"\(photo.key)\"; filename=\"\(photo.fileName)\"\(lineBreak)")
            body.append("Content-Type: \(photo.mimeType + lineBreak + lineBreak)")
            body.append(photo.data)
            body.append(lineBreak)
        }
    }

    body.append("--\(boundary)--\(lineBreak)")

    return body
}


extension Data {
    mutating func append(_ string: String) {
        if let data = string.data(using: .utf8) {
            append(data)
        }
    }
}


struct Media {
    let key: String
    let fileName: String
    let data: Data
    let mimeType: String

    init?(withImage image: UIImage, forKey key: String) {
        self.key = key
        self.mimeType = "image/jpg"
        self.fileName = "\(arc4random()).jpeg"

        guard let data = image.jpegData(compressionQuality: 0.5) else { return nil }
        self.data = data
    }
}
0 голосов
/ 04 октября 2019
  1. Тип содержимого в вашем заголовке неправильный. Это должно выглядеть так:
var request = URLRequest(url: imageUploadEndpoint!)

let boundary = "Boundary-\(UUID().uuidString)"
request.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type")
Вам необходимо сформировать тело на основе каждого поля в вашем объекте, точно так же, как вы добавляете значения к multipartFormData в своем примере Alamofire (вы использовали там словарь)
let body = NSMutableData()

let boundaryPrefix = "--\(boundary)\r\n"

for (key, value) in parameters {
    body.appendString(boundaryPrefix)
    body.appendString("Content-Disposition: form-data; name=\"\(key)\"\r\n\r\n")
    body.appendString("\(value)\r\n")
}

body.appendString(boundaryPrefix)
body.appendString("Content-Disposition: form-data; name=\"file\"; filename=\"\(filename)\"\r\n")
body.appendString("Content-Type: \(mimeType)\r\n\r\n")
body.append(imageData)
body.appendString("\r\n")
body.appendString("--".appending(boundary.appending("--")))

Помощник для добавленияСтроки для ваших данных:

extension NSMutableData {
    func appendString(_ string: String) {
        let data = string.data(using: .utf8)
        append(data!)
    }
}
...