All Products
Search
Document Center

Object Storage Service:Resumable download (iOS SDK)

Last Updated:Mar 20, 2026

Resumable download lets you resume an interrupted download from where it left off, saving time and network traffic.

Important

The OSS SDK for iOS does not natively support resumable downloads. The sample code in this topic is provided for reference only to help you understand the download process. Do not use this code in production. To implement resumable downloads, write your own code or use a third-party open source download framework.

How it works

OSS supports the HTTP/1.1 Range header, which lets a client request a specific byte range of an object. On reconnection after an interruption, the client sends a Range header with the byte offset of the last received byte, so the server picks up from that point instead of restarting from the beginning.

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.

To avoid downloading a modified or replaced file, use the If-Match header alongside Range. Before resuming, the client sends a HEAD request to retrieve the object's current ETag. It then includes that ETag value in the If-Match header of the GET request. If the values match, the server returns the requested byte range (HTTP 206 Partial Content). If they do not match—meaning the object has changed—the server returns 412 Precondition Failed, and the download starts from the beginning.

OSS supports the following conditional headers for GetObject operations, enabling resumable downloads on mobile devices:

HeaderPurpose
RangeSpecifies the byte range to retrieve
If-MatchResumes only if the object ETag matches
If-None-MatchResumes only if the object ETag does not match
If-Modified-SinceResumes only if the object was modified after the specified time
If-Unmodified-SinceResumes only if the object was not modified after the specified time

Range header formats

ValueBehavior
Range: bytes=100-Transfers data from byte 101 to the end of the file
Range: bytes=100-200Transfers data from byte 101 to byte 201; commonly used for chunked transfer of large files such as videos
Range: bytes=-100Transfers the last 100 bytes of the content
Range: bytes=0-100, 200-300Specifies multiple content ranges at the same time

Sample code

The following sample implements resumable download using three components: Checkpoint, DownloadRequest, and DownloadService.

ComponentRole
CheckpointStores the object's ETag and total expected size, persisted across pause and resume operations
DownloadRequestHolds the download configuration: source URL, HEAD URL, local file path, progress callback, and success/failure handlers
DownloadServiceManages the download session. Call resume to start or continue, pause to suspend, and cancel to abort and delete the partial file

DownloadService implementation

#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;    // Signed URL for GET requests.
@property (nonatomic, copy) NSString *headURLString;       // Signed URL for HEAD requests.
@property (nonatomic, copy) NSString *targetPath;     // Local path for the downloaded file.
@property (nonatomic, assign) unsigned long long totalReceivedContentLength; // Bytes downloaded so far.
@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;
}

/**
 * Sends a HEAD request to get the object's ETag, then compares it to the
 * ETag stored in the local checkpoint. Returns YES if they match and
 * the download can be resumed from where it left off.
 */
- (BOOL)getFileInfo {
    __block BOOL resumable = NO;
    NSURL *url = [NSURL URLWithString:self.headURLString];
    NSMutableURLRequest *request = [[NSMutableURLRequest alloc]initWithURL:url];
    [request setHTTPMethod:@"HEAD"];
    // ETag is used for the consistency check in resumable downloads;
    // Content-Length is used to calculate 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;
}

/**
 * Returns the size of the partially downloaded file at the given path.
 * Returns 0 if the file does not exist.
 */
- (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];    // Returns NO if the conditions for resuming 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

// Fires when the download task completes. Saves the final ETag and total size
// to the checkpoint, then calls the success or failure handler.
- (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);
}

// Appends received data to the local file and updates 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,                                 // bytesReceived: bytes received in this chunk
            self.totalReceivedContentLength,             // totalBytesReceived: total bytes received so far
            self.checkpoint.totalExpectedLength          // totalBytesExpectToReceived: total file size
        );
    }
}

@end

DownloadRequest 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 object.
@property (nonatomic, assign) unsigned long long totalExpectedLength;    // The total size of the file.

@end

@interface DownloadRequest : NSObject

@property (nonatomic, copy) NSString *sourceURLString;      // The signed URL for GET requests.
@property (nonatomic, copy) NSString *headURLString;        // The signed URL for HEAD requests.
@property (nonatomic, copy) NSString *downloadFilePath;     // The local path to save the downloaded file.
@property (nonatomic, copy) DownloadProgressBlock downloadProgress; // The download progress callback.
@property (nonatomic, copy) DownloadFailureBlock failure;   // Called when the download fails.
@property (nonatomic, copy) DownloadSuccessBlock success;   // Called when the download succeeds.
@property (nonatomic, copy) Checkpoint *checkpoint;         // Stores the ETag for the consistency check.

@end


@interface DownloadService : NSObject

+ (instancetype)downloadServiceWithRequest:(DownloadRequest *)request;

/**
 * Starts or resumes the download.
 */
- (void)resume;

/**
 * Pauses the download. The checkpoint is available in the failure callback.
 */
- (void)pause;

/**
 * Cancels the download and deletes the partial file.
 */
- (void)cancel;

@end

Upper-layer service call

The following example shows how to generate signed URLs for GET and HEAD requests, then use DownloadService to start, pause, and cancel a download.

- (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 (valid for 1800 seconds).
    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 (valid for 1800 seconds).
    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;
    _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) {
        // Update the progress UI on the main thread.
        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) {
        // Save the checkpoint so the download can be resumed later.
        __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];
}
Note

When you pause or cancel a download, retrieve the checkpoint from the failure callback and pass it to DownloadRequest when restarting. DownloadService uses the checkpoint to perform a consistency check before resuming.