Performing a Batch Upload
#import <Foundation/Foundation.h>
#import <OBS/OBS.h>
#pragma mark - Result Model
@interface BatchUploadResult : NSObject
@property (nonatomic, copy) NSString *filePath;
@property (nonatomic, copy) NSString *objectKey;
@property (nonatomic, assign) BOOL success;
@property (nonatomic, strong) NSError *error;
@property (nonatomic, copy) NSString *etag;
@end
@implementation BatchUploadResult
@end
#pragma mark - Batch Uploader
@interface BatchUploader : NSObject
@property (nonatomic, strong) OBSClient *client;
@property (nonatomic, copy) NSString *bucketName;
@property (nonatomic, assign) NSInteger concurrentCount;
@property (nonatomic, strong) NSMutableArray<BatchUploadResult *> *results;
@property (nonatomic, strong) dispatch_queue_t resultQueue;
- (instancetype)initWithClient:(OBSClient *)client
bucketName:(NSString *)bucketName
concurrentCount:(NSInteger)concurrentCount;
- (void)uploadFiles:(NSMutableDictionary<NSString *, NSString *> *)fileMap
waitUntilDone:(BOOL)wait;
@end
@implementation BatchUploader
- (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.batchupload.result", DISPATCH_QUEUE_SERIAL);
}
return self;
}
- (void)uploadFiles:(NSMutableDictionary<NSString *, NSString *> *)fileMap
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.batchupload", DISPATCH_QUEUE_CONCURRENT);
NSLog(@"[BatchUpload] Starting %lu uploads, concurrency: %ld",
(unsigned long)fileMap.count, (long)self.concurrentCount);
[fileMap enumerateKeysAndObjectsUsingBlock:^(NSString *filePath, NSString *objectKey, BOOL *stop) {
// Note: Each iteration calls enter once. leave is called once in each of the two mutually exclusive paths (skip upon semaphore timeout or callback upon upload completion). enter and leave must be paired. Deleting any leave will cause the group to be permanently suspended.
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(@"[BatchUpload] semaphore wait timeout, skip file: %@", filePath);
dispatch_group_leave(group);
return;
}
NSString *key = objectKey.length > 0 ? objectKey : [filePath lastPathComponent];
[self uploadFile:filePath objectKey:key 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(@"[BatchUpload] group wait timeout, some uploads may not finish");
}
[self printSummary];
}
}
- (void)uploadFile:(NSString *)filePath
objectKey:(NSString *)objectKey
completion:(void (^)(void))completion {
NSString *resolvedKey = objectKey.length > 0 ? objectKey : [filePath lastPathComponent];
__weak typeof(self) weakSelf = self; __strong typeof(weakSelf) strongSelf = weakSelf; if (!strongSelf) return;
// Note: Before uploading a file, you are advised to check whether the file is readable using isReadableFileAtPath to avoid read failures caused by insufficient permissions.
// Note: fileExistsAtPath only checks whether the file exists. The file may be modified by another process during the upload. You are advised to add a file lock or copy the file before uploading it.
if (![[NSFileManager defaultManager] fileExistsAtPath:filePath]) {
BatchUploadResult *result = [[BatchUploadResult alloc] init];
result.filePath = filePath;
result.objectKey = resolvedKey;
result.success = NO;
result.error = [NSError errorWithDomain:@"BatchUpload"
code:-1
userInfo:@{NSLocalizedDescriptionKey: @"File not found"}];
NSLog(@"[Upload] FAILED - %@ : file not found", filePath);
dispatch_sync(strongSelf.resultQueue, ^{ [strongSelf.results addObject:result]; });
if (completion) completion();
return;
}
// Note: The maximum size of a single file that can be uploaded using OBS PutObject is 5 GB. For large files, use OBSUploadFileRequest to upload them in resumable mode.
OBSPutObjectWithFileRequest *request = [[OBSPutObjectWithFileRequest alloc] initWithBucketName:strongSelf.bucketName
objectKey:resolvedKey
uploadFilePath:filePath];
request.uploadProgressBlock = ^(int64_t bytesSent, int64_t totalBytesSent, int64_t totalBytesExpectedToSend) {
float progress = totalBytesExpectedToSend > 0 ? MIN((float)totalBytesSent * 100.0f / (float)totalBytesExpectedToSend, 100.0f) : 0;
NSLog(@"[Upload] %@ - %0.1f%% (%lld / %lld bytes)", resolvedKey, progress, totalBytesSent, totalBytesExpectedToSend);
};
[strongSelf.client putObject:request completionHandler:^(OBSPutObjectResponse *response, NSError *error) {
BatchUploadResult *result = [[BatchUploadResult alloc] init];
result.filePath = filePath;
result.objectKey = resolvedKey;
result.error = error;
result.success = (error == nil);
result.etag = response.etag;
if (error) {
NSLog(@"[Upload] FAILED - %@ : %@", resolvedKey, error.localizedDescription);
} else {
NSLog(@"[Upload] SUCCESS - %@ (ETag: %@)", resolvedKey, response.etag);
}
dispatch_sync(strongSelf.resultQueue, ^{ [strongSelf.results addObject:result]; });
if (completion) completion();
}];
}
- (void)printSummary {
NSInteger success = 0, failure = 0;
for (BatchUploadResult *r in self.results) {
if (r.success) success++;
else failure++;
}
NSLog(@"\n========== Batch Upload Summary ==========");
NSLog(@"Total: %lu | Success: %ld | Failed: %ld", (unsigned long)self.results.count, (long)success, (long)failure);
if (failure > 0) {
NSLog(@"Failed items:");
for (BatchUploadResult *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
// Map local file paths to object keys
// If object key is nil/empty, filename is used as the key
NSMutableDictionary<NSString *, NSString *> *fileMap = [NSMutableDictionary dictionary];
// Example: add your local files here
NSArray *localFiles = @[
@"/path/to/file1.jpg",
@"/path/to/file2.pdf",
@"/path/to/file3.zip"
];
NSString *objectKeyPrefix = @"uploads/";
for (NSString *filePath in localFiles) {
NSString *filename = [filePath lastPathComponent];
fileMap[filePath] = [NSString stringWithFormat:@"%@%@", objectKeyPrefix, filename];
}
// === Initialize OBS Client ===
OBSStaticCredentialProvider *credentialProvider = [[OBSStaticCredentialProvider alloc] initWithAccessKey:AK secretKey:SK];
OBSServiceConfiguration *conf = [[OBSServiceConfiguration alloc] initWithURLString:endPoint credentialProvider:credentialProvider];
// Optionally increase concurrent upload limit
conf.maxConcurrentUploadRequestCount = concurrentCount;
OBSClient *client = [[OBSClient alloc] initWithConfiguration:conf];
// === Batch Upload ===
BatchUploader *uploader = [[BatchUploader alloc] initWithClient:client
bucketName:bucketName
concurrentCount:concurrentCount];
[uploader uploadFiles:fileMap waitUntilDone:YES];
}
return 0;
}
In the current SDK, the uploadFilePath parameter of the file upload APIs (OBSPutObjectWithFileRequest and OBSUploadFileRequest) only accepts file paths of the NSString type. Directly passing an NSURL object created with [NSURL fileURLWithPath:@"xxx"] is not supported. To use NSURL, obtain the file path string through [fileURL path] and then pass it to the uploadFilePath parameter.
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