3

I need to upload an mp4 video file from iPhone/iPad to a server, also in the background, so I read that is possible with URLSession.uploadTask(with: URLRequest, fromFile: URL) method, but I don't understand how do I prepare the request before.I need to create a multipart/form-data request because I want to append other string parameters.

func requestBodyFor(video: URL) -> Data? {
    let url = URL(string: "url_of_upload_handler.php")!

    let parameters = ["type":"video", "user":"112"]

    do {

        let kBoundary = "Boundary-\(UUID().uuidString)"
        let kStartTag = "--%@\r\n"
        let kEndTag = "\r\n"
        let kContent = "Content-Disposition: form-data; name=\"%@\"\r\n\r\n"

        var body = Data()

        let videoData = try Data(contentsOf: video)

        // parameters
        for (key,value) in parameters {
            body.append(String(format: kStartTag, kBoundary).data(using: String.Encoding.utf8)!)
            body.append(String(format: kContent, key).data(using: String.Encoding.utf8)!)
            body.append(value.data(using: String.Encoding.utf8)!)
            body.append(String(format: kEndTag).data(using: String.Encoding.utf8)!)
        }

        //Video data
        body.append(String(format: kStartTag, boundary).data(using: String.Encoding.utf8)!)
        body.append(String(format: "Content-Disposition: form-data; name=\"%@\"; filename=\"%@\"\r\n", "file", video.lastPathComponent).data(using: String.Encoding.utf8)!)
        body.append("Content-Type: video/mp4\r\n\r\n".data(using: String.Encoding.utf8)!)
        body.append(videoData)
        body.append(String(format: kEndTag).data(using: String.Encoding.utf8)!)

        // close form
        body.append("--\(boundary)--\r\n".data(using: String.Encoding.utf8)!)

       return body
    } catch let error {
        print(error)
        return nil
    }
}


if let body = requestBodyFor(video: fileUrl) {
        let contentType = "multipart/form-data; boundary=\(kBoundary)"

        var request = URLRequest(url: url)
        request.httpMethod = "POST"
        request.setValue(contentType, forHTTPHeaderField: "Content-Type")

        let task = URLSession.shared.uploadTask(with: request, from: body) { data, response, error in

        guard error == nil && data != nil else {
          return
        }

        if let data = String(data: data!, encoding: String.Encoding.utf8) {
            print(data)
        }

        }
        task.resume()
}

How does the uploadTask work? maybe it appends the data of the file to the request body and then adds the boundary automatically? if I use this code, the upload doesn't work, what I have to change?

UPDATE: I've updated the code, now the upload works in foreground using the completionHandler of the uploadTask, but if I create a background session and using URLSessionDataDelegate instead of the completionHandler (because it doesn't work in the background), the transfer rate is very slow also with a 2 MB file, how can I solve this?

UPDATE 2: with the background session, the uploadTask restarts many times and it doesn't complete, never.

Roran
  • 403
  • 4
  • 19
  • use alamofire . – KKRocks Jun 28 '17 at 10:00
  • if possible I wouldn't use any external framework to do this – Roran Jun 28 '17 at 10:11
  • see this : https://stackoverflow.com/a/40542471/3901620 – KKRocks Jun 28 '17 at 10:21
  • the example shows dataTask (it doesn't work in the background) and downloadTask, i need working uploadTask example – Roran Jun 28 '17 at 10:28
  • you need to configuration for background task. it is not depends on upload task or datatask. – KKRocks Jun 28 '17 at 10:32
  • no, I need to know how the uploadTask manipulate the request to attach the file, I already know what I have to do to use the uploadTask in the background (creating a background session) – Roran Jun 28 '17 at 10:35
  • why can't you pass `"Content-Disposition": "attachment; filename="filename.mp4"` in the request header? Then you won't have to deal with multipart. – Eugene Dudnyk Jun 29 '17 at 14:36
  • @DisableR I don't need to change the content disposition, now with the updated code the upload works fine but if I change the shared urlsession with a new background session using uploadTask(with: URLRequest, from: Data) and the delegate, the session is very slowly and after that has sent some bytes, it seems stucked – Roran Jun 29 '17 at 14:41
  • For background session configuration, you have no control over priority of network connection of your app in background. iOS decides when it's best for it to send/receive the data. You can try to set `sessionConfiguration.networkServiceType = [.video, .background]` and `sessionConfiguration.discretionary = false` – Eugene Dudnyk Jun 29 '17 at 14:42
  • thank you but xcode says that I can't set the networkServiceType as array literal, how can I set it? – Roran Jun 29 '17 at 14:49
  • Yeah, I just found that it's not a bitmasked property, so you have to choose either `.video` or `.background`, you can't combine them. Try `sessionConfiguration.networkServiceType = .background` – Eugene Dudnyk Jun 29 '17 at 14:49
  • it seems very slow also with this property (I'm using the simulator) – Roran Jun 29 '17 at 14:52
  • What aboud `.video`? Also slow? – Eugene Dudnyk Jun 29 '17 at 14:52
  • yes, It doesn't seem to change anything, maybe a little bit faster than before, but too much time for a 2 mb file that is uploaded in 10-20 seconds in foreground mode – Roran Jun 29 '17 at 14:57

3 Answers3

9

After some attempts, I saw the URLSession.uploadTask(with: URLRequest, fromFile: URL) method attaches the file as raw body to the request, so the problem was the server counterpart that was parsing form-data requests instead raw body requests.After I fixed the server side script, the upload works in background with this code:

    var request = URLRequest(url: "my_url")
    request.httpMethod = "POST"
    request.setValue(file.lastPathComponent, forHTTPHeaderField: "filename")


    let sessionConfig = URLSessionConfiguration.background(withIdentifier: "it.example.upload")
    sessionConfig.isDiscretionary = false
    sessionConfig.networkServiceType = .video
    let session = URLSession(configuration: sessionConfig, delegate: self, delegateQueue: OperationQueue.main)

    let task = session.uploadTask(with: request, fromFile: file)
    task.resume()
Roran
  • 403
  • 4
  • 19
2

solution as of 2020 with native URLSession to run background upload with uploadTask and multipart/form-data:

  • I followed this tutorial on setting up request for multipart/form-data as it required for my NodeJS server
  • Then I change a bit on the part for setting up URLSession:
let config = URLSessionConfiguration.background(withIdentifier: "uniqueID")
let session = URLSession(configuration: config, delegate: self, delegateQueue: nil)

// This line is important: here we use withStreamedRequest
let task = session.uploadTask(withStreamedRequest: request)

task.resume()

A little bit about my server side:

  • It's written in NodeJS with Express
  • File uploading is handled by Multer: the way I did it is really standard, you can find in many tutorials online

Hope this help

Duc Trung Mai
  • 733
  • 1
  • 6
  • 13
  • Thank you, the link is really helpful. How did you get server response from delegation? – DareDevil Dec 12 '20 at 09:45
  • I just check if my request is complete using `didCompleteWithError`, I don't get server response, you may need to search on google yourself. – Duc Trung Mai Dec 12 '20 at 11:13
0

Sometimes it is easier for the server to read file from form-data. For instance, the flask framework can read file uploaded in the format of form-data easily by request.files. Alamofire provides an easy way to do this

AF.upload(multipartFormData: { multipartFormData in
                multipartFormData.append(FilePath, withName: FilePath.lastPathComponent)
            }, to: url).responseJSON { response in
                    debugPrint(response)
            }

In this way, the file will be uploaded in the format of form-data.

Collin Zhang
  • 243
  • 3
  • 11