Search code examples
iosswiftapiuploadredcap

How to upload a file (to REDCap) from an iOS device using swift


I am trying to upload a file saved to a directory within an app (built using ResearchKit) to a server (via REDCap API). REDCap API playground suggests code in several languages, but not Obj-C or Swift. For this case, I need to use Swift, which is a first for me. Below is my attempt at a method, which is unfortunately not working:

func postFileToRedcap (_ fileUrl: URL) {
    guard let url = URL (string: "https://our_redcap_url.edu/api/") else {return}
    let token = "ABCDEFGHIJK123456789" // unique to REDCap project
    let record = "1" // can be set elsewhere
    let event = ""
    let field = "file_upload_field" // name of field in REDCap
    
    if !fileUrl.pathComponents.isEmpty {
        let fileName = URL(fileURLWithPath: fileUrl.absoluteString).lastPathComponent
        var request = URLRequest.init(url: url)
        request.httpMethod = "POST"
        // parameters required by REDCap
        request.addValue(token, forHTTPHeaderField: "token")
        request.addValue(fileName, forHTTPHeaderField: "content")
        request.addValue("import", forHTTPHeaderField: "action")
        request.addValue(record, forHTTPHeaderField: "record")
        request.addValue(field, forHTTPHeaderField: "field")
        request.addValue(event, forHTTPHeaderField: "event")
        request.addValue("json", forHTTPHeaderField: "returnFormat")
        
        let task = URLSession.shared.uploadTask(
            with: request,
            fromFile: fileUrl
        )
        task.resume()
    }
}

The primary problem appears to be the parameter settings, but I can't seem to find a solution that works for REDCap.

All help graciously received. Thanks in advance!


Solution

  • After far too much time, I realised the key was to use

    request.addValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type")

    but it was code snippets from Postman that enabled me to successfully build the parameter array and the payload. Here's a working version that can be simplified, I'm sure.

       import Foundation
         
        guard let url = URL (string: "https://our_redcap_url.edu/api/") else {return}
        let token = "ABCDEFGHIJK123456789" // unique to REDCap project
        let record : = "1" // set elsewhere
        let event = ""
        let field = "file_upload_field" // name of field in REDCap
        let path = "path_of_file_to_be_uploaded"
        let repeat_instance : Int = 1
         
        let parameters = [
            [
            "key": "token",
            "value": token,
            "type": "text"
            ],
            [
            "key": "content",
            "value": "file",
            "type": "text"
            ],
            [
            "key": "action",
            "value": "import",
            "type": "text"
            ],
            [
            "key": "record",
            "value": record,
            "type": "text"
            ],
            [
            "key": "field",
            "value": field_name,
            "type": "text"
            ],
            [
            "key": "event",
            "value": event,
            "type": "text"
            ],
            [
            "key": "repeat_instance",
            "value": repeat_instance,
            "type": "text"
            ],
            [
            "key": "returnFormat",
            "value": "json",
            "type": "text"
            ],
            [
            "key": "file",
            "src": path,
            "type": "file"
            ]] as [[String : Any]]
         
        // build payload
        let boundary = "Boundary-\(UUID().uuidString)"
        var body = ""
        for param in parameters {
        if param["disabled"] == nil {
            let paramName = param["key"]!
            body += "--\(boundary)\r\n"
            body += "Content-Disposition:form-data; name=\"\(paramName)\""
            if param["contentType"] != nil {
                body += "\r\nContent-Type: \(param["contentType"] as! String)"
            }
            let paramType = param["type"] as! String
            if paramType == "text" {
                let paramValue = param["value"] as! String
                body += "\r\n\r\n\(paramValue)\r\n"
            } else {
                let paramSrc = param["src"] as! String
                let fileData = try? NSData(contentsOfFile:paramSrc, options:[]) as Data
                let fileContent = String(data: fileData!, encoding: .utf8)!
                body += "; filename=\"\(paramSrc)\"\r\n"
                + "Content-Type: \"content-type header\"\r\n\r\n\(fileContent)\r\n"
                }
            }
        }
        body += "--\(boundary)--\r\n";
        let postData = body.data(using: .utf8)
         
        // create URL request and session
        var request = URLRequest(url: url,timeoutInterval: Double.infinity)
        request.addValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type")
         
        request.httpMethod = "POST"
        request.httpBody = postData
         
        let task = URLSession.shared.dataTask(with: request) { data, response, error in
        guard let data = data else {
            print(String(describing: error))
            return
            }
            print(String(data: data, encoding: .utf8)!)
        }
        task.resume()