NSURLSession和Amazon S3上传

14

我有一个应用程序,目前正在将图像上传到亚马逊S3。我一直在尝试将其从使用NSURLConnection切换到NSURLSession,以便在应用程序在后台时可以继续上传!但我似乎遇到了些问题。虽然创建了NSURLRequest并将其传递给NSURLSession,但是如果我将同样的请求传递给NSURLConnection,亚马逊会返回403 - 禁止响应,并且无法上传该文件。

这是创建响应的代码:

NSString *requestURLString = [NSString stringWithFormat:@"http://%@.%@/%@/%@", BUCKET_NAME, AWS_HOST, DIRECTORY_NAME, filename];
NSURL *requestURL = [NSURL URLWithString:requestURLString];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:requestURL
                                                       cachePolicy:NSURLRequestReloadIgnoringLocalAndRemoteCacheData
                                                   timeoutInterval:60.0];
// Configure request
[request setHTTPMethod:@"PUT"];
[request setValue:[NSString stringWithFormat:@"%@.%@", BUCKET_NAME, AWS_HOST] forHTTPHeaderField:@"Host"];
[request setValue:[self formattedDateString] forHTTPHeaderField:@"Date"];
[request setValue:@"public-read" forHTTPHeaderField:@"x-amz-acl"];
[request setHTTPBody:imageData];

然后这个标志着响应的结束(我想这是来自另一个SO答案):

NSString *contentMd5  = [request valueForHTTPHeaderField:@"Content-MD5"];
NSString *contentType = [request valueForHTTPHeaderField:@"Content-Type"];
NSString *timestamp   = [request valueForHTTPHeaderField:@"Date"];

if (nil == contentMd5)  contentMd5  = @"";
if (nil == contentType) contentType = @"";

NSMutableString *canonicalizedAmzHeaders = [NSMutableString string];

NSArray *sortedHeaders = [[[request allHTTPHeaderFields] allKeys] sortedArrayUsingSelector:@selector(caseInsensitiveCompare:)];

for (id key in sortedHeaders)
{
    NSString *keyName = [(NSString *)key lowercaseString];
    if ([keyName hasPrefix:@"x-amz-"]){
        [canonicalizedAmzHeaders appendFormat:@"%@:%@\n", keyName, [request valueForHTTPHeaderField:(NSString *)key]];
    }
}

NSString *bucket = @"";
NSString *path   = request.URL.path;
NSString *query  = request.URL.query;

NSString *host  = [request valueForHTTPHeaderField:@"Host"];

if (![host isEqualToString:@"s3.amazonaws.com"]) {
    bucket = [host substringToIndex:[host rangeOfString:@".s3.amazonaws.com"].location];
}

NSString* canonicalizedResource;

if (nil == path || path.length < 1) {
    if ( nil == bucket || bucket.length < 1 ) {
        canonicalizedResource = @"/";
    }
    else {
        canonicalizedResource = [NSString stringWithFormat:@"/%@/", bucket];
    }
}
else {
    canonicalizedResource = [NSString stringWithFormat:@"/%@%@", bucket, path];
}

if (query != nil && [query length] > 0) {
    canonicalizedResource = [canonicalizedResource stringByAppendingFormat:@"?%@", query];
}

NSString* stringToSign = [NSString stringWithFormat:@"%@\n%@\n%@\n%@\n%@%@", [request HTTPMethod], contentMd5, contentType, timestamp, canonicalizedAmzHeaders, canonicalizedResource];

NSString *signature = [self signatureForString:stringToSign];

[request setValue:[NSString stringWithFormat:@"AWS %@:%@", self.S3AccessKey, signature] forHTTPHeaderField:@"Authorization"];

那么如果我使用这行代码:

[NSURLConnection connectionWithRequest:request delegate:self];

它能够正常工作并上传文件,但如果我使用:

NSURLSessionUploadTask *task = [self.session uploadTaskWithRequest:request fromFile:[NSURL fileURLWithPath:filePath]];
[task resume];

我收到了“禁止访问”错误..!?

有没有人试过使用这个方法上传到S3并遇到类似的问题?我想知道这是否与会话暂停和恢复上传的方式有关,或者它对请求做出了一些奇怪的操作..?

一个可能的解决方案是将文件上传到我控制的临时服务器,等待上传完成后再将其转发到S3...但这显然不是理想的解决方案!

非常感谢任何帮助!

谢谢!


@GeogeGreen 我需要上传大型视频到S3存储桶,最可能是5GB,我能否使用NSURLSession来完成,因为我所读到的是后台会话不会执行很长时间。 - Mr.G
8个回答

8

我根据Zeev Vax的回答使其起作用。我想提供一些我遇到的问题的见解,并提供微小的改进。

构建一个普通的PutRequest,例如

S3PutObjectRequest* putRequest = [[S3PutObjectRequest alloc] initWithKey:keyName inBucket:bucketName];

putRequest.credentials = credentials;
putRequest.filename = theFilePath;

现在我们需要做一些S3Client通常为我们做的工作。
// set the endpoint, so it is not null
putRequest.endpoint = s3Client.endpoint;

// if you are using session based authentication, otherwise leave it out
putRequest.securityToken = messageTokenDTO.securityToken;

// sign the request (also computes md5 checksums etc.)
NSMutableURLRequest *request = [s3Client signS3Request:putRequest];

现在将所有内容复制到一个新请求中。亚马逊使用自己的NSUrlRequest类,这可能会引起异常。

NSMutableURLRequest* request2 = [[NSMutableURLRequest alloc]initWithURL:request.URL];
[request2 setHTTPMethod:request.HTTPMethod];
[request2 setAllHTTPHeaderFields:[request allHTTPHeaderFields]];

现在我们可以开始实际的传输

NSURLSession* backgroundSession = [self backgroundSession];
_uploadTask = [backgroundSession uploadTaskWithRequest:request2 fromFile:[NSURL fileURLWithPath:theFilePath]];
[_uploadTask resume];

这是创建后台会话的代码:
- (NSURLSession *)backgroundSession {
    static NSURLSession *session = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{

        NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration backgroundSessionConfiguration:@"com.example.my.unique.id"];
        session = [NSURLSession sessionWithConfiguration:configuration delegate:self delegateQueue:nil];
    });

    return session;
}

我花了一些时间才弄清楚,会话/任务委托需要处理身份验证挑战(我们实际上正在对s3进行身份验证)。因此,只需实现即可。

- (void)URLSession:(NSURLSession *)session didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition, NSURLCredential *))completionHandler {
    NSLog(@"session did receive challenge");
    completionHandler(NSURLSessionAuthChallengePerformDefaultHandling, nil);
}

1
你如何使用预签名认证来完成这个任务?客户端只有accessKey和signature,这意味着你拥有key和secret。 - Ryan Romanchuk
我实际上是在使用FederationToken来生成临时凭证 (http://docs.aws.amazon.com/STS/latest/APIReference/API_GetFederationToken.html)。我从未使用过预签名URL,但据我所了解,根据文档,它只是对生成的URL进行简单的URL请求。无需使用AWS SDK方法,只需创建一个NSUrlSessionUploadTask即可,完全不需要任何AWS集成。 - Marius Jeskulke
好的,它可以工作。奇怪的是,我无法向上传的文件添加前缀,即 https://s3.amazonaws.com/NSURLessionUploadTest/image.jpg 可以正常工作,但是 https://s3.amazonaws.com/NSURLessionUploadTest/Prefix/image.jpg 却失败了。有人遇到过这种情况吗? - Xcoder
由于这只是S3的任何其他字符串,因此结果应该与前缀有无相同。也许是存储桶策略阻止了您的上传。到底是什么错误?第二个想法:在开始上传任务之前,请打印出请求2的URL,也许斜杠会以某种方式被更改。 - Marius Jeskulke
如果网络连接中断,我该如何恢复? - Mr.G
使用s3,通常会使用多部分上传来完成此操作。它应该与此处演示的相同原则一起工作(通过aws sdk签名+包装在NSUrlRequest中)。有关多部分上传的详细信息,请参见https://aws.amazon.com/articles/Amazon-S3/0006282245644577。目前我不使用它们,所以很遗憾我无法为您提供示例。此外,我建议为此打开一个新问题。 - Marius Jeskulke

7

这里的答案略有过时,我花了很多时间在Swift和新的AWS SDK上尝试让它工作。所以,以下是如何使用新的AWSS3PreSignedURLBuilder(版本2.0.7+可用)在Swift中完成它:

class S3BackgroundUpload : NSObject {

    // Swift doesn't support static properties yet, so have to use structs to achieve the same thing.
    struct Static {
        static var session : NSURLSession?
    }

    override init() {
        super.init()

        // Note: There are probably safer ways to store the AWS credentials.
        let configPath = NSBundle.mainBundle().pathForResource("appconfig", ofType: "plist")
        let config = NSDictionary(contentsOfFile: configPath!)
        let accessKey = config.objectForKey("awsAccessKeyId") as String?
        let secretKey = config.objectForKey("awsSecretAccessKey") as String?
        let credentialsProvider = AWSStaticCredentialsProvider .credentialsWithAccessKey(accessKey!, secretKey: secretKey!)

        // AWSRegionType.USEast1 is the default S3 endpoint (use it if you don't need specific endpoints such as s3-us-west-2.amazonaws.com)
        let configuration = AWSServiceConfiguration(region: AWSRegionType.USEast1, credentialsProvider: credentialsProvider)

        // This is setting the configuration for all AWS services, you can also pass in this configuration to the AWSS3PreSignedURLBuilder directly.
        AWSServiceManager.defaultServiceManager().setDefaultServiceConfiguration(configuration)

        if Static.session == nil {
            let configIdentifier = "com.example.s3-background-upload"

            var config : NSURLSessionConfiguration
            if NSURLSessionConfiguration.respondsToSelector("backgroundSessionConfigurationWithIdentifier:") {
                // iOS8
                config = NSURLSessionConfiguration.backgroundSessionConfigurationWithIdentifier(configIdentifier)
            } else {
                // iOS7
                config = NSURLSessionConfiguration.backgroundSessionConfiguration(configIdentifier)
            }

            // NSURLSession background sessions *need* to have a delegate.
            Static.session = NSURLSession(configuration: config, delegate: self, delegateQueue: nil)
        }
    }

    func upload() {
        let s3path = "/some/path/some_file.jpg"
        let filePath = "/var/etc/etc/some_file.jpg"

        // Check if the file actually exists to prevent weird uncaught obj-c exceptions.
        if NSFileManager.defaultManager().fileExistsAtPath(filePath) == false {
            NSLog("file does not exist at %@", filePath)
            return
        }

        // NSURLSession needs the filepath in a "file://" NSURL format.
        let fileUrl = NSURL(string: "file://\(filePath)")

        let preSignedReq = AWSS3GetPreSignedURLRequest()
        preSignedReq.bucket = "bucket-name"
        preSignedReq.key = s3path
        preSignedReq.HTTPMethod = AWSHTTPMethod.PUT                   // required
        preSignedReq.contentType = "image/jpeg"                       // required
        preSignedReq.expires = NSDate(timeIntervalSinceNow: 60*60)    // required

        // The defaultS3PreSignedURLBuilder uses the global config, as specified in the init method.
        let urlBuilder = AWSS3PreSignedURLBuilder.defaultS3PreSignedURLBuilder()

        // The new AWS SDK uses BFTasks to chain requests together:
        urlBuilder.getPreSignedURL(preSignedReq).continueWithBlock { (task) -> AnyObject! in

            if task.error != nil {
                NSLog("getPreSignedURL error: %@", task.error)
                return nil
            }

            var preSignedUrl = task.result as NSURL
            NSLog("preSignedUrl: %@", preSignedUrl)

            var request = NSMutableURLRequest(URL: preSignedUrl)
            request.cachePolicy = NSURLRequestCachePolicy.ReloadIgnoringLocalCacheData

            // Make sure the content-type and http method are the same as in preSignedReq
            request.HTTPMethod = "PUT"
            request.setValue(preSignedReq.contentType, forHTTPHeaderField: "Content-Type")

            // NSURLSession background session does *not* support completionHandler, so don't set it.
            let uploadTask = Static.session?.uploadTaskWithRequest(request, fromFile: fileUrl)

            // Start the upload task:
            uploadTask?.resume()

            return nil
        }
    }
}

extension S3BackgroundUpload : NSURLSessionDelegate {

    func URLSession(session: NSURLSession, dataTask: NSURLSessionDataTask, didReceiveData data: NSData) {
        NSLog("did receive data: %@", NSString(data: data, encoding: NSUTF8StringEncoding))
    }

    func URLSession(session: NSURLSession, task: NSURLSessionTask, didCompleteWithError error: NSError?) {
        NSLog("session did complete")
        if error != nil {
            NSLog("error: %@", error!.localizedDescription)
        }
        // Finish up your post-upload tasks.
    }
}

我想使用V2 API将视频上传到S3存储桶,并且它应该支持暂停和恢复功能,可以使用此代码片段的Objective-C版本进行上传。 - Mr.G

3

我对NSURLSessionUploadTask并不十分了解,但是我可以告诉您我会如何进行调试。

我会使用像Charles这样的工具来查看应用程序发送的HTTP(S)请求。问题可能是NSURLSessionUploadTask忽略了您设置的标头,或者它使用了Amazon的S3不期望的文件上传HTTP方法。这可以通过拦截代理轻松验证。

此外,当Amazon S3返回像403这样的错误时,它实际上会发送一个包含有关错误的更多信息的XML文档。也许有一个NSURLSession的委托方法可以检索响应正文?如果没有,那么Charles肯定会为您提供更多的见解。


太棒了,真的帮了我大忙。苹果在我签署请求后添加了一个额外的头字段! - George Green
@GeorgeGreen,你能提供更多的信息吗?你最终是如何克服这个问题的? - Stavash
@GeorgeGreen 我也非常感兴趣。 - pronebird
@GeorgeGreen,您能否详细说明一下这个问题? - Ryan Romanchuk
从“使用文件上传正文内容”的文档中:会话对象根据数据对象的大小计算Content-Length标头。如果您的应用程序未为Content-Type标头提供值,则会话还会提供一个值。这可能会破坏您的S3签名。 - Isak

2

这是我的代码来运行任务:

AmazonS3Client *s3Client = [[AmazonS3Client alloc] initWithAccessKey:accessKey withSecretKey:secretKey];
S3PutObjectRequest *s3PutObjectRequest = [[S3PutObjectRequest alloc] initWithKey:[url lastPathComponent] inBucket:bucket];
s3PutObjectRequest.cannedACL = [S3CannedACL publicRead];
s3PutObjectRequest.endpoint = s3Client.endpoint;
s3PutObjectRequest.contentType = fileMIMEType([url absoluteString]);
[s3PutObjectRequest configureURLRequest];

NSMutableURLRequest *request = [s3Client signS3Request:s3PutObjectRequest];
NSMutableURLRequest *request2 = [[NSMutableURLRequest alloc]initWithURL:request.URL];
[request2 setHTTPMethod:request.HTTPMethod];
[request2 setAllHTTPHeaderFields:[request allHTTPHeaderFields]];

NSURLSessionUploadTask *task = [[self backgroundURLSession] uploadTaskWithRequest:request2 fromFile:url];
[task resume];

我开源了我的S3后台上传程序https://github.com/genadyo/S3Uploader/


不鼓励仅提供链接的答案。请在您的答案中包含解决方案的要点,或删除此答案并留下评论。 - Rob
@Genady,如果互联网连接断开了,有什么方法可以恢复上传吗? - Mr.G
有趣的问题,我还没有尝试过。 - Genady Okrain
@Genady Okrain 目前如果连接中断,上传过程将停止,您认为有没有可能从停止的地方恢复上传? - Mr.G
仅适用于API v1,但目前已有v2版本可用,因此它并不实际。 - Vyachaslav Gerchicov

2

为了进行后台上传/下载,您需要使用带有后台配置的NSURLSession。

自AWS SDK 2.0.7以来,您可以使用预签名请求:

预签名URL生成器** - SDK现在包括对预签名Amazon Simple Storage Service (S3) URL的支持。您可以使用这些URL来使用NSURLSession类执行后台传输。

初始化后台NSURLSession和AWS服务。

- (void)initBackgroundURLSessionAndAWS
{
    NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:AWSS3BackgroundSessionUploadIdentifier];
    self.urlSession = [NSURLSession sessionWithConfiguration:configuration delegate:self delegateQueue:nil];
    AWSServiceConfiguration *configuration = [AWSServiceConfiguration configurationWithRegion:DefaultServiceRegionType credentialsProvider:credentialsProvider];
    [AWSServiceManager defaultServiceManager].defaultServiceConfiguration = configuration;
    self.awss3 = [[AWSS3 alloc] initWithConfiguration:configuration];
}

实现文件上传功能

- (void)uploadFile
{
    AWSS3GetPreSignedURLRequest *getPreSignedURLRequest = [AWSS3GetPreSignedURLRequest new];
    getPreSignedURLRequest.bucket = @"your_bucket";
    getPreSignedURLRequest.key = @"your_key";
    getPreSignedURLRequest.HTTPMethod = AWSHTTPMethodPUT;
    getPreSignedURLRequest.expires = [NSDate dateWithTimeIntervalSinceNow:3600];
    //Important: must set contentType for PUT request
    getPreSignedURLRequest.contentType = @"your_contentType";

    [[[AWSS3PreSignedURLBuilder defaultS3PreSignedURLBuilder] getPreSignedURL:getPreSignedURLRequest] continueWithBlock:^id(BFTask *task) {
        if (task.error)
        {
            NSLog(@"Error BFTask: %@", task.error);
        }
        else
        {
            NSURL *presignedURL = task.result;
            NSLog(@"upload presignedURL is: \n%@", presignedURL);

            NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:presignedURL];
            request.cachePolicy = NSURLRequestReloadIgnoringLocalCacheData;
            [request setHTTPMethod:@"PUT"];
            [request setValue:contentType forHTTPHeaderField:@"Content-Type"];

//          Background NSURLSessions do not support the block interfaces, delegate only.
            NSURLSessionUploadTask *uploadTask = [self.session uploadTaskWithRequest:request fromFile:@"file_path"];

            [uploadTask resume];
        }
        return nil;
    }];
}

NSURLSession的代理方法:

- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error
{
    if (error)
    {
        NSLog(@"S3 UploadTask: %@ completed with error: %@", task, [error localizedDescription]);
    }
    else
    {
//      AWSS3GetPreSignedURLRequest does not contain ACL property, so it has to be set after file was uploaded
        AWSS3PutObjectAclRequest *aclRequest = [AWSS3PutObjectAclRequest new];
        aclRequest.bucket = @"your_bucket";
        aclRequest.key = @"yout_key";
        aclRequest.ACL = AWSS3ObjectCannedACLPublicRead;

        [[self.awss3 putObjectAcl:aclRequest] continueWithBlock:^id(BFTask *bftask) {
            dispatch_async(dispatch_get_main_queue(), ^{
                if (bftask.error)
                {
                    NSLog(@"Error putObjectAcl: %@", [bftask.error localizedDescription]);
                }
                else
                {
                    NSLog(@"ACL for an uploaded file was changed successfully!");
                }
            });
            return nil;
        }];
    }
}

1
我刚刚花了一些时间,最终成功了。最好的方法是使用AWS库创建带有签名标头的请求,然后复制该请求。复制请求非常关键,因为否则NSURLSessionTask会失败。在下面的代码示例中,我使用了AFNetworking并对AFHTTPSessionManager进行了子类化,但此代码也适用于NSURLSession。
    @implementation MyAFHTTPSessionManager
    {

    }

    static MyAFHTTPSessionManager *sessionManager = nil;
    + (instancetype)manager {
        if (!sessionManager)
            sessionManager = [[MyAFHTTPSessionManager alloc] init];
        return sessionManager;
    }

    - (id)init {
        NSURLSessionConfiguration *sessionConfiguration = [NSURLSessionConfiguration          backgroundSessionConfiguration:toutBackgroundSessionNameAF];
        sessionConfiguration.timeoutIntervalForRequest = 30;
        sessionConfiguration.timeoutIntervalForResource = 300;
        self = [super initWithSessionConfiguration:sessionConfiguration];
        if (self)
        {
        }
        return self;
    }

    - (NSURLSessionDataTask *)POSTDataToS3:(NSURL *)fromFile
                               Key:(NSString *)key
                         completionHandler:(void (^)(NSURLResponse *response, id responseObject, NSError *error))completionHandler
    {
        S3PutObjectRequest *s3Request = [[S3PutObjectRequest alloc] initWithKey:key inBucket:_s3Bucket];
        s3Request.cannedACL = [S3CannedACL publicReadWrite];
        s3Request.securityToken = [CTUserDefaults awsS3SessionToken];
        [s3Request configureURLRequest];
        NSMutableURLRequest *request = [_s3Client signS3Request:s3Request];
        // For some reason, the signed S3 request comes back with '(null)' as a host.
        NSString *urlString = [NSString stringWithFormat:@"%@/%@/%@", _s3Client.endpoint, _s3Bucket, [key stringWithURLEncoding]] ;
        request.URL = [NSURL URLWithString:urlString];
        // Have to create a new request and copy all the headers otherwise the NSURLSessionDataTask will fail (since request get a pointer back to AmazonURLRequest which is a subclass of NSMutableURLRequest)
        NSMutableURLRequest *request2 = [[NSMutableURLRequest alloc] initWithURL:[NSURL URLWithString:urlString]];
        [request2 setHTTPMethod:@"PUT"];
        [request2 setAllHTTPHeaderFields:[request allHTTPHeaderFields]];
        NSURLSessionDataTask *task = [self uploadTaskWithRequest:request2
                                                fromFile:fromFile
                                                progress:nil 
                                       completionHandler:completionHandler];
        return task;
    }

    @end    

另一个好的资源是苹果示例代码这里,查找"简单后台传输"。


0

最近,亚马逊已经更新了他们的AWS API到2.2.4版本。 这次更新的特点是支持后台上传,您不必使用NSURLSession来上传视频,非常简单,您可以使用以下源代码块进行测试,我已经与我的旧版本进行了测试,比以前的版本快30-40%

在AppDelegate.m didFinishLaunchingWithOptions方法中 // ~GM~ 设置Cognito为AWS V2配置

AWSStaticCredentialsProvider *staticProvider = [[AWSStaticCredentialsProvider alloc] initWithAccessKey:@"xxxx secretKey:@"xxxx"];  

AWSServiceConfiguration *configuration = [[AWSServiceConfiguration alloc] initWithRegion:AWSRegionUSWest2                                                                 credentialsProvider:staticProvider];

AWSServiceManager.defaultServiceManager.defaultServiceConfiguration = configuration;

在 handleEventsForBackgroundURLSession 方法中

[AWSS3TransferUtility interceptApplication:application
       handleEventsForBackgroundURLSession:identifier
                         completionHandler:completionHandler];

在上传类中

NSURL *fileURL = // The file to upload.

AWSS3TransferUtilityUploadExpression *expression = [AWSS3TransferUtilityUploadExpression new];
expression.uploadProgress = ^(AWSS3TransferUtilityTask *task, int64_t bytesSent, int64_t totalBytesSent, int64_t totalBytesExpectedToSend) {
    dispatch_async(dispatch_get_main_queue(), ^{
        // Do something e.g. Update a progress bar.
    });
};

AWSS3TransferUtilityUploadCompletionHandlerBlock completionHandler = ^(AWSS3TransferUtilityUploadTask *task, NSError *error) {
    dispatch_async(dispatch_get_main_queue(), ^{
        // Do something e.g. Alert a user for transfer completion.
        // On failed uploads, `error` contains the error object.
    });
};

AWSS3TransferUtility *transferUtility = [AWSS3TransferUtility defaultS3TransferUtility];
[[transferUtility uploadFile:fileURL
                      bucket:@"YourBucketName"
                         key:@"YourObjectKeyName"
                 contentType:@"text/plain"
                  expression:expression
            completionHander:completionHandler] continueWithBlock:^id(AWSTask *task) {
    if (task.error) {
        NSLog(@"Error: %@", task.error);
    }
    if (task.exception) {
        NSLog(@"Exception: %@", task.exception);
    }
    if (task.result) {
        AWSS3TransferUtilityUploadTask *uploadTask = task.result;
        // Do something with uploadTask.
    }

    return nil;
}];

更多参考资料:https://aws.amazon.com/blogs/mobile/amazon-s3-transfer-utility-for-ios/


0

我已经更新了@melvinmt的答案到Swift 5。希望它能帮助到某些人!


import Foundation
import AWSS3

class S3BackgroundUpload : NSObject {

    // Swift doesn't support static properties yet, so have to use structs to achieve the same thing.
    struct Static {
        static var session : URLSession?
    }

    override init() {
        super.init()

        // Note: There are probably safer ways to store the AWS credentials.
        let configPath = Bundle.main.path(forResource: "appconfig", ofType: "plist")
        let config = NSDictionary(contentsOfFile: configPath!)
        let accessKey = config?.object(forKey: "awsAccessKeyId") as? String
        let secretKey = config?.object(forKey: "awsSecretAccessKey") as? String?
        let provider = AWSStaticCredentialsProvider(accessKey: accessKey!, secretKey: secretKey!!)

        // AWSRegionType.USEast1 is the default S3 endpoint (use it if you don't need specific endpoints such as s3-us-west-2.amazonaws.com)
        let configuration = AWSServiceConfiguration(region: AWSRegionType.USEast1, credentialsProvider: provider)

        // This is setting the configuration for all AWS services, you can also pass in this configuration to the AWSS3PreSignedURLBuilder directly.
        AWSServiceManager.default().defaultServiceConfiguration = configuration

        if Static.session == nil {
            let configIdentifier = "com.example.s3-background-upload"

            var config : URLSessionConfiguration
            if URLSessionConfiguration.responds(to: "backgroundSessionConfigurationWithIdentifier:") {
                // iOS8
                config = URLSessionConfiguration.background(withIdentifier: configIdentifier)
            } else {
                // iOS7
                config = URLSessionConfiguration.backgroundSessionConfiguration(configIdentifier)
            }

            // NSURLSession background sessions *need* to have a delegate.
            Static.session = Foundation.URLSession(configuration: config, delegate: self, delegateQueue: nil)
        }
    }

    func upload() {
        let s3path = "/some/path/some_file.jpg"
        let filePath = "/var/etc/etc/some_file.jpg"

        // Check if the file actually exists to prevent weird uncaught obj-c exceptions.
        if FileManager.default.fileExists(atPath: filePath) == false {
            NSLog("file does not exist at %@", filePath)
            return
        }

        // NSURLSession needs the filepath in a "file://" NSURL format.
        let fileUrl = NSURL(string: "file://\(filePath)")

        let preSignedReq = AWSS3GetPreSignedURLRequest()
        preSignedReq.bucket = "bucket-name"
        preSignedReq.key = s3path
        preSignedReq.httpMethod = AWSHTTPMethod.PUT                   // required
        preSignedReq.contentType = "image/jpeg"                       // required
        preSignedReq.expires = Date(timeIntervalSinceNow: 60*60)    // required

        // The defaultS3PreSignedURLBuilder uses the global config, as specified in the init method.
        let urlBuilder = AWSS3PreSignedURLBuilder.default()

        // The new AWS SDK uses BFTasks to chain requests together:
        urlBuilder.getPreSignedURL(preSignedReq).continueWith { (task) -> AnyObject? in

            if task.error != nil {
                print("getPreSignedURL error: %@", task.error)
                return nil
            }

            var preSignedUrl = task.result as! URL
            print("preSignedUrl: %@", preSignedUrl)

            var request = URLRequest(url: preSignedUrl)
            request.cachePolicy = .reloadIgnoringLocalCacheData

            // Make sure the content-type and http method are the same as in preSignedReq
            request.httpMethod = "PUT"
            request.setValue(preSignedReq.contentType, forHTTPHeaderField: "Content-Type")

            // NSURLSession background session does *not* support completionHandler, so don't set it.
            let uploadTask = Static.session?.uploadTask(with: request, fromFile: fileUrl! as URL)

            // Start the upload task:
            uploadTask?.resume()

            return nil
        }
    }
}

extension S3BackgroundUpload : URLSessionDelegate {

    func URLSession(session: URLSession, dataTask: URLSessionDataTask, didReceiveData data: Data) {
        print("did receive data: %@", String(data: data, encoding: .utf8))
    }

    func URLSession(session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
        print("session did complete")
        if error != nil {
            print("error: %@", error!.localizedDescription)
        }
        // Finish up your post-upload tasks.
    }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接