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.
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)
}()
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
}
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)")
}
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