Resumable download lets you continue an interrupted download from where it left off. This feature saves time and network traffic.
Process
For example, if you are downloading a video on your phone and the network switches from Wi-Fi to a mobile network, the app interrupts the download by default. If you enable resumable download, the download resumes from where it left off when you switch back to Wi-Fi.
The following figure illustrates how a resumable download works.
The following figure shows the process of a resumable download.
Features
HTTP 1.1 supports the Range header. This header specifies the range of data to retrieve. The Range header supports the following formats.
Timestamp range
Description
Range: bytes=100-
Transfers data from byte 101 to the last byte.
Range: bytes=100-200
Transfers data from byte 101 to byte 201. This is often used for chunked transfer of large files, such as videos.
Range: bytes=-100
Transfers the last 100 bytes of the content, not the first 100 bytes.
Range: bytes=0-100, 200-300
Specifies multiple content ranges at the same time.
For a resumable download, you can use the
If-Matchheader to check whether the file on the server has changed. The value ofIf-Matchcorresponds to the file'sETagvalue.When a client sends a request, include the
RangeandIf-Matchheaders. The OSS server receives the request and validates the ETag value in the If-Match header. If the values do not match, the server returns a 412 Precondition Failed status code.The OSS server supports
Range,If-Match,If-None-Match,If-Modified-Since, andIf-Unmodified-Sincefor GetObject operations. This lets you implement resumable downloads for OSS resources on mobile devices.
Sample code
The OSS SDK for iOS does not natively support resumable downloads. The following code is provided for reference only to help you understand the download process. Do not use this code in production projects. To implement resumable downloads, you must write your own code or use a third-party open source download framework.
The following sample code demonstrates how to perform a resumable download in iOS:
#import "DownloadService.h"
#import "OSSTestMacros.h"
@implementation DownloadRequest
@end
@implementation Checkpoint
- (instancetype)copyWithZone:(NSZone *)zone {
Checkpoint *other = [[[self class] allocWithZone:zone] init];
other.etag = self.etag;
other.totalExpectedLength = self.totalExpectedLength;
return other;
}
@end
@interface DownloadService()<NSURLSessionTaskDelegate, NSURLSessionDataDelegate>
@property (nonatomic, strong) NSURLSession *session; // Network session.
@property (nonatomic, strong) NSURLSessionDataTask *dataTask; // Data request task.
@property (nonatomic, copy) DownloadFailureBlock failure; // Request error.
@property (nonatomic, copy) DownloadSuccessBlock success; // Request success.
@property (nonatomic, copy) DownloadProgressBlock progress; // Download progress.
@property (nonatomic, copy) Checkpoint *checkpoint; // Checkpoint.
@property (nonatomic, copy) NSString *requestURLString; // File resource address for download requests.
@property (nonatomic, copy) NSString *headURLString; // File resource address for HEAD requests.
@property (nonatomic, copy) NSString *targetPath; // File storage path.
@property (nonatomic, assign) unsigned long long totalReceivedContentLength; // Size of the downloaded file.
@property (nonatomic, strong) dispatch_semaphore_t semaphore;
@end
@implementation DownloadService
- (instancetype)init
{
self = [super init];
if (self) {
NSURLSessionConfiguration *conf = [NSURLSessionConfiguration defaultSessionConfiguration];
conf.timeoutIntervalForRequest = 15;
NSOperationQueue *processQueue = [NSOperationQueue new];
_session = [NSURLSession sessionWithConfiguration:conf delegate:self delegateQueue:processQueue];
_semaphore = dispatch_semaphore_create(0);
_checkpoint = [[Checkpoint alloc] init];
}
return self;
}
// DownloadRequest is the core of the download logic.
+ (instancetype)downloadServiceWithRequest:(DownloadRequest *)request {
DownloadService *service = [[DownloadService alloc] init];
if (service) {
service.failure = request.failure;
service.success = request.success;
service.requestURLString = request.sourceURLString;
service.headURLString = request.headURLString;
service.targetPath = request.downloadFilePath;
service.progress = request.downloadProgress;
if (request.checkpoint) {
service.checkpoint = request.checkpoint;
}
}
return service;
}
/**
* Get file information using the HEAD method. OSS compares the ETag of the file with the ETag saved in the local checkpoint and returns the comparison result.
*/
- (BOOL)getFileInfo {
__block BOOL resumable = NO;
NSURL *url = [NSURL URLWithString:self.headURLString];
NSMutableURLRequest *request = [[NSMutableURLRequest alloc]initWithURL:url];
[request setHTTPMethod:@"HEAD"];
// Process object information. For example, ETag is used for prechecks in resumable downloads, and content-length is used to calculate the download progress.
NSURLSessionDataTask *task = [[NSURLSession sharedSession] dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
if (error) {
NSLog(@"Failed to get file metadata. Error: %@", error);
} else {
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response;
NSString *etag = [httpResponse.allHeaderFields objectForKey:@"Etag"];
if ([self.checkpoint.etag isEqualToString:etag]) {
resumable = YES;
} else {
resumable = NO;
}
}
dispatch_semaphore_signal(self.semaphore);
}];
[task resume];
dispatch_semaphore_wait(self.semaphore, DISPATCH_TIME_FOREVER);
return resumable;
}
/**
* Get the size of the local file.
*/
- (unsigned long long)fileSizeAtPath:(NSString *)filePath {
unsigned long long fileSize = 0;
NSFileManager *dfm = [NSFileManager defaultManager];
if ([dfm fileExistsAtPath:filePath]) {
NSError *error = nil;
NSDictionary *attributes = [dfm attributesOfItemAtPath:filePath error:&error];
if (!error && attributes) {
fileSize = attributes.fileSize;
} else if (error) {
NSLog(@"error: %@", error);
}
}
return fileSize;
}
- (void)resume {
NSURL *url = [NSURL URLWithString:self.requestURLString];
NSMutableURLRequest *request = [[NSMutableURLRequest alloc]initWithURL:url];
[request setHTTPMethod:@"GET"];
BOOL resumable = [self getFileInfo]; // If resumable returns NO, the conditions for a resumable download are not met.
if (resumable) {
self.totalReceivedContentLength = [self fileSizeAtPath:self.targetPath];
NSString *requestRange = [NSString stringWithFormat:@"bytes=%llu-", self.totalReceivedContentLength];
[request setValue:requestRange forHTTPHeaderField:@"Range"];
} else {
self.totalReceivedContentLength = 0;
}
if (self.totalReceivedContentLength == 0) {
[[NSFileManager defaultManager] createFileAtPath:self.targetPath contents:nil attributes:nil];
}
self.dataTask = [self.session dataTaskWithRequest:request];
[self.dataTask resume];
}
- (void)pause {
[self.dataTask cancel];
self.dataTask = nil;
}
- (void)cancel {
[self.dataTask cancel];
self.dataTask = nil;
[self removeFileAtPath: self.targetPath];
}
- (void)removeFileAtPath:(NSString *)filePath {
NSError *error = nil;
[[NSFileManager defaultManager] removeItemAtPath:self.targetPath error:&error];
if (error) {
NSLog(@"remove file with error : %@", error);
}
}
#pragma mark - NSURLSessionDataDelegate
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task
// Check whether the download task is complete and return the result to the upper-layer service.
didCompleteWithError:(nullable NSError *)error {
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)task.response;
if ([httpResponse isKindOfClass:[NSHTTPURLResponse class]]) {
if (httpResponse.statusCode == 200) {
self.checkpoint.etag = [[httpResponse allHeaderFields] objectForKey:@"Etag"];
self.checkpoint.totalExpectedLength = httpResponse.expectedContentLength;
} else if (httpResponse.statusCode == 206) {
self.checkpoint.etag = [[httpResponse allHeaderFields] objectForKey:@"Etag"];
self.checkpoint.totalExpectedLength = self.totalReceivedContentLength + httpResponse.expectedContentLength;
}
}
if (error) {
if (self.failure) {
NSMutableDictionary *userInfo = [NSMutableDictionary dictionaryWithDictionary:error.userInfo];
[userInfo oss_setObject:self.checkpoint forKey:@"checkpoint"];
NSError *tError = [NSError errorWithDomain:error.domain code:error.code userInfo:userInfo];
self.failure(tError);
}
} else if (self.success) {
self.success(@{@"status": @"success"});
}
}
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveResponse:(NSURLResponse *)response completionHandler:(void (^)(NSURLSessionResponseDisposition))completionHandler
{
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)dataTask.response;
if ([httpResponse isKindOfClass:[NSHTTPURLResponse class]]) {
if (httpResponse.statusCode == 200) {
self.checkpoint.totalExpectedLength = httpResponse.expectedContentLength;
} else if (httpResponse.statusCode == 206) {
self.checkpoint.totalExpectedLength = self.totalReceivedContentLength + httpResponse.expectedContentLength;
}
}
completionHandler(NSURLSessionResponseAllow);
}
// Write the received network data to the file in append mode and update the download progress.
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data {
NSFileHandle *fileHandle = [NSFileHandle fileHandleForWritingAtPath:self.targetPath];
[fileHandle seekToEndOfFile];
[fileHandle writeData:data];
[fileHandle closeFile];
self.totalReceivedContentLength += data.length;
if (self.progress) {
self.progress(data.length, self.totalReceivedContentLength, self.checkpoint.totalExpectedLength);
}
}
@endDownloadRequest definition
#import <Foundation/Foundation.h> typedef void(^DownloadProgressBlock)(int64_t bytesReceived, int64_t totalBytesReceived, int64_t totalBytesExpectToReceived); typedef void(^DownloadFailureBlock)(NSError *error); typedef void(^DownloadSuccessBlock)(NSDictionary *result); @interface Checkpoint : NSObject<NSCopying> @property (nonatomic, copy) NSString *etag; // The ETag value of the resource. @property (nonatomic, assign) unsigned long long totalExpectedLength; // The total size of the file. @end @interface DownloadRequest : NSObject @property (nonatomic, copy) NSString *sourceURLString; // The download URL. @property (nonatomic, copy) NSString *headURLString; // The URL to get file metadata. @property (nonatomic, copy) NSString *downloadFilePath; // The local storage path of the file. @property (nonatomic, copy) DownloadProgressBlock downloadProgress; // The download progress. @property (nonatomic, copy) DownloadFailureBlock failure; // The callback for a failed download. @property (nonatomic, copy) DownloadSuccessBlock success; // The callback for a successful download. @property (nonatomic, copy) Checkpoint *checkpoint; // Stores the ETag of the file. @end @interface DownloadService : NSObject + (instancetype)downloadServiceWithRequest:(DownloadRequest *)request; /** * Start the download. */ - (void)resume; /** * Pause the download. */ - (void)pause; /** * Cancel the download. */ - (void)cancel; @endUpper-layer service call
- (void)initDownloadURLs { OSSPlainTextAKSKPairCredentialProvider *pCredential = [[OSSPlainTextAKSKPairCredentialProvider alloc] initWithPlainTextAccessKey:OSS_ACCESSKEY_ID secretKey:OSS_SECRETKEY_ID]; _mClient = [[OSSClient alloc] initWithEndpoint:OSS_ENDPOINT credentialProvider:pCredential]; // Generate a signed URL for GET requests. OSSTask *downloadURLTask = [_mClient presignConstrainURLWithBucketName:@"aliyun-dhc-shanghai" withObjectKey:OSS_DOWNLOAD_FILE_NAME withExpirationInterval:1800]; [downloadURLTask waitUntilFinished]; _downloadURLString = downloadURLTask.result; // Generate a signed URL for HEAD requests. OSSTask *headURLTask = [_mClient presignConstrainURLWithBucketName:@"aliyun-dhc-shanghai" withObjectKey:OSS_DOWNLOAD_FILE_NAME httpMethod:@"HEAD" withExpirationInterval:1800 withParameters:nil]; [headURLTask waitUntilFinished]; _headURLString = headURLTask.result; } - (IBAction)resumeDownloadClicked:(id)sender { _downloadRequest = [DownloadRequest new]; _downloadRequest.sourceURLString = _downloadURLString; // Set the URL of the resource. _downloadRequest.headURLString = _headURLString; NSString *documentPath = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject; _downloadRequest.downloadFilePath = [documentPath stringByAppendingPathComponent:OSS_DOWNLOAD_FILE_NAME]; // Set the local path to save the downloaded file. __weak typeof(self) wSelf = self; _downloadRequest.downloadProgress = ^(int64_t bytesReceived, int64_t totalBytesReceived, int64_t totalBytesExpectToReceived) { // totalBytesReceived indicates the number of bytes cached on the client. totalBytesExpectToReceived indicates the total number of bytes to be downloaded. dispatch_async(dispatch_get_main_queue(), ^{ __strong typeof(self) sSelf = wSelf; CGFloat fProgress = totalBytesReceived * 1.f / totalBytesExpectToReceived; sSelf.progressLab.text = [NSString stringWithFormat:@"%.2f%%", fProgress * 100]; sSelf.progressBar.progress = fProgress; }); }; _downloadRequest.failure = ^(NSError *error) { __strong typeof(self) sSelf = wSelf; sSelf.checkpoint = error.userInfo[@"checkpoint"]; }; _downloadRequest.success = ^(NSDictionary *result) { NSLog(@"Download successful"); }; _downloadRequest.checkpoint = self.checkpoint; NSString *titleText = [[_downloadButton titleLabel] text]; if ([titleText isEqualToString:@"download"]) { [_downloadButton setTitle:@"pause" forState: UIControlStateNormal]; _downloadService = [DownloadService downloadServiceWithRequest:_downloadRequest]; [_downloadService resume]; } else { [_downloadButton setTitle:@"download" forState: UIControlStateNormal]; [_downloadService pause]; } } - (IBAction)cancelDownloadClicked:(id)sender { [_downloadButton setTitle:@"download" forState: UIControlStateNormal]; [_downloadService cancel]; }
You can obtain the checkpoint from the failure callback when you pause or cancel a download. When you restart the download, you can pass the checkpoint to DownloadRequest. DownloadService then uses the checkpoint to perform a consistency check.