【问题标题】:Efficient way to upload multiple images to S3 from iOS从 iOS 将多个图像上传到 S3 的有效方法
【发布时间】:2017-01-17 19:56:23
【问题描述】:

我在应用程序中使用 Amazon S3 作为我的文件存储系统。我所有的项目对象都有几个与之关联的图像,每个都只存储图像 url 以保持我的数据库轻量级。因此,我需要一种有效的方法将多个图像直接从 iOS 上传到 S3,并在成功完成后将它们的 url 存储在我发送到服务器的对象中。我仔细阅读了亚马逊提供的 SDK 和示例应用程序,但我遇到的唯一示例是单个图像上传,如下所示:

 func uploadData(data: NSData) {
    let expression = AWSS3TransferUtilityUploadExpression()
    expression.progressBlock = progressBlock

    let transferUtility = AWSS3TransferUtility.defaultS3TransferUtility()

    transferUtility.uploadData(
        data,
        bucket: S3BucketName,
        key: S3UploadKeyName,
        contentType: "text/plain",
        expression: expression,
        completionHander: completionHandler).continueWithBlock { (task) -> AnyObject! in
            if let error = task.error {
                NSLog("Error: %@",error.localizedDescription);
                self.statusLabel.text = "Failed"
            }
            if let exception = task.exception {
                NSLog("Exception: %@",exception.description);
                self.statusLabel.text = "Failed"
            }
            if let _ = task.result {
                self.statusLabel.text = "Generating Upload File"
                NSLog("Upload Starting!")
                // Do something with uploadTask.
            }

            return nil;
    }
}

对于超过 5 张图片,这将变成一个嵌套的混乱,因为我必须等待每次上传成功返回,然后再启动下一张,然后最后将对象发送到我的数据库。是否有一种高效的、干净的代码来实现我的目标?

亚马逊示例应用 github 的 URL:https://github.com/awslabs/aws-sdk-ios-samples/tree/master/S3TransferUtility-Sample/Swift

【问题讨论】:

  • 你解决了吗?我也在寻找同样的东西。
  • @user2722667 检查我添加的响应...可能为时已晚,但肯定会有所帮助

标签: ios amazon-web-services amazon-s3


【解决方案1】:

这是我用来将多个图像上传到 S3 的代码,同时使用 DispatchGroup()

func uploadOfferImagesToS3() {
    let group = DispatchGroup()

    for (index, image) in arrOfImages.enumerated() {
        group.enter()

        Utils.saveImageToTemporaryDirectory(image: image, completionHandler: { (url, imgScalled) in
            if let urlImagePath = url,
                let uploadRequest = AWSS3TransferManagerUploadRequest() {
                uploadRequest.body          = urlImagePath
                uploadRequest.key           = ProcessInfo.processInfo.globallyUniqueString + "." + "png"
                uploadRequest.bucket        = Constants.AWS_S3.Image
                uploadRequest.contentType   = "image/" + "png"
                uploadRequest.uploadProgress = {(bytesSent:Int64, totalBytesSent:Int64, totalBytesExpectedToSend:Int64) in
                    let uploadProgress = Float(Double(totalBytesSent)/Double(totalBytesExpectedToSend))

                    print("uploading image \(index) of \(arrOfImages.count) = \(uploadProgress)")

                    //self.delegate?.amazonManager_uploadWithProgress(fProgress: uploadProgress)
                }

                self.uploadImageStatus      = .inProgress

                AWSS3TransferManager.default()
                    .upload(uploadRequest)
                    .continueWith(executor: AWSExecutor.immediate(), block: { (task) -> Any? in

                        group.leave()

                        if let error = task.error {
                            print("\n\n=======================================")
                            print("❌ Upload image failed with error: (\(error.localizedDescription))")
                            print("=======================================\n\n")

                            self.uploadImageStatus = .failed
                            self.delegate?.amazonManager_uploadWithFail()

                            return nil
                        }

                        //=>    Task completed successfully
                        let imgS3URL = Constants.AWS_S3.BucketPath + Constants.AWS_S3.Image + "/" + uploadRequest.key!
                        print("imgS3url = \(imgS3URL)")
                        NewOfferManager.shared.arrUrlsImagesNewOffer.append(imgS3URL)

                        self.uploadImageStatus = .completed
                        self.delegate?.amazonManager_uploadWithSuccess(strS3ObjUrl: imgS3URL, imgSelected: imgScalled)

                        return nil
                    })
            }
            else {
                print(" Unable to save image to NSTemporaryDirectory")
            }
        })
    }

    group.notify(queue: DispatchQueue.global(qos: .background)) {
        print("All \(arrOfImages.count) network reqeusts completed")
    }
}

这是关键部分,我至少损失了 5 个小时。 来自NSTemporaryDirectory 的网址每张图片必须不同!!!

class func saveImageToTemporaryDirectory(image: UIImage, completionHandler: @escaping (_ url: URL?, _ imgScalled: UIImage) -> Void) {
    let imgScalled              = ClaimitUtils.scaleImageDown(image)
    let data                    = UIImagePNGRepresentation(imgScalled)

    let randomPath = "offerImage" + String.random(ofLength: 5)

    let urlImgOfferDir = URL(fileURLWithPath: NSTemporaryDirectory().appending(randomPath))
    do {
        try data?.write(to: urlImgOfferDir)
        completionHandler(urlImgOfferDir, imgScalled)
    }
    catch (let error) {
        print(error)
        completionHandler(nil, imgScalled)
    }
}

希望这会有所帮助!

【讨论】:

  • 我正在使用您遇到此问题时共享的相同代码,请您帮我解决我做错了什么。上传图片失败并出现错误:(操作无法完成。(com.amazonaws.AWSCognitoIdentityErrorDomain 错误 10。))
  • @Bonnke 是否可以像这样显示一些进度上传图像 1/5,然后如果上传第一张图像,则更新计数器 2/5。像这样的东西?您的代码是按顺序上传还是并行上传?
【解决方案2】:

正如我在 H. Al-Amri 的回复中所说的那样,如果您需要知道上次上传完成的时间,您不能简单地遍历一组数据并一次性上传它们。

在 Javascript 中有一个库(我认为是 Async.js)可以轻松对数组的各个元素进行后台操作,并在每个元素完成时以及整个数组完成时获取回调。由于我不知道 Swift 有类似的情况,因此您必须链接上传的直觉是正确的。

正如@DavidTamrazov 在 cmets 中解释的那样,您可以使用递归将您的调用链接在一起。我解决这个问题的方法有点复杂,因为我的网络操作是使用 NSOperationQueue 链接 NSOperations 完成的。我将图像数组传递给自定义 NSOperation,该 NSOperation 从数组上传第一张图像。完成后,它会将另一个 NSOperation 添加到我的 NSOperationsQueue 中,其中包含剩余图像的数组。当数组用完图像时,我知道我已经完成了。

以下是我从我使用的大得多的块中切出的示例。将其视为伪代码,因为我对其进行了大量编辑,甚至没有时间编译它。但希望关于如何使用 NSOperations 执行此操作的框架已经足够清楚了。

class NetworkOp : Operation {
    var isRunning = false

    override var isAsynchronous: Bool {
        get {
            return true
        }
    }

    override var isConcurrent: Bool {
        get {
            return true
        }
    }

    override var isExecuting: Bool {
        get {
            return isRunning
        }
    }

    override var isFinished: Bool {
        get {
            return !isRunning
        }
    }

    override func start() {
        if self.checkCancel() {
            return
        }
        self.willChangeValue(forKey: "isExecuting")
        self.isRunning = true
        self.didChangeValue(forKey: "isExecuting")
        main()
    }

    func complete() {
        self.willChangeValue(forKey: "isFinished")
        self.willChangeValue(forKey: "isExecuting")
        self.isRunning = false
        self.didChangeValue(forKey: "isFinished")
        self.didChangeValue(forKey: "isExecuting")
        print( "Completed net op: \(self.className)")
    }

    // Always resubmit if we get canceled before completion
    func checkCancel() -> Bool {
        if self.isCancelled {
            self.retry()
            self.complete()
        }
        return self.isCancelled
    }

    func retry() {
        // Create a new NetworkOp to match and resubmit since we can't reuse existing.
    }

    func success() {
        // Success means reset delay
        NetOpsQueueMgr.shared.resetRetryIncrement()
    }
}

class ImagesUploadOp : NetworkOp {
    var imageList : [PhotoFileListMap]

    init(imageList : [UIImage]) {
        self.imageList = imageList
    }

    override func main() {
        print( "Photos upload starting")
        if self.checkCancel() {
            return
        }

        // Pop image off front of array
        let image = imageList.remove(at: 0)

        // Now call function that uses AWS to upload image, mine does save to file first, then passes
        // an error message on completion if it failed, nil if it succceeded
        ServerMgr.shared.uploadImage(image: image, completion: {  errorMessage ) in
            if let error = errorMessage {
                print("Failed to upload file - " + error)
                self.retry()
            } else {
                print("Uploaded file")
                if !self.isCancelled {
                    if self.imageList.count == 0 {
                        // All images done, here you could call a final completion handler or somthing.
                    } else {
                        // More images left to do, let's put another Operation on the barbie:)
                        NetOpsQueueMgr.shared.submitOp(netOp: ImagesUploadOp(imageList: self.imageList))
                    }
                }
            }
            self.complete()
         })
    }

    override func retry() {
        NetOpsQueueMgr.shared.retryOpWithDelay(op: ImagesUploadOp(form: self.form, imageList: self.imageList))
    }
}


// MARK: NetOpsQueueMgr  -------------------------------------------------------------------------------

class NetOpsQueueMgr {
    static let shared = NetOpsQueueMgr()

    lazy var opsQueue :OperationQueue = {
        var queue = OperationQueue()
        queue.name = "myQueName"
        queue.maxConcurrentOperationCount = 1
        return queue
    }()

    func submitOp(netOp : NetworkOp) {
         opsQueue.addOperation(netOp)
    }

   func uploadImages(imageList : [UIImage]) {
        let imagesOp = ImagesUploadOp(form: form, imageList: imageList)
        self.submitOp(netOp: imagesOp)
    }
}

【讨论】:

  • 这是一个很好的选择,可以很好地利用预先存在的 Swift 类,谢谢!我最终使用了一个递归函数,该函数将图像数组作为参数,并且与您的逻辑类似,一旦传递给它的数组为空,它就会终止。我只在成功上传图片后递归以确保一致性。
  • 谢谢@DavidTamrazov,你是对的,这不是唯一的方法,事实上你做的方式更直接,也是一个更好的例子。我将更新我的答案以提及这一点。
猜你喜欢
  • 2017-07-29
  • 2020-07-29
  • 1970-01-01
  • 1970-01-01
  • 2013-08-27
  • 2019-04-11
  • 2021-04-04
  • 1970-01-01
  • 2011-11-10
相关资源
最近更新 更多