All Products
Search
Document Center

Object Storage Service:Unduhan yang dapat dilanjutkan (OSS SDK untuk iOS)

Last Updated:Jun 22, 2026

Unduhan yang dapat dilanjutkan memungkinkan Anda melanjutkan unduhan yang terganggu dari titik terakhir, sehingga menghemat waktu dan lalu lintas jaringan.

Proses

Misalnya, saat Anda mengunduh video di ponsel dan koneksi beralih dari Wi-Fi ke jaringan seluler, aplikasi secara default akan menghentikan unduhan. Jika fitur unduhan yang dapat dilanjutkan diaktifkan, unduhan akan dilanjutkan dari titik terakhir begitu Anda kembali ke jaringan Wi-Fi.

Selama proses unduhan, aplikasi menampilkan dialog progres yang menunjukkan persentase unduhan saat ini, bilah progres, serta tombol pause atau cancel untuk unduhan tersebut.

Fitur

  • HTTP/1.1 mendukung header Range, yang menentukan rentang data yang akan diambil. Header ini mendukung format berikut.

    Rentang Cap Waktu

    Deskripsi

    Range: bytes=100-

    Memindahkan data dari byte ke-101 hingga byte terakhir.

    Range: bytes=100-200

    Memindahkan data dari byte ke-101 hingga byte ke-201. Format ini sering digunakan untuk transfer terbagi (chunked) file besar, seperti video.

    Range: bytes=-100

    Memindahkan 100 byte terakhir dari konten, bukan 100 byte pertama.

    Range: bytes=0-100, 200-300

    Menentukan beberapa rentang konten sekaligus.

  • Untuk unduhan yang dapat dilanjutkan, Anda dapat menggunakan header If-Match untuk memverifikasi apakah file di server telah berubah. Nilai If-Match harus sesuai dengan nilai ETag file tersebut.

  • Saat client mengirim permintaan, sertakan header Range dan If-Match. Server OSS menerima permintaan tersebut dan memvalidasi nilai ETag dalam header If-Match. Jika nilainya tidak cocok, server akan mengembalikan kode status 412 Precondition Failed.

  • Server OSS mendukung header Range, If-Match, If-None-Match, If-Modified-Since, dan If-Unmodified-Since untuk operasi GetObject, sehingga memungkinkan implementasi unduhan yang dapat dilanjutkan untuk resource OSS di perangkat seluler.

Kode contoh

Penting

OSS SDK untuk iOS tidak mendukung unduhan yang dapat dilanjutkan secara native. Kode berikut hanya disediakan sebagai referensi untuk membantu Anda memahami proses unduhan. Jangan gunakan kode ini dalam proyek produksi. Untuk mengimplementasikan unduhan yang dapat dilanjutkan, Anda harus menulis kode sendiri atau menggunakan framework unduhan open source pihak ketiga.

Kode contoh berikut menunjukkan cara melakukan unduhan yang dapat dilanjutkan di 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;         // Sesi jaringan.
@property (nonatomic, strong) NSURLSessionDataTask *dataTask;   // Tugas permintaan data.
@property (nonatomic, copy) DownloadFailureBlock failure;    // Dipanggil saat permintaan gagal.
@property (nonatomic, copy) DownloadSuccessBlock success;    // Dipanggil saat permintaan berhasil.
@property (nonatomic, copy) DownloadProgressBlock progress;  // Melacak progres unduhan.
@property (nonatomic, copy) Checkpoint *checkpoint;        // Checkpoint.
@property (nonatomic, copy) NSString *requestURLString;    // URL resource yang akan diunduh.
@property (nonatomic, copy) NSString *headURLString;       // URL untuk permintaan HEAD.
@property (nonatomic, copy) NSString *targetPath;     // Jalur lokal untuk menyimpan file.
@property (nonatomic, assign) unsigned long long totalReceivedContentLength; // Ukuran konten yang telah diterima sejauh ini.
@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 adalah inti dari logika unduhan.
+ (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;
}
/**
 * Mendapatkan informasi file menggunakan metode HEAD dan membandingkan ETag file dengan yang ada di checkpoint lokal.
 */
- (BOOL)getFileInfo {
    __block BOOL resumable = NO;
    NSURL *url = [NSURL URLWithString:self.headURLString];
    NSMutableURLRequest *request = [[NSMutableURLRequest alloc]initWithURL:url];
    [request setHTTPMethod:@"HEAD"];
    // Memproses informasi objek. Misalnya, ETag digunakan untuk pemeriksaan awal pada unduhan yang dapat dilanjutkan, dan content-length digunakan untuk menghitung progres unduhan.
    NSURLSessionDataTask *task = [[NSURLSession sharedSession] dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
        if (error) {
            NSLog(@"Gagal mendapatkan metadata file. 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;
}
/**
 * Mendapatkan ukuran file lokal.
 */
- (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];    // Nilai kembalian `NO` menunjukkan bahwa kondisi untuk unduhan yang dapat dilanjutkan tidak terpenuhi.
    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(@"hapus file dengan error : %@", error);
    }
}
#pragma mark - NSURLSessionDataDelegate
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task
// Menentukan apakah unduhan telah selesai dan melaporkan hasilnya ke layanan tingkat atas.
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);
}
// Menambahkan data yang diterima ke file dan memperbarui progres unduhan.
- (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
  • Definisi 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 dari resource.
    @property (nonatomic, assign) unsigned long long totalExpectedLength;    // Ukuran total file.
    @end
    @interface DownloadRequest : NSObject
    @property (nonatomic, copy) NSString *sourceURLString;      // URL unduhan.
    @property (nonatomic, copy) NSString *headURLString;        // URL untuk mendapatkan metadata file.
    @property (nonatomic, copy) NSString *downloadFilePath;     // Jalur penyimpanan lokal untuk file.
    @property (nonatomic, copy) DownloadProgressBlock downloadProgress; // Callback untuk progres unduhan.
    @property (nonatomic, copy) DownloadFailureBlock failure;   // Callback untuk kegagalan unduhan.
    @property (nonatomic, copy) DownloadSuccessBlock success;   // Callback untuk keberhasilan unduhan.
    @property (nonatomic, copy) Checkpoint *checkpoint;         // Menyimpan ETag file.
    @end
    @interface DownloadService : NSObject
    + (instancetype)downloadServiceWithRequest:(DownloadRequest *)request;
    /**
     * Memulai unduhan.
     */
    - (void)resume;
    /**
     * Menjeda unduhan.
     */
    - (void)pause;
    /**
     * Membatalkan unduhan.
     */
    - (void)cancel;
    @end
  • Pemanggilan layanan tingkat atas

    - (void)initDownloadURLs {
        OSSPlainTextAKSKPairCredentialProvider *pCredential = [[OSSPlainTextAKSKPairCredentialProvider alloc] initWithPlainTextAccessKey:OSS_ACCESSKEY_ID secretKey:OSS_SECRETKEY_ID];
        _mClient = [[OSSClient alloc] initWithEndpoint:OSS_ENDPOINT credentialProvider:pCredential];
        // Menghasilkan URL yang ditandatangani untuk permintaan GET.
        OSSTask *downloadURLTask = [_mClient presignConstrainURLWithBucketName:@"aliyun-dhc-shanghai" withObjectKey:OSS_DOWNLOAD_FILE_NAME withExpirationInterval:1800];
        [downloadURLTask waitUntilFinished];
        _downloadURLString = downloadURLTask.result;
        // Menghasilkan URL yang ditandatangani untuk permintaan HEAD.
        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;       // Mengatur URL resource.
        _downloadRequest.headURLString = _headURLString;
        NSString *documentPath = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject;
        _downloadRequest.downloadFilePath = [documentPath stringByAppendingPathComponent:OSS_DOWNLOAD_FILE_NAME];   // Mengatur jalur lokal untuk menyimpan file yang diunduh.
        __weak typeof(self) wSelf = self;
        _downloadRequest.downloadProgress = ^(int64_t bytesReceived, int64_t totalBytesReceived, int64_t totalBytesExpectToReceived) {
            // totalBytesReceived adalah jumlah byte yang telah diunduh. totalBytesExpectToReceived adalah ukuran total yang diharapkan dari file.
            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(@"Unduhan berhasil");
        };
        _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];
    }
Catatan

Anda dapat memperoleh checkpoint dari callback kegagalan saat menjeda atau membatalkan unduhan. Saat memulai ulang unduhan, Anda dapat meneruskan checkpoint tersebut ke DownloadRequest. DownloadService kemudian menggunakan checkpoint tersebut untuk melakukan pemeriksaan konsistensi.