更新时间:2026-08-28 GMT+08:00
批量上传
本示例用于批量上传本地文件到OBS桶中。您可以指定本地文件路径和目标对象Key的映射关系,以指定并发数并发上传,上传过程实时显示每个文件的进度和ETag,适用于批量迁移、资料同步等场景。
#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) {
// 注意:每次迭代enter一次,leave在两条互斥路径各一次(信号量超时跳过 或 上传完成回调),enter/leave配对,删除任一leave会导致group永久挂起
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;
// 注意:上传前建议检查文件可读性 isReadableFileAtPath,避免因权限不足导致读取失败
// 注意:fileExistsAtPath仅检查存在性,文件可能在上传过程中被其他进程修改,建议加文件锁或拷贝后上传
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;
}
// 注意:OBS PutObject单文件上限5GB,大文件请使用OBSUploadFileRequest断点续传
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";
// 认证用的ak和sk硬编码到代码中或者明文存储都有很大的安全风险,建议在配置文件或者环境变量中密文存放,使用时解密,确保安全;本示例以ak和sk保存在环境变量中为例,运行本示例前请先在本地环境中设置环境变量AccessKeyID和SecretAccessKey。
// 您可以登录访问管理控制台获取访问密钥AK/SK,获取方式请参见https://support.huaweicloud.com/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;
}
当前SDK的文件上传接口(OBSPutObjectWithFileRequest、OBSUploadFileRequest)的uploadFilePath参数仅接受NSString类型的文件路径,不支持直接传入[NSURL fileURLWithPath:@"xxx"]构造的NSURL对象。如需使用NSURL,请先通过[fileURL path]获取文件路径字符串,再传入uploadFilePath参数。
父主题: 上传对象