When uploading data (in my case, images) to an AWS S3 bucket
, the default timeout for the upload operation is 50 minutes. The uploads I plan on doing are pretty small, ~150kb per photo, up to 9 photos, so if a user has a decent connection, it should only take a few seconds max. I plan on locking the UI on a 'uploading' spinning wheel while the upload is happening, so I need to guarantee a callback on a success or failure so I can unlock the UI and the user can move on.
The problem is that if the user has no connection, the AWSS3TransferUtilityUploadTask
won't give a failure callback until the 50 minute timeout occurs- which is obviously too long for my use case.
Here is the relevant code for this:
import UIKit
import AWSCore
import AWSS3
enum UploadImageResult {
case success
case failure
}
struct UploadImagePacket {
let name : String
let image : UIImage
}
class ImageUploader {
private var packets : [UploadImagePacket] = []
private var currentPacketIndex = 0
private let bucketName = "this-is-not-a-real-bucket-name"
private var transferUtility : AWSS3TransferUtility {
get {
return AWSS3TransferUtility.default()
}
}
init() {
// Set up service configurations for user
let accessKey = "XXXXXXXXXXXXXXXXXXXX"
let secretKey = "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
let credentials = AWSStaticCredentialsProvider(accessKey: accessKey, secretKey: secretKey)
let configuration = AWSServiceConfiguration(region: AWSRegionType.USEast2, credentialsProvider: credentials)
AWSServiceManager.default().defaultServiceConfiguration = configuration
}
// Upload a single image to S3 bucket
private func uploadImage(_ packet : UploadImagePacket, completion : @escaping (UploadImageResult) -> Void) {
let imageData = packet.image.jpegData(compressionQuality: 1)!
// Called periodically to report upload progress
let expression = AWSS3TransferUtilityUploadExpression()
expression.progressBlock = { (task, progress) in
DispatchQueue.main.async(execute: {
// For solution 2, I would create a timer and reset it every time this closure is fired . . .
})
}
// Called when upload is complete
var completionHandler: AWSS3TransferUtilityUploadCompletionHandlerBlock?
completionHandler = { (task, error) -> Void in
DispatchQueue.main.async(execute: {
if let error = error {
// Failed
NSLog(error.localizedDescription)
completion(.failure)
} else {
// Did not fail
completion(.success)
}
})
}
// Actually begin upload
transferUtility.uploadData(imageData, bucket: bucketName, key: packet.name, contentType: "image/jpeg", expression: expression, completionHandler: completionHandler).continueWith { (task) -> Any? in
if let error = task.error {
NSLog("S3 upload failed with error : \(error.localizedDescription)")
completion(.failure)
}
return nil
}
}
// Upload multiple images to S3
func uploadImages(_ packets : [UploadImagePacket], completion : @escaping (UploadImageResult) -> Void) {
self.packets = packets
currentPacketIndex = 0
_uploadImages(completion: completion)
}
private func _uploadImages (completion : @escaping (UploadImageResult) -> Void) {
// If no packet to upload, call completion with a success
guard currentPacketIndex < packets.count else {
completion(.success)
return
}
NSLog("Uploading \(currentPacketIndex) of \(packets.count) images . . .")
// Upload packet & upload rest of packets in completion
uploadImage(packets[currentPacketIndex]) { result in
switch result {
case .success:
// On success, move onto next packet
self.currentPacketIndex += 1
self._uploadImages(completion: completion)
case .failure:
// On failure, call completion with a failure
completion(.failure)
}
}
}
@objc func uploadTimeOut() {
// This would be used for solution 2 to cancel all of the transfer utility tasks & call the completion with a failure . . .
}
}
The two potential solutions I have determined are
- customize AWS's default timeout for this process, or
- Use a swift
Timer
/NSTimer
and instead of a timeout that is called if an upload isn't complete within [x] seconds, have a timeout that is called if the upload hasn't made any progress within [x] seconds.
For solution 1
I simply haven't been able to find a way to do this. I have only been able to find relative solutions pertaining to downloads, not uploads- the best hints found in this post.
For solution 2
Ideally I can use solution 1, but if not, the best way I have found to do this is to create a Timer
, that will call a timeout after x amount of seconds (something like 5 seconds), and reset the timer every time the expression.progressBlock
is called. My issue with this is the only way I've found to reset a Timer
to invalidate
and then redefine the timer, which seems awfully expensive considering how rapidly expression.progressBlock
is called.