2013-10-20 310 views
13

我有一個應用程序,目前正在上傳圖像到亞馬遜S3。我一直試圖將它從使用NSURLConnection切換到NSURLSession,以便在應用程序處於後臺時可以繼續上傳!我似乎遇到了一些問題。創建NSURLRequest並將其傳遞給NSURLSession,但如果我將相同的請求傳遞給NSURLConnection,則它會完美地上傳文件,而亞馬遜會發回403禁止的響應。NSURLSession和亞馬遜S3上傳

下面是創建響應代碼:

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它完成時臨時服務器...但是這顯然不是一個理想的解決方案!

任何幫助非常感謝!

謝謝!

+0

@ GeogeGreen我需要上傳大型視頻到s3存儲桶,最有可能是5GB,我可以用NSURLSession做嗎?我讀過的是後臺會話不會長時間執行 –

回答

2

我不知道NSURLSessionUploadTask非常好,但是我可以告訴你我會怎麼調試此。

我會用一個工具,如Charles能夠看到我的應用程序進行HTTP(S)請求。問題很可能是NSURLSessionUploadTask忽略了您設置的標頭,或者它使用與Amazon S3的文件上傳不同的HTTP方法。這可以通過攔截代理輕鬆驗證。

此外,當亞馬遜S3返回類似403的錯誤,它實際上發回了有關該錯誤的一些信息的XML文檔。 NSURLSession也許有一個委託方法可以檢索響應體?如果沒有,查爾斯一定會給你更多的見解。

+0

真棒,真的有幫助。在簽署請求後,Apple正在添加額外的標題字段! –

+0

@GeorgeGreen你能提供一些更多的信息嗎?你最終如何克服了這個問題? – Stavash

+0

@GeorgeGreen我也很感興趣。 – Andy

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  

另一個很好的資源是蘋果的示例代碼here並查找「簡單的背景轉移」

8

我做它的基礎上茲夫的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); 
} 
+0

謝謝。這很有效 – honcheng

+1

你會如何使用預先驗證的身份驗證來做到這一點?客戶端只有accessKey和簽名,這假定你有密鑰和祕密。 –

+0

我實際上使用FederationToken來生成臨時憑證(http://docs.aws.amazon.com/STS/latest/APIReference/API_GetFederationToken.html)。我從來沒有使用預先簽署的網址,但據我瞭解,這是一個簡單的URL請求到生成的網址。無需爲此使用AWS軟件開發工具包方法,只需創建NSUrlSessionUploadTask,而無需任何AWS集成。 –

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/

+0

鏈接只有答案是不鼓勵的。請在答案中包含您的解決方案的重要方面,或者刪除此答案並發表評論。 – Rob

+0

@Genady無論如何,我可以恢復上傳,如果互聯網連接下降? –

+0

有趣的問題,我還沒有嘗試過。 –

5

的答案在這裏有些過時,花費了大量我的一天試圖在Swift和新的AWS SDK中完成這項工作。因此,這裏是如何使用新AWSS3PreSignedURLBuilder(在版本2.0.7+)做在斯威夫特:

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. 
    } 
} 
+0

我想使用V2 API將視頻上傳到S3存儲桶,它應該支持暫停和恢復功能,可以使用此代碼片段的objc版本上傳 –

2

對於後臺上傳/下載您需要使用NSURLSession與後臺配置。 由於AWS SDK 2.0.7您可以使用預先簽署的請求:

PreSigned網址構建器** - 該SDK現在包括預簽約 亞馬遜簡單存儲服務(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; 
     }]; 
    } 
} 
0

最近亞馬遜已經有更新AWS API 2.2 0.4。 這個更新的特色是,它支持後臺上傳,你不必使用NSURLSession來上傳視頻其相當簡單,你可以使用下面的源代碼塊來測試它,我已經測試了與我的舊版本,它比AppDelegate中的先前版本更快30-40%

。米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/