0

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

  1. customize AWS's default timeout for this process, or
  2. 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.

  • `the default timeout for the upload operation is 50 minutes` This seems odd. Is it documented somewhere? – ahbou Dec 24 '19 at 11:31
  • @ahbou yes in the 'Limitations' section at this link https://aws.amazon.com/blogs/mobile/amazon-s3-transfer-utility-for-ios/ . I think the reasoning is that there are cases for extremely large uploads (a long video for example) – Charles Hetterich Dec 24 '19 at 11:34
  • It looks like a maximum not the default, but check my answer bellow. – ahbou Dec 24 '19 at 11:38

1 Answers1

2

You can define a custom time out by setting timeoutIntervalForResource and registering the Custom configuration

    let transferUtilityConfigurationShortExpiry = AWSS3TransferUtilityConfiguration()        
    transferUtilityConfigurationShortExpiry.isAccelerateModeEnabled = false
    transferUtilityConfigurationShortExpiry.timeoutIntervalForResource = 2 //2 seconds

    AWSS3TransferUtility.register(
       with: AWSServiceManager.default().defaultServiceConfiguration!,
       transferUtilityConfiguration: transferUtilityConfigurationShortExpiry,
       forKey: "custom-timeout"
    )

And then use the associated transferUtility

AWSS3TransferUtility.s3TransferUtility(forKey: "custom-timeout")
ahbou
  • 4,710
  • 23
  • 36
  • This is it, but it's worth noting you also need to register that config with the serviceConfiguration with a key, and then to use the Transfer Utility for that key – Charles Hetterich Dec 24 '19 at 11:59