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;
} What is your overall rating for this page?
Thank you very much for your feedback. We will continue working to improve the documentation.See the reply and handling status in My Cloud VOC.
For any further questions, feel free to contact us through the chatbot.
Chatbot