iosswiftamazon-s3urlsessionnsurlsessionuploadtask

Swift: Background upload using URLSession


I'm trying to upload files to the s3 bucket using URLSession. I understood that to upload files in the background, I need to use uploadTask(with:fromFile:) method as mentioned here. So I am performing below steps to upload the file.

  1. Create a background URLSession
lazy var session: URLSession = {
                let bundleIdentifier = Bundle.main.bundleIdentifier!
                let config = URLSessionConfiguration.background(withIdentifier: bundleIdentifier + ".background")
                config.sharedContainerIdentifier = bundleIdentifier
                config.sessionSendsLaunchEvents = true
                return URLSession(configuration: config, delegate: self, delegateQueue: nil)
            }()
  1. Generate request with multipart data

Data Model

struct UploadRequest {
    let destinationUrl: URL
    let sourceURL: URL
    let params: [String: String]
    let fileName: String
    let mimeType: String
}
private func requestAndPath(for
                                uploadParam: UploadRequest) -> (request: URLRequest,
                                                                filePath: URL)? {

        // Create an empty file and append header, file content and footer to it
        let uuid = UUID().uuidString
        let directoryURL = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first!
        let fileURL = directoryURL.appendingPathComponent(uuid)
        let filePath = fileURL.path
        FileManager.default.createFile(atPath: filePath, contents: nil, attributes: nil)
        let file = FileHandle(forWritingAtPath: filePath)!

        let boundary = UUID().uuidString
        let newline = "\r\n"

        do {
            let partName = "file"
            let data = try Data(contentsOf: uploadParam.sourceURL)
            // Write boundary header
            var header = ""
            header += "--\(boundary)" + newline
            header += "Content-Disposition: form-data; name=\"\(partName)\"; filename=\"\(uploadParam.fileName)\"" + newline
            for (key, value) in uploadParam.params {
                header += "Content-Disposition: form-data; name=\"\(key)" + newline
                header += newline
                header += value + newline
            }
            
            header += "Content-Type: \(uploadParam.mimeType)" + newline
            header += newline

            let headerData = header.data(using: .utf8, allowLossyConversion: false)
            // Write data
            file.write(headerData!)
            file.write(data)

            // Write boundary footer
            var footer = ""
            footer += newline
            footer += "--\(boundary)--" + newline
            footer += newline

            let footerData = footer.data(using: .utf8, allowLossyConversion: false)
            file.write(footerData!)
            file.closeFile()

            let contentType = "multipart/form-data; boundary=\(boundary)"
            var urlRequest = URLRequest(url: uploadParam.destinationUrl)
            urlRequest.httpMethod = "POST"
            urlRequest.setValue(contentType, forHTTPHeaderField: "Content-Type")
            return (urlRequest, fileURL)
        } catch {
            debugPrint("Error generating url request")
        }
        return nil
    }
  1. Upload file
func uploadFile(request: UploadRequest) {
       if let reqPath = requestAndPath(for: uploadRequest) {
            let task = session.uploadTask(with: reqPath.request,
                                          fromFile: reqPath.filePath)
            task.resume()
        }
}

When I call the uploadFile method, the delegate didSendBodyData is called once and the control goes to didCompleteWithError with error as nil. But the file is not uploaded to the s3 bucket. What could be the issue?

I am able to upload the file using Alamofire but since Alamofire doesn't support background upload, I would like to fallback to URLSession

Upload using Alamofire (default)

AF.upload(multipartFormData: { multipartFormData in
            for (key, value) in uploadRequest.params {
                if let data = value.data(using: String.Encoding.utf8, allowLossyConversion: false) {
                    multipartFormData.append(data, withName: key)
                }
            }
            multipartFormData.append(
                uploadRequest.sourceURL,
                withName: "File",
                fileName: uploadRequest.fileName,
                mimeType: uploadRequest.mimeType
            )
        }, with: urlRequest).responseData { response in
            if let responseData = response.data {
                let strData = String(decoding: responseData, as: UTF8.self)
                debugPrint("Response data \(strData)")
            } else {
                debugPrint("Error is \(response.error)")
            }
        }.uploadProgress { progress in
            debugPrint("Progress \(progress)")
        }

Solution

  • I made changes in the request body and wrote the data to the file and used uploadTask(with:fromFile:) method using the background session. urlSessionDidFinishEvents(forBackgroundURLSession:) will be called once the upload is completed when the app is in the background.

    private func requestAndPath(for
                                    uploadParam: UploadRequest) -> (request: URLRequest,
                                                                    filePath: URL)? {
            
            let uuid = UUID().uuidString
            let directoryURL = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first!
            let fileURL = directoryURL.appendingPathComponent(uuid)
            let filePath = fileURL.path
            FileManager.default.createFile(atPath: filePath, contents: nil, attributes: nil)
            let file = FileHandle(forWritingAtPath: filePath)!
            
            let boundary = generateBoundary()
            let lineBreak = "\r\n"
            var body = Data()
    
            for (key, value) in uploadParam.params {
                body.append("--\(boundary + lineBreak)")
                body.append("Content-Disposition: form-data; name=\"\(key)\"\(lineBreak + lineBreak)")
                body.append("\(value + lineBreak)")
            }
            
            do {
                let data = try Data(contentsOf: uploadParam.sourceURL)
                body.append("--\(boundary + lineBreak)")
                body.append("Content-Disposition: form-data; name=\"File\"; filename=\"\(uploadParam.fileName)\"\(lineBreak)")
                body.append("Content-Type: \(uploadParam.mimeType + lineBreak + lineBreak)")
                body.append(data)
                body.append(lineBreak)
                body.append("--\(boundary)--\(lineBreak)")
                file.write(body)
                file.closeFile()
                
                var urlRequest = URLRequest(url: uploadParam.destinationUrl)
                urlRequest.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type")
                urlRequest.httpMethod = "POST"
                return (urlRequest, fileURL)
            } catch {
                debugPrint("Error getting request")
            }
            return nil
        } 
    
    
    func generateBoundary() -> String {
            return UUID().uuidString
        }
    
    
    extension Data {
        mutating func append(_ string: String) {
        if let data = string.data(using: .utf8) {
          self.append(data)
        }
      }
    }
    

    Reference: https://stackoverflow.com/a/58246456/696465