すべてのプロダクト
Search
ドキュメントセンター

Object Storage Service:再開可能なダウンロード (iOS SDK)

最終更新日:Dec 17, 2025

再開可能なダウンロードを使用すると、中断されたダウンロードを中断した箇所から続行できます。この機能により、時間とネットワークトラフィックを節約できます。

プロセス

たとえば、スマートフォンで動画をダウンロードしているときに、ネットワークが Wi-Fi からモバイルネットワークに切り替わると、アプリはデフォルトでダウンロードを中断します。再開可能なダウンロードを有効にすると、Wi-Fi に戻ったときに中断した箇所からダウンロードが再開されます。

次の図は、再開可能なダウンロードの仕組みを示しています。

次の図は、再開可能なダウンロードのプロセスを示しています。

image

機能

  • 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 操作に対して RangeIf-MatchIf-None-MatchIf-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);
    }
}

@end
  • DownloadRequest の定義

    #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 はチェックポイントを使用して一貫性チェックを実行します。