Uploading to Amazon S3 over 3G using the AWS SDK for iOS

Articles & Tutorials>SDKs>iOS>Uploading to Amazon S3 over 3G using the AWS SDK for iOS
Uploading to Amazon S3 over 3G using the AWS SDK for iOS

Details

Submitted By: Glenn@AWS
AWS Products Used: AWS SDK for iOS, Amazon S3
Created On: May 23, 2012 6:09 PM GMT
Last Updated: September 25, 2014 8:19 PM GMT
Version 2 of the AWS Mobile SDK
  • This article and sample apply to Version 1 of the AWS Mobile SDK. If you are building new apps, we recommend you use Version 2. For details, please visit the AWS Mobile SDK page.
  • This content is being maintained for historical reference.

Uploading to Amazon S3 over 3G using the AWS SDK for iOS

Uploading large files to Amazon S3 from iOS devices over 3G can be error prone. Apple fixed this issue in iOS 6, but even with the fix, the problem will persist on older devices and versions of iOS. This article provides sample code that you can use with the AWS SDK for iOS to work around this issue. A link to the AWS SDK for iOS is available at the end of this article.

To use the sample code, you'll first need to obtain your own AWS credentials: an Access Key ID and Secret Key. If you haven't already signed up for Amazon Web Services (AWS), you will need to do that first to get your AWS credentials. You can sign up for AWS here. After you sign up, you can retrieve your credentials on this page.

Overview

Prior to iOS 6, uploading large files to Amazon S3 over 3G is problematic. However, our testing has shown that throttling the upload significantly improves the success rate. Also, smaller objects tend to upload with greater success than larger ones. The following code demonstrates how to throttle an upload to Amazon S3 using the AWS SDK for iOS. The code also demonstrates two methods for upload, a single put request and a multipart upload request. For large uploads, you should use a multipart upload request.

Checking for WiFi

First, we determine if the device is connected to 3G or WiFi. To do this, we utilize Apple's Reachability sample code. This code allows you to quickly determine if you are connected to a WiFi network or not. Apple's reachability sample can be found here. After you include the Reachability.m and Reachability.h files into your project, you can use the Reachability functionality. The following method returns true if you are on a WiFi network.

-(BOOL)isWifiAvailable 
{
    Reachability *r = [Reachability reachabilityForLocalWiFi];
    return !( [r currentReachabilityStatus] == NotReachable); 
}

Single Put Object Request

In this section we show how to upload an object to Amazon S3 in a single request. For objects larger than 5MB, we recommend using the multipart upload described in the next section. The code initially creates an instance of the AmazonS3Client class and then creates an Amazon S3 bucket where the object will be stored. As part of creating the S3PutObjectRequest, the code creates a specialized input stream, S3UploadInputStream, that allows the stream to be throttled. This type of stream enables you to control the packet size and the delay between packets while streaming. By default, the settings for S3UploadInputStream are equivalent to the standard NSInputStream: no delay and a 32K packet size. Adding a delay and reducing the packet size acts as a throttle and reduces the chance of an error during transmission over 3G. For optimal performance, the code first determines if the network is WiFi or not. The code uses a modified S3UploadInputStream only if a 3G network is being used.

-(void)upload:(NSData*)dataToUpload inBucket:(NSString*)bucket forKey:(NSString*)key
{
    bool using3G = ![self isWifiAvailable]; 

    @try {	
        AmazonS3Client *s3 = [[[AmazonS3Client alloc] initWithAccessKey:ACCESS_KEY withSecretKey:SECRET_KEY] autorelease];
        S3CreateBucketRequest *cbr = [[[S3CreateBucketRequest alloc] initWithName:bucket] autorelease];		
        [s3 createBucket:cbr];
				
        S3PutObjectRequest *por = [[[S3PutObjectRequest alloc] initWithKey:key inBucket:bucket] autorelease];        

        // The S3UploadInputStream was deprecated after the release of iOS6.
        S3UploadInputStream *stream = [S3UploadInputStream inputStreamWithData:dataToUpload];  
        if ( using3G ) {
            // If connected via 3G "throttle" the stream.
            stream.delay = 0.2; // In seconds
            stream.packetSize = 16; // Number of 1K blocks
        }
        
        por.contentLength = [dataToUpload length];
        por.stream = stream;
        
        [s3 putObject:por];
    }
    @catch ( AmazonServiceException *exception ) {
        NSLog( @"Upload Failed, Reason: %@", exception );
    }	
}

Multipart Upload

Here we demonstrate how to upload an object to Amazon S3 using a multipart upload. To do this, we need to break up the original data into parts, which requires two utility functions. The countParts method determines the number of 5MB parts needed to be sent to Amazon S3. The smallest part size allowed by Amazon S3 is 5MB. The getPart method returns the NSData for the specified part from the larger object. The multipartUpload method uses these two functions to iterate through the larger data object and send each part to Amazon S3 in series. A check for 3G is made and if 3G is present, the S3UploadInputStream is modified in order to throttle the upload as described in the previous section.

const int PART_SIZE = (5 * 1024 * 1024); // 5MB is the smallest part size allowed for a multipart upload. (Only the last part can be smaller.)

-(void)multipartUpload:(NSData*)dataToUpload inBucket:(NSString*)bucket forKey:(NSString*)key
{
    bool using3G = ![self isWifiAvailable];
    
    AmazonS3Client *s3 = [[[AmazonS3Client alloc] initWithAccessKey:ACCESS_KEY withSecretKey:SECRET_KEY] autorelease];
    @try {
        [s3 createBucketWithName:bucket];
        
        S3InitiateMultipartUploadRequest *initReq = [[[S3InitiateMultipartUploadRequest alloc] initWithKey:key inBucket:bucket] autorelease];
        S3MultipartUpload *upload = [s3 initiateMultipartUpload:initReq].multipartUpload;
        S3CompleteMultipartUploadRequest *compReq = [[[S3CompleteMultipartUploadRequest alloc] initWithMultipartUpload:upload] autorelease];        
        
        int numberOfParts = [self countParts:dataToUpload];        
        for ( int part = 0; part < numberOfParts; part++ ) {
            NSData *dataForPart = [self getPart:part fromData:dataToUpload];

            // The S3UploadInputStream was deprecated after the release of iOS6.
            S3UploadInputStream *stream = [S3UploadInputStream inputStreamWithData:dataForPart];        
            if ( using3G ) {
                // If connected via 3G "throttle" the stream.
                stream.delay = 0.2; // In seconds
                stream.packetSize = 16; // Number of 1K blocks
            }

            S3UploadPartRequest *upReq = [[S3UploadPartRequest alloc] initWithMultipartUpload:upload];
            upReq.partNumber = ( part + 1 );
            upReq.contentLength = [dataForPart length];
            upReq.stream = stream;
            
            S3UploadPartResponse *response = [s3 uploadPart:upReq];            
            [compReq addPartWithPartNumber:( part + 1 ) withETag:response.etag];            
        }
        
        [s3 completeMultipartUpload:compReq];
    }
    @catch ( AmazonServiceException *exception ) {
        NSLog( @"Multipart Upload Failed, Reason: %@", exception  );
    }	
}

-(NSData*)getPart:(int)part fromData:(NSData*)fullData 
{
    NSRange range;
    range.length = PART_SIZE;
    range.location = part * PART_SIZE;    
    
    int maxByte = (part + 1) * PART_SIZE;
    if ( [fullData length] < maxByte ) {
        range.length = [fullData length] - range.location;
    }
    
    return [fullData subdataWithRange:range];
}

-(int)countParts:(NSData*)fullData 
{
    int q = (int)([fullData length] / PART_SIZE);
    int r = (int)([fullData length] % PART_SIZE);
    
    return ( r == 0 ) ? q : q + 1;
}

Conclusion and Additional Resources

The code in this article demonstrates a workaround for the issues that some iOS users experience when uploading large objects to Amazon S3 over 3G. Here are some additional items to consider when using the above code:

  • If the objects to be uploaded to Amazon S3 are larger then 5MB, we recommend using multipart upload as it has better success in our testing.
  • You might want to add some additional error handling logic to our code if you use it in a production environment. For example, add retry logic for part failures during multipart upload.
  • The 0.2 second delay and 16K packet size worked well in our testing; you may experience different results and may need to adjust these settings accordingly.
  • The sample presented above uses blocking calls. Using asynchronous requests may provide better application responsiveness. The AWS SDK for iOS has samples that demonstrate how to make the requests in an asynchronous fashion.

You can download the AWS SDK for iOS using the following link:

Want to learn more?

To learn about mobile development best practices, follow our AWS Mobile Development Blog. You can also ask questions or post comments in the Mobile Development Forum about this or any other topic related to mobile development with AWS.

©2014, Amazon Web Services, Inc. or its affiliates. All rights reserved.