更新时间: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参数。

相关文档