再開可能なダウンロードを使用すると、中断されたダウンロードを中断した箇所から続行できます。この機能により、時間とネットワークトラフィックを節約できます。
プロセス
たとえば、スマートフォンで動画をダウンロードしているときに、ネットワークが Wi-Fi からモバイルネットワークに切り替わると、アプリはデフォルトでダウンロードを中断します。再開可能なダウンロードを有効にすると、Wi-Fi に戻ったときに中断した箇所からダウンロードが再開されます。
次の図は、再開可能なダウンロードの仕組みを示しています。
次の図は、再開可能なダウンロードのプロセスを示しています。
機能
HTTP 1.1 は Range ヘッダーをサポートしています。このヘッダーは、取得するデータの範囲を指定します。Range ヘッダーは、次のフォーマットをサポートしています。
タイムスタンプ範囲
説明
Range: bytes=100-
101 バイト目から最後のバイトまでのデータを転送します。
Range: bytes=100-200
101 バイト目から 201 バイト目までのデータを転送します。これは、動画などの大きなファイルのチャンク転送によく使用されます。
Range: bytes=-100
最初の 100 バイトではなく、コンテンツの最後の 100 バイトを転送します。
Range: bytes=0-100, 200-300
複数のコンテンツ範囲を同時に指定します。
再開可能なダウンロードでは、
If-Matchヘッダーを使用して、サーバー上のファイルが変更されたかどうかを確認できます。If-Matchの値は、ファイルのETag値に対応します。クライアントがリクエストを送信するときは、
RangeヘッダーとIf-Matchヘッダーを含めます。OSS サーバーはリクエストを受信し、If-Match ヘッダーの ETag 値を検証します。値が一致しない場合、サーバーは 412 Precondition Failed 状態コードを返します。OSS サーバーは、GetObject 操作に対して
Range、If-Match、If-None-Match、If-Modified-Since、およびIf-Unmodified-Sinceをサポートしています。これにより、モバイルデバイス上の OSS リソースの再開可能なダウンロードを実装できます。
サンプルコード
iOS 向け OSS SDK は、ネイティブでは再開可能なダウンロードをサポートしていません。以下のコードは、ダウンロードプロセスを理解するための参考としてのみ提供されています。このコードを本番プロジェクトで使用しないでください。再開可能なダウンロードを実装するには、独自のコードを作成するか、サードパーティのオープンソースダウンロードフレームワークを使用する必要があります。
次のサンプルコードは、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; // ネットワークセッション。
@property (nonatomic, strong) NSURLSessionDataTask *dataTask; // データリクエストタスク。
@property (nonatomic, copy) DownloadFailureBlock failure; // リクエストエラー。
@property (nonatomic, copy) DownloadSuccessBlock success; // リクエスト成功。
@property (nonatomic, copy) DownloadProgressBlock progress; // ダウンロードの進行状況。
@property (nonatomic, copy) Checkpoint *checkpoint; // チェックポイント。
@property (nonatomic, copy) NSString *requestURLString; // ダウンロードリクエスト用のファイルリソースアドレス。
@property (nonatomic, copy) NSString *headURLString; // HEAD リクエスト用のファイルリソースアドレス。
@property (nonatomic, copy) NSString *targetPath; // ファイルストレージパス。
@property (nonatomic, assign) unsigned long long totalReceivedContentLength; // ダウンロードされたファイルのサイズ。
@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 はダウンロードロジックのコアです。
+ (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 メソッドを使用してファイル情報を取得します。OSS はファイルの ETag をローカルのチェックポイントに保存されている ETag と比較し、比較結果を返します。
*/
- (BOOL)getFileInfo {
__block BOOL resumable = NO;
NSURL *url = [NSURL URLWithString:self.headURLString];
NSMutableURLRequest *request = [[NSMutableURLRequest alloc]initWithURL:url];
[request setHTTPMethod:@"HEAD"];
// オブジェクト情報を処理します。たとえば、ETag は再開可能なダウンロードの事前チェックに使用され、content-length はダウンロードの進行状況の計算に使用されます。
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;
}
/**
* ローカルファイルのサイズを取得します。
*/
- (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]; // resumable が NO を返した場合、再開可能なダウンロードの条件は満たされていません。
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);
}
}
@endDownloadRequest の定義
#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 値。 @property (nonatomic, assign) unsigned long long totalExpectedLength; // ファイルの合計サイズ。 @end @interface DownloadRequest : NSObject @property (nonatomic, copy) NSString *sourceURLString; // ダウンロード URL。 @property (nonatomic, copy) NSString *headURLString; // ファイルメタデータを取得するための URL。 @property (nonatomic, copy) NSString *downloadFilePath; // ファイルのローカルストレージパス。 @property (nonatomic, copy) DownloadProgressBlock downloadProgress; // ダウンロードの進行状況。 @property (nonatomic, copy) DownloadFailureBlock failure; // ダウンロード失敗時のコールバック。 @property (nonatomic, copy) DownloadSuccessBlock success; // ダウンロード成功時のコールバック。 @property (nonatomic, copy) Checkpoint *checkpoint; // ファイルの ETag を格納します。 @end @interface DownloadService : NSObject + (instancetype)downloadServiceWithRequest:(DownloadRequest *)request; /** * ダウンロードを開始します。 */ - (void)resume; /** * ダウンロードを一時停止します。 */ - (void)pause; /** * ダウンロードをキャンセルします。 */ - (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]; // GET リクエスト用の署名付き URL を生成します。 OSSTask *downloadURLTask = [_mClient presignConstrainURLWithBucketName:@"aliyun-dhc-shanghai" withObjectKey:OSS_DOWNLOAD_FILE_NAME withExpirationInterval:1800]; [downloadURLTask waitUntilFinished]; _downloadURLString = downloadURLTask.result; // HEAD リクエスト用の署名付き URL を生成します。 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; // リソースの URL を設定します。 _downloadRequest.headURLString = _headURLString; NSString *documentPath = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject; _downloadRequest.downloadFilePath = [documentPath stringByAppendingPathComponent:OSS_DOWNLOAD_FILE_NAME]; // ダウンロードしたファイルを保存するローカルパスを設定します。 __weak typeof(self) wSelf = self; _downloadRequest.downloadProgress = ^(int64_t bytesReceived, int64_t totalBytesReceived, int64_t totalBytesExpectToReceived) { // totalBytesReceived はクライアントにキャッシュされたバイト数を示します。totalBytesExpectToReceived はダウンロードされる合計バイト数を示します。 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]; }
ダウンロードを一時停止またはキャンセルすると、失敗コールバックからチェックポイントを取得できます。ダウンロードを再開するときに、チェックポイントを DownloadRequest に渡すことができます。その後、DownloadService はチェックポイントを使用して一貫性チェックを実行します。