All Products
Search
Document Center

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

Last Updated:Dec 17, 2025

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

Proses

Misalnya, jika Anda sedang mengunduh video di ponsel dan jaringan beralih dari Wi-Fi ke jaringan seluler, aplikasi secara default akan menghentikan unduhan tersebut. Dengan mengaktifkan unduhan yang dapat dilanjutkan, unduhan akan dilanjutkan dari titik terakhir ketika koneksi kembali stabil, seperti saat Anda kembali ke Wi-Fi.

Gambar berikut mengilustrasikan cara kerja unduhan yang dapat dilanjutkan.

Gambar berikut menunjukkan proses unduhan yang dapat dilanjutkan.

image

Fitur

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

    Rentang timestamp

    Deskripsi

    Range: bytes=100-

    Mentransfer data dari byte 101 hingga byte terakhir.

    Range: bytes=100-200

    Mentransfer data dari byte 101 hingga byte 201. Format ini sering digunakan untuk transfer chunk file besar, seperti video.

    Range: bytes=-100

    Mentransfer 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 dalam operasi GetObject, sehingga memungkinkan Anda mengimplementasikan unduhan yang dapat dilanjutkan untuk resource OSS pada 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;    // Kesalahan permintaan.
@property (nonatomic, copy) DownloadSuccessBlock success;    // Keberhasilan permintaan.
@property (nonatomic, copy) DownloadProgressBlock progress;  // Progres unduhan.
@property (nonatomic, copy) Checkpoint *checkpoint;        // Checkpoint.
@property (nonatomic, copy) NSString *requestURLString;    // Alamat resource file untuk permintaan unduhan.
@property (nonatomic, copy) NSString *headURLString;       // Alamat resource file untuk permintaan HEAD.
@property (nonatomic, copy) NSString *targetPath;     // Jalur penyimpanan file.
@property (nonatomic, assign) unsigned long long totalReceivedContentLength; // Ukuran file yang telah diunduh.
@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. OSS membandingkan ETag file dengan ETag yang disimpan di checkpoint lokal dan mengembalikan hasil perbandingan tersebut.
 */
- (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 dalam 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];    // Jika resumable mengembalikan NO, 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
// Memeriksa apakah tugas unduhan telah selesai dan mengembalikan 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);
}
// Menulis data jaringan yang diterima ke file dalam mode tambah (append) 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;     // Nilai 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 file.
    
    @property (nonatomic, copy) DownloadProgressBlock downloadProgress; // Progres unduhan.
    
    @property (nonatomic, copy) DownloadFailureBlock failure;   // Callback saat unduhan gagal.
    
    @property (nonatomic, copy) DownloadSuccessBlock success;   // Callback saat unduhan berhasil.
    
    @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;       // Menetapkan URL resource.
        _downloadRequest.headURLString = _headURLString;
        NSString *documentPath = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject;
        _downloadRequest.downloadFilePath = [documentPath stringByAppendingPathComponent:OSS_DOWNLOAD_FILE_NAME];   // Menetapkan jalur lokal untuk menyimpan file yang diunduh.
    
        __weak typeof(self) wSelf = self;
        _downloadRequest.downloadProgress = ^(int64_t bytesReceived, int64_t totalBytesReceived, int64_t totalBytesExpectToReceived) {
            // totalBytesReceived menunjukkan jumlah byte yang telah di-cache di client. totalBytesExpectToReceived menunjukkan jumlah total byte yang akan diunduh.
            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.