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.
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-Matchuntuk memverifikasi apakah file di server telah berubah. NilaiIf-Matchharus sesuai dengan nilaiETagfile tersebut.Saat client mengirim permintaan, sertakan header
RangedanIf-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, danIf-Unmodified-Sincedalam operasi GetObject, sehingga memungkinkan Anda mengimplementasikan unduhan yang dapat dilanjutkan untuk resource OSS pada perangkat seluler.
Kode contoh
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);
}
}
@endDefinisi 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; @endPemanggilan 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]; }
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.