Updated on 2026-09-22 GMT+08:00

Performing a Batch Download

This example downloads objects from an OBS bucket to a local directory in batches. You can list objects whose names contain the specified prefix and download them concurrently based on the specified concurrency. The files are saved to the local directory based on the original key structure. This method is suitable for scenarios such as batch export and offline backup.

#import <Foundation/Foundation.h>
#import <OBS/OBS.h>
#pragma mark - Result Model
@interface BatchDownloadResult : NSObject
@property (nonatomic, copy) NSString *objectKey;
@property (nonatomic, copy) NSString *localPath;
@property (nonatomic, assign) BOOL success;
@property (nonatomic, strong) NSError *error;
@property (nonatomic, copy) NSString *httpStatusCode;
@end
@implementation BatchDownloadResult
@end
#pragma mark - Batch Downloader
@interface BatchDownloader : NSObject
@property (nonatomic, strong) OBSClient *client;
@property (nonatomic, copy) NSString *bucketName;
@property (nonatomic, assign) NSInteger concurrentCount;
@property (nonatomic, strong) NSMutableArray<BatchDownloadResult *> *results;
@property (nonatomic, strong) dispatch_queue_t resultQueue;
- (instancetype)initWithClient:(OBSClient *)client
                    bucketName:(NSString *)bucketName
               concurrentCount:(NSInteger)concurrentCount;
- (void)downloadObjects:(NSArray<NSString *> *)objectKeys
          toDirectory:(NSString *)directoryPath
            waitUntilDone:(BOOL)wait;
@end
@implementation BatchDownloader
- (instancetype)initWithClient:(OBSClient *)client
                    bucketName:(NSString *)bucketName
               concurrentCount:(NSInteger)concurrentCount {
    self = [super init];
    if (self) {
        _client = client;
        _bucketName = bucketName;
        _concurrentCount = MIN(concurrentCount > 0 ? concurrentCount : 3, 10);
        _results = [NSMutableArray array];
        _resultQueue = dispatch_queue_create("com.obs.batchdownload.result", DISPATCH_QUEUE_SERIAL);
    }
    return self;
}
- (void)downloadObjects:(NSArray<NSString *> *)objectKeys
          toDirectory:(NSString *)directoryPath
            waitUntilDone:(BOOL)wait {
    dispatch_group_t group = dispatch_group_create();
    dispatch_semaphore_t semaphore = dispatch_semaphore_create(self.concurrentCount);
    dispatch_queue_t queue = dispatch_queue_create("com.obs.batchdownload", DISPATCH_QUEUE_CONCURRENT);
    NSLog(@"[BatchDownload] Starting %lu downloads, concurrency: %ld",
          (unsigned long)objectKeys.count, (long)self.concurrentCount);
    for (NSString *objectKey in objectKeys) {
        dispatch_group_enter(group);
        dispatch_async(queue, ^{
            dispatch_time_t timeout = dispatch_time(DISPATCH_TIME_NOW, 300 * NSEC_PER_SEC);
            if (dispatch_semaphore_wait(semaphore, timeout) != 0) {
                NSLog(@"[BatchDownload] semaphore wait timeout, skip object: %@", objectKey);
                dispatch_group_leave(group);
                return;
            }
            [self downloadObject:objectKey toDirectory:directoryPath completion:^{
                dispatch_semaphore_signal(semaphore);
                dispatch_group_leave(group);
            }];
        });
    }
    if (wait) {
        dispatch_time_t groupTimeout = dispatch_time(DISPATCH_TIME_NOW, 300 * NSEC_PER_SEC);
        if (dispatch_group_wait(group, groupTimeout) != 0) {
            NSLog(@"[BatchDownload] group wait timeout, some downloads may not finish");
        }
        [self printSummary];
    }
}
- (void)downloadObject:(NSString *)objectKey
          toDirectory:(NSString *)directoryPath
            completion:(void (^)(void))completion {
    NSError *dirError;    [[NSFileManager defaultManager] createDirectoryAtPath:directoryPath
                              withIntermediateDirectories:YES
                                               attributes:nil
                                                    error:&dirError];    if (dirError) {        NSLog(@"Failed to create the directory: %@", dirError);    }
    NSString *localPath = [directoryPath stringByAppendingPathComponent:objectKey];
    dirError = nil;    [[NSFileManager defaultManager] createDirectoryAtPath:[localPath stringByDeletingLastPathComponent] withIntermediateDirectories:YES attributes:nil error:&dirError];    if (dirError) {        NSLog(@"Failed to create the directory: %@", dirError);    }
    OBSGetObjectToFileRequest *request = [[OBSGetObjectToFileRequest alloc] initWithBucketName:self.bucketName
                                                                                   objectKey:objectKey
                                                                           downloadFilePath:localPath];
    request.downloadProgressBlock = ^(int64_t bytesWritten, int64_t totalBytesWritten, int64_t totalBytesExpectedToWrite) {
        float progress = totalBytesExpectedToWrite > 0 ? MIN((float)totalBytesWritten * 100.0f / (float)totalBytesExpectedToWrite, 100.0f) : 0;
        NSLog(@"[Download] %@ - %0.1f%% (%lld / %lld bytes)", objectKey, progress, totalBytesWritten, totalBytesExpectedToWrite);
    };
    // Note: The getObject operation does not check whether the object exists. If the object does not exist, the server returns 404.
    __weak typeof(self) weakSelf = self;    __strong typeof(weakSelf) strongSelf = weakSelf;    if (!strongSelf) return;    [self.client getObject:request completionHandler:^(OBSGetObjectResponse *response, NSError *error) {
        BatchDownloadResult *result = [[BatchDownloadResult alloc] init];
        result.objectKey = objectKey;
        result.localPath = localPath;
        result.error = error;
        result.success = (error == nil);
        // Note: Record the HTTP status code. If the value of statusCode is nil, use a hyphen (-) as the placeholder.
        result.httpStatusCode = response.statusCode ?: @"-";
        if (error) {
            // Note: If the download fails, it is recommended that an error message be displayed or a retry mechanism be provided.
            NSLog(@"[Download] FAILED - %@ : %@", objectKey, error.localizedDescription);
        } else {
            NSLog(@"[Download] SUCCESS - %@ -> %@", objectKey, localPath);
        }
        dispatch_sync(strongSelf.resultQueue, ^{ [strongSelf.results addObject:result]; });
        if (completion) completion();
    }];
}
- (void)printSummary {
    NSInteger success = 0, failure = 0;
    for (BatchDownloadResult *r in self.results) {
        if (r.success) success++;
        else failure++;
    }
    NSLog(@"\n========== Batch Download Summary ==========");
    NSLog(@"Total: %lu | Success: %ld | Failed: %ld", (unsigned long)self.results.count, (long)success, (long)failure);
    if (failure > 0) {
        NSLog(@"Failed items:");
        for (BatchDownloadResult *r in self.results) {
            if (!r.success) {
                NSLog(@"  - %@ : %@", r.objectKey, r.error.localizedDescription);
            }
        }
    }
    NSLog(@"============================================\n");
}
@end
#pragma mark - Entry Point
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // === Configuration ===
        NSString *endPoint = @"https://your-endpoint";
        // Hard-coded or plaintext AK and SK are risky. For security purposes, encrypt your AK and SK before storing them in the configuration file or environment variables. In this example, the AK and SK are stored in environment variables. Before running the code in this example, configure environment variables AccessKeyID and SecretAccessKey.
        // Obtain an AK/SK pair on the management console. For details, see https://support.huaweicloud.com/intl/en-us/usermanual-ca/ca_01_0003.html.
        char* ak_env = getenv("AccessKeyID");
        char* sk_env = getenv("SecretAccessKey");
        if (ak_env == NULL || sk_env == NULL) {
            NSLog(@"Error: AccessKeyID or SecretAccessKey environment variable not set");
            return 1;
        }
        NSString *AK = [NSString stringWithUTF8String:ak_env];
        NSString *SK = [NSString stringWithUTF8String:sk_env];
        NSString *bucketName = @"your-bucket-name";
        NSInteger concurrentCount = 3;  // Adjust as needed
        // Objects to download
        NSArray<NSString *> *objectKeys = @[
            @"folder/file1.txt",
            @"folder/file2.jpg",
            @"folder/file3.pdf",
            @"folder/file4.mp4",
            @"folder/file5.zip"
        ];
        // Local download directory
        NSString *downloadDir = [NSTemporaryDirectory() stringByAppendingPathComponent:@"BatchDownload"];
        // === Initialize OBS Client ===
        OBSStaticCredentialProvider *credentialProvider = [[OBSStaticCredentialProvider alloc] initWithAccessKey:AK secretKey:SK];
        OBSServiceConfiguration *conf = [[OBSServiceConfiguration alloc] initWithURLString:endPoint credentialProvider:credentialProvider];
        // Optionally increase concurrent download limit
        conf.maxConcurrentDownloadRequestCount = concurrentCount;
        OBSClient *client = [[OBSClient alloc] initWithConfiguration:conf];
        // === Batch Download ===
        BatchDownloader *downloader = [[BatchDownloader alloc] initWithClient:client
                                                                   bucketName:bucketName
                                                              concurrentCount:concurrentCount];
        // Note: Before downloading objects, check whether the local disk space is sufficient.
        [downloader downloadObjects:objectKeys toDirectory:downloadDir waitUntilDone:YES];
    }
    return 0;
}