Implementing OSS Multipart Download for Mobile Devices

Technical Points

  1. Range: bytes=100- Start from the 101st byte until finished.
  2. Range: Bytes=100-200 Specify the length from the beginning to the end, remember that Range is counted from 0, so this requires the server to start from the 101st byte to the 201st byte. This is generally used for sharding a particularly large file, such as video.
  3. Range: Bytes=-100 If the range does not specify a starting position, it requires the server to transfer the last 100 bytes of content, rather than 100 bytes starting from byte 0.
  4. Range: bytes=0-100, 200-300 Multiple ranges of content can be specified at the same time, which is not common.

Best Practices

#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 failure
@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; //the URL of the file for download request
@property (nonatomic, copy) NSString *headURLString; //the URL of the file for head request
@property (nonatomic, copy) NSString *targetPath; //the path of the file
@property (nonatomic, assign) unsigned long long totalReceivedContentLength; //the length of the downloaded content
@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;
}
+ (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;
}
/**
* head file information, the etag of the extracted file is compared with the etag saved in the local checkpoint, and the result is returned
*/
- (BOOL)getFileInfo {
__block BOOL resumable = NO;
NSURL *url = [NSURL URLWithString:self.headURLString];
NSMutableURLRequest *request = [[NSMutableURLRequest alloc]initWithURL:url];
[request setHTTPMethod:@"HEAD"];

NSURLSessionDataTask *task = [[NSURLSession sharedSession] dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
if (error) {
cNSLog(@"Failed to obtain 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;
}
/**
* for obtaining 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 the resumable is NO, the download cannot be resumed, otherwise go to the resume logic.
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
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);
}
- (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);
}
}
@end
#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; // etag value of the resource
@property (nonatomic, assign) unsigned long long totalExpectedLength; //total length of the file
@end@interface DownloadRequest : NSObject@property (nonatomic, copy) NSString *sourceURLString; // the URL for download@property (nonatomic, copy) NSString *headURLString; // the URL for obtaining the file source@property (nonatomic, copy) NSString *downloadFilePath; // the local path of the file@property (nonatomic, copy) DownloadProgressBlock downloadProgress; // download progress@property (nonatomic, copy) DownloadFailureBlock failure; // callback for a download success@property (nonatomic, copy) DownloadSuccessBlock success; // callback for a download failure@property (nonatomic, copy) Checkpoint *checkpoint; // checkpoint for storing 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;
@end
- (void)initDownloadURLs {
OSSPlainTextAKSKPairCredentialProvider *pCredential = [[OSSPlainTextAKSKPairCredentialProvider alloc] initWithPlainTextAccessKey:OSS_ACCESSKEY_ID secretKey:OSS_SECRETKEY_ID];
_mClient = [[OSSClient alloc] initWithEndpoint:OSS_ENDPOINT credentialProvider:pCredential];

// generate the signed URL for the get request
OSSTask *downloadURLTask = [_mClient presignConstrainURLWithBucketName:@"aliyun-dhc-shanghai" withObjectKey:OSS_DOWNLOAD_FILE_NAME withExpirationInterval:1800];
[downloadURLTask waitUntilFinished];
_downloadURLString = downloadURLTask.result;

// generate the signed URL for the head request
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 storage path of the downloaded file

__weak typeof(self) wSelf = self;
_downloadRequest.downloadProgress = ^(int64_t bytesReceived, int64_t totalBytesReceived, int64_t totalBytesExpectToReceived) {
// totalBytesReceived is the number of bytes that the client currently has cached, and totalBytesExpectToReceived is the total number of bytes that need 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];
}
//1. First use SDK to get the download URL of the object
String signedURLString = ossClient.presignConstrainedObjectURL(bucket, object, expires);
//2. Add the download task mDownloadManager = DownloadManager.getInstance();
mDownloadManager.add(signedURLString, new DownloadListner() {
@Override
public void onFinished() {
Toast.makeText(MainActivity.this, "Download successful", Toast.LENGTH_SHORT).show();
}
@Override
public void onProgress(float progress) {
pb_progress1.setProgress((int) (progress * 100));
tv_progress1.setText(String.format("%.2f", progress * 100) + "%");
}
@Override
public void onPause() {
Toast.makeText(MainActivity.this, "Paused!", Toast.LENGTH_SHORT).show();
}
@Override
public void onCancel() {
tv_progress1.setText("0%");
pb_progress1.setProgress(0);
btn_download1.setText("Download");
Toast.makeText(MainActivity.this, "Download canceled!", Toast.LENGTH_SHORT).show();
}
});

//3. Start the download
mDownloadManager.download(signedURLString);

//4. Pause the download
mDownloadManager.cancel(signedURLString);

//5. Resume the download
mDownloadManager.download(signedURLString);

Improvements

--

--

--

Follow me to keep abreast with the latest technology news, industry insights, and developer trends. Alibaba Cloud website:https://www.alibabacloud.com

Love podcasts or audiobooks? Learn on the go with our new app.

Recommended from Medium

What Is Containerization?

KJUG #38 “Slowdown to Gain Speed: Technical practices that take time but save time”

How to Install and Use Composer on Ubuntu 16.04

Terraform 0.12 Development: A Step-by-Step Guide to Get Running with Providers

What are the Active Uses of a Class?

Let’s get started!

Transible — represents cloud configuration as Ansible playbooks

How I Maintain a Vocabulary List in Drafts with Terminology

Terminology URL Scheme and Vocabulary Workspace

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Alibaba Cloud

Alibaba Cloud

Follow me to keep abreast with the latest technology news, industry insights, and developer trends. Alibaba Cloud website:https://www.alibabacloud.com

More from Medium

What is Verifiable Credentials?

Cloud native with Saiyam — Motivating February

Microservices Integration Test under Cloud Native Architecture

Private enterprise blockchains