George Green
George Green

Reputation: 4905

NSURLSession and amazon S3 uploads

I have an app which is currently uploading images to amazon S3. I have been trying to switch it from using NSURLConnection to NSURLSession so that the uploads can continue while the app is in the background! I seem to be hitting a bit of an issue. The NSURLRequest is created and passed to the NSURLSession but amazon sends back a 403 - forbidden response, if I pass the same request to a NSURLConnection it uploads the file perfectly.

Here is the code that creates the response:

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];

And then this signs the response (I think this came from another SO answer):

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"];

Then if I use this line of code:

[NSURLConnection connectionWithRequest:request delegate:self];

It works and uploads the file, but if I use:

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

I get the forbidden error..!?

Has anyone tried uploading to S3 with this and hit similar issues? I wonder if it is to do with the way the session pauses and resumes uploads, or it is doing something funny to the request..?

One possible solution would be to upload the file to an interim server that I control and have that forward it to S3 when it is complete... but this is clearly not an ideal solution!

Any help is much appreciated!!

Thanks!

Upvotes: 14

Views: 11362

Answers (8)

Jmz
Jmz

Reputation: 169

I have updated @melvinmt 's answer to Swift 5. I hope it helps someone!!


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

Upvotes: 0

Mr.G
Mr.G

Reputation: 1275

Recently Amazon has updated there AWS api to 2.2.4. speciality of this update is that, it supports background uploading, you don't have to use NSURLSession to upload videos its pretty simple, you can use following source block to test it, I have tested against with my older version, it is 30 - 40 % faster than the previous version

in AppDelegate.m didFinishLaunchingWithOptions method // ~GM~ setup cognito for AWS V2 configurations

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

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

AWSServiceManager.defaultServiceManager.defaultServiceConfiguration = configuration;

in handleEventsForBackgroundURLSession method

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

in upload class

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;
}];

More references: https://aws.amazon.com/blogs/mobile/amazon-s3-transfer-utility-for-ios/

Upvotes: 0

hash3r
hash3r

Reputation: 411

For background uploading/downloading you need to use NSURLSession with background configuration. Since AWS SDK 2.0.7 you can use pre signed requests:

PreSigned URL Builder** - The SDK now includes support for pre-signed Amazon Simple Storage Service (S3) URLs. You can use these URLS to perform background transfers using the NSURLSession class.

Init background NSURLSession and AWS Services

- (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];
}

Implement upload file function

- (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 Delegate Function:

- (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;
        }];
    }
}

Upvotes: 2

melvinmt
melvinmt

Reputation: 161

The answers here are slightly outdated, spent a great deal of my day trying to get this work in Swift and the new AWS SDK. So here's how to do it in Swift by using the new AWSS3PreSignedURLBuilder (available in version 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.
    }
}

Upvotes: 7

Genady Okrain
Genady Okrain

Reputation: 203

Here is my code to run the task:

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];

I open sourced my S3 background uploaded https://github.com/genadyo/S3Uploader/

Upvotes: 2

Marius Jeskulke
Marius Jeskulke

Reputation: 233

I made it work based on Zeev Vax answer. I want to provide some insight on problems I ran into and offer minor improvements.

Build a normal PutRequest, for instance

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

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

Now we need to do some work the S3Client usually does for us

// 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];

Now copy all of that to a new request. Amazon use their own NSUrlRequest class which would cause an exception

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

Now we can start the actual transfer

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

This is the code that creates the background session:

- (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;
}

It took me a while to figure out that the session / task delegate needs to handle an auth challenge (we are in fact authentication to s3). So just implement

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

Upvotes: 8

Zeev Vax
Zeev Vax

Reputation: 924

I just spent sometime on that, and finally succeeded. The best way is to use AWS library to create the request with the signed headers and than copy the request. It is critical to copy the request since NSURLSessionTask would fail other wise. In the code example below I used AFNetworking and sub-classed AFHTTPSessionManager, but this code also works with 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    

Another good resource is the apple sample code hereand look for "Simple Background Transfer"

Upvotes: 1

Stefan Arentz
Stefan Arentz

Reputation: 34945

I don't know NSURLSessionUploadTask very well yet but I can tell you how I would debug this.

I would use a tool like Charles to be able to see HTTP(S) requests that my application makes. The problem is likely that the NSURLSessionUploadTask ignores a header that you set or it uses a different HTTP method than Amazon's S3 expects for the file upload. This can be easily verified with an intercepting proxy.

Also, when Amazon S3 returns an error like 403, it actually sends back an XML document that has some more information about the error. Maybe there is a delegate method for NSURLSession that can retrieve the response body? If not then Charles will certainly give you more insight.

Upvotes: 3

Related Questions