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

Copying Objects in Batches

This example copies objects in OBS in batches. Both intra-bucket and cross-bucket copy are supported. You can specify the mapping between the source object keys and the destination object keys, specify the number of concurrent copy tasks, and choose whether to retain the source object metadata or replace it with new metadata. This method is suitable for scenarios such as data migration and cross-region backup.

#import <Foundation/Foundation.h>
#import <OBS/OBS.h>
#pragma mark - Result Model
@interface BatchCopyResult : NSObject
@property (nonatomic, copy) NSString *srcObjectKey;
@property (nonatomic, copy) NSString *dstObjectKey;
@property (nonatomic, assign) BOOL success;
@property (nonatomic, strong) NSError *error;
@property (nonatomic, copy) NSString *etag;
@end
@implementation BatchCopyResult
@end
#pragma mark - Batch Copier
@interface BatchCopier : NSObject
@property (nonatomic, strong) OBSClient *client;
@property (nonatomic, copy) NSString *srcBucketName;
@property (nonatomic, copy) NSString *dstBucketName;
@property (nonatomic, assign) NSInteger concurrentCount;
@property (nonatomic, assign) OBSMetaDirective metadataDirective;
@property (nonatomic, strong) NSDictionary<NSString *, NSString *> *customMetadata;
@property (nonatomic, strong) NSMutableArray<BatchCopyResult *> *results;
@property (nonatomic, strong) dispatch_queue_t resultQueue;
- (instancetype)initWithClient:(OBSClient *)client
                 srcBucketName:(NSString *)srcBucketName
                 dstBucketName:(NSString *)dstBucketName
               concurrentCount:(NSInteger)concurrentCount;
- (void)copyObjects:(NSArray<NSString *> *)srcObjectKeys
       toObjectKeys:(NSArray<NSString *> *)dstObjectKeys
      waitUntilDone:(BOOL)wait;
@end
@implementation BatchCopier
- (instancetype)initWithClient:(OBSClient *)client
                 srcBucketName:(NSString *)srcBucketName
                 dstBucketName:(NSString *)dstBucketName
               concurrentCount:(NSInteger)concurrentCount {
    self = [super init];
    if (self) {
        _client = client;
        _srcBucketName = srcBucketName;
        _dstBucketName = dstBucketName;
        _concurrentCount = MIN(concurrentCount > 0 ? concurrentCount : 3, 10);
        _metadataDirective = OBSMetaDirectiveCopy;
        _results = [NSMutableArray array];
        _resultQueue = dispatch_queue_create("com.obs.batchcopy.result", DISPATCH_QUEUE_SERIAL);
    }
    return self;
}
- (void)copyObjects:(NSArray<NSString *> *)srcObjectKeys
       toObjectKeys:(NSArray<NSString *> *)dstObjectKeys
      waitUntilDone:(BOOL)wait {
    if (srcObjectKeys.count != dstObjectKeys.count) {
        NSLog(@"[BatchCopy] ERROR: source and destination key arrays must have the same count");
        return;
    }
    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.batchcopy", DISPATCH_QUEUE_CONCURRENT);
    NSLog(@"[BatchCopy] Starting %lu copy operations, concurrency: %ld, src: %@ -> dst: %@",
          (unsigned long)srcObjectKeys.count, (long)self.concurrentCount, self.srcBucketName, self.dstBucketName);
    for (NSUInteger i = 0; i < srcObjectKeys.count; i++) {
        NSString *srcKey = srcObjectKeys[i];
        NSString *dstKey = dstObjectKeys[i];
        if (srcKey.length == 0 || dstKey.length == 0) {
            continue;
        }
        dispatch_group_enter(group);
        dispatch_async(queue, ^{
            dispatch_semaphore_wait(semaphore, dispatch_time(DISPATCH_TIME_NOW, 300 * NSEC_PER_SEC));
            [self copyObjectFrom:srcKey to:dstKey completion:^{
                dispatch_semaphore_signal(semaphore);
                dispatch_group_leave(group);
            }];
        });
    }
    if (wait) {
        dispatch_group_wait(group, dispatch_time(DISPATCH_TIME_NOW, 300 * NSEC_PER_SEC));
        [self printSummary];
    }
}
- (void)copyObjectFrom:(NSString *)srcObjectKey
                    to:(NSString *)dstObjectKey
             completion:(void (^)(void))completion {
    //Note: The copyObject operation does not check whether the source object exists. If the source object does not exist, the server returns 404.
    OBSCopyObjectRequest *request = [[OBSCopyObjectRequest alloc] initWithSrcBucketName:self.srcBucketName
                                                                          srcObjectKey:srcObjectKey
                                                                          dstBucketName:self.dstBucketName
                                                                           dstObjectKey:dstObjectKey];
    // Note: To replace metadata, set metadataDirective to OBSMetaDirectiveReplace and configure customMetadata.
    request.dstObjectMetaDirective = self.metadataDirective;
    if (self.metadataDirective == OBSMetaDirectiveReplace && self.customMetadata.count > 0) {
        request.metaDataDict = [self.customMetadata mutableCopy];
    }
    __weak typeof(self) weakSelf = self;    __strong typeof(weakSelf) strongSelf = weakSelf;    if (!strongSelf) return;
    [self.client copyObject:request completionHandler:^(OBSCopyObjectResponse *response, NSError *error) {
        BatchCopyResult *result = [[BatchCopyResult alloc] init];
        result.srcObjectKey = srcObjectKey;
        result.dstObjectKey = dstObjectKey;
        result.error = error;
        result.success = (error == nil);
        result.etag = response.etag;
        if (error) {
            NSLog(@"[Copy] FAILED - %@ -> %@ : %@", srcObjectKey, dstObjectKey, error.localizedDescription);
        } else {
            NSLog(@"[Copy] SUCCESS - %@ (%@) -> %@ (%@)",
                  srcObjectKey, strongSelf.srcBucketName, dstObjectKey, strongSelf.dstBucketName);
        }
        dispatch_sync(strongSelf.resultQueue, ^{ [strongSelf.results addObject:result]; });
        if (completion) completion();
    }];
}
- (void)printSummary {
    NSInteger success = 0, failure = 0;
    for (BatchCopyResult *r in self.results) {
        if (r.success) success++;
        else failure++;
    }
    NSLog(@"\n========== Batch Copy Summary ==========");
    NSLog(@"Total: %lu | Success: %ld | Failed: %ld", (unsigned long)self.results.count, (long)success, (long)failure);
    if (failure > 0) {
        NSLog(@"Failed items:");
        for (BatchCopyResult *r in self.results) {
            if (!r.success) {
                NSLog(@"  - %@ -> %@ : %@", r.srcObjectKey, r.dstObjectKey, 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];
        // Source and destination buckets
        // Set dstBucketName equal to srcBucketName for in-bucket copy
        NSString *srcBucketName = @"your-source-bucket";
        NSString *dstBucketName = @"your-destination-bucket";
        NSInteger concurrentCount = 3;  // Adjust as needed
        // Source object keys
        NSArray<NSString *> *srcObjectKeys = @[
            @"source_folder/file1.txt",
            @"source_folder/file2.jpg",
            @"source_folder/file3.pdf",
            @"source_folder/file4.mp4",
            @"source_folder/file5.zip"
        ];
        // Destination object keys (must match srcObjectKeys count)
        NSString *dstPrefix = @"backup/";
        NSMutableArray<NSString *> *dstObjectKeys = [NSMutableArray arrayWithCapacity:srcObjectKeys.count];
        for (NSString *srcKey in srcObjectKeys) {
            // Note: lastPathComponent returns only the file name. The intermediate directory structure is lost.
            [dstObjectKeys addObject:[dstPrefix stringByAppendingString:[srcKey lastPathComponent]]];
        }
        // === Initialize OBS Client ===
        OBSStaticCredentialProvider *credentialProvider = [[OBSStaticCredentialProvider alloc] initWithAccessKey:AK secretKey:SK];
        OBSServiceConfiguration *conf = [[OBSServiceConfiguration alloc] initWithURLString:endPoint credentialProvider:credentialProvider];
        OBSClient *client = [[OBSClient alloc] initWithConfiguration:conf];
        // === Batch Copy ===
        BatchCopier *copier = [[BatchCopier alloc] initWithClient:client
                                                  srcBucketName:srcBucketName
                                                  dstBucketName:dstBucketName
                                                concurrentCount:concurrentCount];
        // OBSMetaDirectiveCopy: keep source object's metadata (default)
        // OBSMetaDirectiveReplace: use new metadata (set via customMetadata below)
        copier.metadataDirective = OBSMetaDirectiveCopy;
        // Optional: set custom metadata when using OBSMetaDirectiveReplace
        // copier.customMetadata = @{@"X-Obs-Meta-Copied-By": @"BatchCopyTest", @"X-Obs-Meta-Copy-Time": [[NSDate date] description]};
        [copier copyObjects:srcObjectKeys toObjectKeys:dstObjectKeys waitUntilDone:YES];
    }
    return 0;
}