aws-amplify / amplify-swift

A declarative library for application development using cloud services.
Apache License 2.0
453 stars 196 forks source link

How to download multiple files to a file url using Amplify & Swift? #1340

Closed TerremceK closed 3 years ago

TerremceK commented 3 years ago

Is your feature request related to a problem? Please describe.

Hello, the current example in the iOS swift Library only shows how to download 1 file at a time. I'd like to know how to download multiple files inside of an S3 bucket folder at a time.

Describe the solution you'd like

The example files are helpful, but since documentation & help is limited on how to use Amplify & Swift it would really be helpful if there was an example posted in the Library with example code on how to download multiple files to a local url.

For example.

I'd like to download everything inside of an s3 folder named Terry/jokes/ to a local url on my device.

The directory contains the following.

Terry/jokes/ Terry/jokes/joke1.jpg Terry/jokes/jokes1.mp4 Terry/jokes/jokes2.mp4 etc..

// Say "JokesFolder" is the file url I would like to download to on my device
let downloadToFileName = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0].appendingPathComponent(username)

let jokesFolder = downloadToFileName.appendingPathComponent("jokes")

Describe alternatives you've considered

I've tried the following code below, and it does give me a success message, but the files are no where to be found on the device.

I'm suspecting that part of the issue is that the download url path doesn't have a name to match each file downloaded. I'm not sure how to overcome this, this is a bit over my head and where an example would be helpful.

`//MARK:- download S3 Username Folder

//Inside my download action I have let username = "Terry" let name = self.username + "/jokes" self.downloadRepligram(from: name) //-------

 func downloadRepligram(from prefix: String) {

    //IF folder does not exist on device then create one
    let paths = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)
    let documentsDirectory = paths[0]
    let docURL = URL(string: documentsDirectory)!
    let dataPath = docURL.appendingPathComponent(username)
    let jokesPath = dataPath.appendingPathComponent("jokes")
    print("jokes path is ", jokesPath)
    if !FileManager.default.fileExists(atPath: jokesPath.absoluteString) {
        do {
            try FileManager.default.createDirectory(atPath: jokesPath.absoluteString, withIntermediateDirectories: true, attributes: nil)
        } catch {
            print(error.localizedDescription);
        }
    }

     let options = StorageListRequest.Options(path: prefix.isEmpty ? nil : prefix)
     Amplify.Storage.list(options: options) { result in
         guard case .success(let storageResults) = result else {
             print("Failed to list: \(result)")
             return
         }

         let allObjects = storageResults.items.map { $0.key }
         let prefixObjects = allObjects.filter { $0.starts(with: prefix) }

         guard !prefixObjects.isEmpty else {
             print("Nothing to download")
             return
         }

         DispatchQueue.concurrentPerform(iterations: prefixObjects.count) { iter in
             let key = prefixObjects[iter]
             Amplify.Storage.downloadFile(key: key, local: jokesPath) { result in
                 switch result {
                 case .failure(let error):
                     print("Error downloading \(key): \(error.localizedDescription)")
                 case .success(let successValue):
                     print("Downloaded \(key): \(successValue): to \(jokesPath)")
                 }
             }
         }
     }
 }`

Is the feature request related to any of the existing Amplify categories?

Storage

Additional context

The code I am using is something I modified from Palpatims example on how to delete multiple files from an s3 bucket.

brennanMKE commented 3 years ago

@TerremceK

All network activity with Amplify Storage runs on top of AWSS3 and Transfer Utility. The URLSession is created with a background configuration. (see source) In order to ensure all network activities are handled you must handle the UIAppDelegate function for background events. See the link below for more details.

Included below is an example of it implemented with AppDelegate.

import UIKit
import AWSS3

class AppDelegate: NSObject, UIApplicationDelegate {
    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
        print(#function)
        return true
    }

    func application(_ application: UIApplication, handleEventsForBackgroundURLSession identifier: String, completionHandler: @escaping () -> Void) {
        print(#function)
        AWSS3TransferUtility.interceptApplication(application, handleEventsForBackgroundURLSession: identifier, completionHandler: completionHandler)
    }
}

For a modern iOS app you may need to use @UIApplicationDelegateAdaptor shown below to get your AppDelegate functions to called.

import SwiftUI

@main
struct MultipartUploadApp: App {
    @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate

    var body: some Scene {
        WindowGroup {
            JokesView()
        }
    }
}

Also, from the code you shared I can see URL and path being used with FileManager. This API from Apple can be a bit tricky in Swift. I've provided some code you can try which has broken out each task into private functions and passes back a list of the file URLs if all downloads are successful. See the attached code with the original and revised code.

brennanMKE commented 3 years ago

@TerremceK Please try out this code with your app and let us know how it works for you.

brennanMKE commented 3 years ago

You can also find docs on background transfers on our docs site.

TerremceK commented 3 years ago

@brennanMKE Thanks for your responses! 😀 I'll read up and experiment with your suggestions when I can and let you know how it goes.

TerremceK commented 3 years ago

@brennanMKE Forgive me for sounding like a noob but how do I properly call the function you wrote? I.E I'm trying the following when the download button is tapped.

@IBAction func downloadRepligramTapped(_ sender: Any) {
    self.downloadAllJokes(username: username, completionHandler: (Result<[URL], Failure>) -> Void)
}

Which gives me the error

Cannot convert value of type '((Result<[URL], CallScreenVC.Failure>) -> Void).Type' to expected argument type '(Result<[URL], CallScreenVC.Failure>) -> Void'

brennanMKE commented 3 years ago

@TerremceK This code is based on what you provided so you could hook it into how you had it running before. Depending on how you set up your AWS resources you may need to provide a prefix based on access levels.

You can find the policy set in your IAM service or with S3.

TerremceK commented 3 years ago

Thanks @brennanMKE I was able to achieve multiple downloads based off of the example code you provided with a few tweaks and refresher on completion handlers. Super appreciate your help, I don't think I would have figured that out on my own. Thank you!