Geo*_*een 14 iphone cocoa-touch amazon-s3 ios nsurlsession
我有一个应用程序,目前正在上传图像到亚马逊S3.我一直在尝试将它从使用NSURLConnection切换到NSURLSession,以便在应用程序处于后台时上传可以继续!我似乎遇到了一些问题.创建NSURLRequest并将其传递给NSURLSession,但亚马逊发回403禁止响应,如果我将相同的请求传递给NSURLConnection,则它会完美地上传文件.
以下是创建响应的代码:
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];
Run Code Online (Sandbox Code Playgroud)
然后这标志着回应(我认为这来自另一个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"];
Run Code Online (Sandbox Code Playgroud)
然后,如果我使用这行代码:
[NSURLConnection connectionWithRequest:request delegate:self];
Run Code Online (Sandbox Code Playgroud)
它工作并上传文件,但如果我使用:
NSURLSessionUploadTask *task = [self.session uploadTaskWithRequest:request fromFile:[NSURL fileURLWithPath:filePath]];
[task resume];
Run Code Online (Sandbox Code Playgroud)
我得到了禁止的错误..!?
有没有人尝试使用此功能上传到S3并遇到类似问题?我想知道它是否与会话暂停和恢复上传的方式有关,或者是对请求做了一些有趣的事情......?
一种可能的解决方案是将文件上传到我控制的临时服务器,并在完成后将其转发到S3 ......但这显然不是理想的解决方案!
任何帮助深表感谢!!
谢谢!
我根据Zeev Vax的答案做了它的工作.我想对我遇到的问题提供一些见解并提供一些小改进.
例如,构建一个普通的PutRequest
S3PutObjectRequest* putRequest = [[S3PutObjectRequest alloc] initWithKey:keyName inBucket:bucketName];
putRequest.credentials = credentials;
putRequest.filename = theFilePath;
Run Code Online (Sandbox Code Playgroud)
现在我们需要做一些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];
Run Code Online (Sandbox Code Playgroud)
现在将所有这些复制到新请求中.亚马逊使用自己的NSUrlRequest类,这会导致异常
NSMutableURLRequest* request2 = [[NSMutableURLRequest alloc]initWithURL:request.URL];
[request2 setHTTPMethod:request.HTTPMethod];
[request2 setAllHTTPHeaderFields:[request allHTTPHeaderFields]];
Run Code Online (Sandbox Code Playgroud)
现在我们可以开始实际转移了
NSURLSession* backgroundSession = [self backgroundSession];
_uploadTask = [backgroundSession uploadTaskWithRequest:request2 fromFile:[NSURL fileURLWithPath:theFilePath]];
[_uploadTask resume];
Run Code Online (Sandbox Code Playgroud)
这是创建后台会话的代码:
- (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;
}
Run Code Online (Sandbox Code Playgroud)
我花了一段时间才弄清楚会话/任务委托需要处理auth挑战(我们实际上是对s3的身份验证).所以只需实施
- (void)URLSession:(NSURLSession *)session didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition, NSURLCredential *))completionHandler {
NSLog(@"session did receive challenge");
completionHandler(NSURLSessionAuthChallengePerformDefaultHandling, nil);
}
Run Code Online (Sandbox Code Playgroud)
这里的答案有点过时,我花了很多时间试图在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.
}
}
Run Code Online (Sandbox Code Playgroud)
我NSURLSessionUploadTask
还不太清楚,但我可以告诉你我将如何调试它。
我会使用Charles之类的工具来查看我的应用程序发出的 HTTP(S) 请求。问题可能是NSURLSessionUploadTask
忽略您设置的标头,或者它使用与 Amazon S3 预期的文件上传不同的 HTTP 方法。这可以通过拦截代理轻松验证。
此外,当 Amazon S3 返回 403 等错误时,它实际上会发回一个 XML 文档,其中包含有关该错误的更多信息。也许有一个委托方法NSURLSession
可以检索响应正文?如果没有,那么查尔斯肯定会给你更多的见解。
归档时间: |
|
查看次数: |
9565 次 |
最近记录: |