// // QiniuManager.m // msext // // Created on June 15, 2025. // #import "QiniuManager.h" #import #import "QiniuConfig.h" #import #import #import @implementation QiniuManager + (instancetype)sharedManager { static QiniuManager *instance = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ instance = [[self alloc] init]; [instance setupQiniuSDK]; }); return instance; } - (void)setupQiniuSDK { // Qiniu SDK doesn't require explicit initialization // The configuration will be applied when creating the QNUploadManager instance } - (void)uploadAudioFile:(NSString *)filePath fileName:(NSString *)fileName progressHandler:(QiniuProgressHandler)progressHandler completionHandler:(QiniuUploadCompletionHandler)completionHandler { // 检查文件是否存在 if (![[NSFileManager defaultManager] fileExistsAtPath:filePath]) { NSError *error = [NSError errorWithDomain:@"QiniuManager" code:-1 userInfo:@{NSLocalizedDescriptionKey: @"上传文件不存在"}]; if (completionHandler) { completionHandler(nil, error); } return; } // 创建上传配置 QNConfiguration *config = [QNConfiguration build:^(QNConfigurationBuilder *builder) { builder.zone = [QNFixedZone zone2]; // 使用华东区域 builder.timeoutInterval = 60; // 超时设置,单位为秒 }]; // 生成上传策略 QNUploadManager *uploadManager = [[QNUploadManager alloc] initWithConfiguration:config]; NSString *token = [self generateUploadToken]; // 文件在七牛云存储中的完整路径 NSString *key = [NSString stringWithFormat:@"%@%@", kQiniuRecordingDirectory, fileName]; // 配置上传选项 QNUploadOption *option = [[QNUploadOption alloc] initWithMime:@"audio/amr" progressHandler:^(NSString *key, float percent) { if (progressHandler) { progressHandler(percent); } } params:nil checkCrc:NO cancellationSignal:nil]; // 执行上传 [uploadManager putFile:filePath key:key token:token complete:^(QNResponseInfo *info, NSString *key, NSDictionary *resp) { if (info.statusCode == 200) { if (completionHandler) { completionHandler(key, nil); } } else { NSError *error = [NSError errorWithDomain:@"QiniuManager" code:info.statusCode userInfo:@{NSLocalizedDescriptionKey: info.error.localizedDescription ?: @"上传失败"}]; if (completionHandler) { completionHandler(nil, error); } } } option:option]; } - (void)downloadAudioFile:(NSString *)key progressHandler:(QiniuProgressHandler)progressHandler completionHandler:(QiniuDownloadCompletionHandler)completionHandler { // 获取完整的下载URL NSString *urlString = [self getFileUrlWithKey:key]; NSURL *url = [NSURL URLWithString:urlString]; // 创建下载任务 NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration]; NSURLSession *session = [NSURLSession sessionWithConfiguration:configuration]; NSURLSessionDownloadTask *downloadTask = [session downloadTaskWithURL:url completionHandler:^(NSURL *location, NSURLResponse *response, NSError *error) { if (error) { if (completionHandler) { completionHandler(nil, error); } return; } // 获取临时文件URL NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response; if (httpResponse.statusCode != 200) { NSError *downloadError = [NSError errorWithDomain:@"QiniuManager" code:httpResponse.statusCode userInfo:@{NSLocalizedDescriptionKey: @"下载失败"}]; if (completionHandler) { completionHandler(nil, downloadError); } return; } // 从URL中获取文件名 NSString *fileName = [key lastPathComponent]; if ([fileName length] == 0) { fileName = [NSString stringWithFormat:@"qiniu_download_%@", [[NSUUID UUID] UUIDString]]; } // 创建目标路径 NSString *cachesDirectory = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES).firstObject; NSString *downloadDirectory = [cachesDirectory stringByAppendingPathComponent:@"QiniuDownloads"]; // 确保目录存在 NSFileManager *fileManager = [NSFileManager defaultManager]; if (![fileManager fileExistsAtPath:downloadDirectory]) { [fileManager createDirectoryAtPath:downloadDirectory withIntermediateDirectories:YES attributes:nil error:nil]; } // 构建最终路径 NSString *destinationPath = [downloadDirectory stringByAppendingPathComponent:fileName]; // 移动文件到目标路径 if ([fileManager fileExistsAtPath:destinationPath]) { [fileManager removeItemAtPath:destinationPath error:nil]; } NSError *moveError = nil; [fileManager moveItemAtURL:location toURL:[NSURL fileURLWithPath:destinationPath] error:&moveError]; if (moveError) { if (completionHandler) { completionHandler(nil, moveError); } } else { if (completionHandler) { completionHandler(destinationPath, nil); } } }]; // 添加进度追踪 if (progressHandler) { [downloadTask addObserver:self forKeyPath:@"countOfBytesReceived" options:NSKeyValueObservingOptionNew context:NULL]; objc_setAssociatedObject(downloadTask, "progressHandler", [progressHandler copy], OBJC_ASSOCIATION_COPY); objc_setAssociatedObject(downloadTask, "totalBytes", @(0), OBJC_ASSOCIATION_RETAIN); } // 启动下载任务 [downloadTask resume]; } - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { if ([object isKindOfClass:[NSURLSessionDownloadTask class]]) { NSURLSessionDownloadTask *task = (NSURLSessionDownloadTask *)object; if ([keyPath isEqualToString:@"countOfBytesReceived"]) { NSNumber *totalBytes = objc_getAssociatedObject(task, "totalBytes"); if ([totalBytes longLongValue] == 0 && task.countOfBytesExpectedToReceive > 0) { objc_setAssociatedObject(task, "totalBytes", @(task.countOfBytesExpectedToReceive), OBJC_ASSOCIATION_RETAIN); } float progress = 0; if (task.countOfBytesExpectedToReceive > 0) { progress = (float)task.countOfBytesReceived / (float)task.countOfBytesExpectedToReceive; } QiniuProgressHandler progressHandler = objc_getAssociatedObject(task, "progressHandler"); if (progressHandler) { dispatch_async(dispatch_get_main_queue(), ^{ progressHandler(progress); }); } } } } - (NSString *)getFileUrlWithKey:(NSString *)key { return [NSString stringWithFormat:@"%@/%@", kQiniuDomain, key]; } #pragma mark - Private Methods - (NSString *)generateUploadToken { // 构建上传策略(putPolicy) NSMutableDictionary *policy = [NSMutableDictionary dictionary]; // 指定上传的目标资源空间(确保完整格式为bucketName或bucketName:keyPrefix) NSString *scope = [NSString stringWithFormat:@"%@", kQiniuBucketName]; [policy setObject:scope forKey:@"scope"]; // 上传策略的过期时间(1小时) NSInteger deadline = (NSInteger)[[NSDate dateWithTimeIntervalSinceNow:3600] timeIntervalSince1970]; [policy setObject:@(deadline) forKey:@"deadline"]; // 将上传策略转换为JSON NSError *error = nil; NSData *jsonData = [NSJSONSerialization dataWithJSONObject:policy options:0 error:&error]; if (error) { NSLog(@"生成JSON数据失败: %@", error); return nil; } // Base64编码JSON数据 NSString *encodedPolicy = [self urlsafeBase64EncodeData:jsonData]; // 使用HMAC-SHA1算法进行签名(注意签名的内容只是encodedPolicy) NSData *signData = [encodedPolicy dataUsingEncoding:NSUTF8StringEncoding]; NSString *encodedSign = [self hmacSha1:kQiniuSecretKey data:signData]; // 构造上传凭证 - 格式为: accessKey:encodedSign:encodedPolicy NSString *uploadToken = [NSString stringWithFormat:@"%@:%@:%@", kQiniuAccessKey, encodedSign, encodedPolicy]; // NSLog(@"七牛云配置信息 - AccessKey: %@, BucketName: %@, Domain: %@", // kQiniuAccessKey, kQiniuBucketName, kQiniuDomain); // NSLog(@"生成的上传凭证: %@", uploadToken); return uploadToken; } #pragma mark - Utility Methods - (NSString *)urlsafeBase64EncodeData:(NSData *)data { NSString *base64 = [data base64EncodedStringWithOptions:0]; base64 = [base64 stringByReplacingOccurrencesOfString:@"+" withString:@"-"]; base64 = [base64 stringByReplacingOccurrencesOfString:@"/" withString:@"_"]; return base64; } - (NSString *)hmacSha1:(NSString *)key data:(NSData *)data { const char *cKey = [key cStringUsingEncoding:NSUTF8StringEncoding]; const char *cData = [data bytes]; unsigned char cHMAC[CC_SHA1_DIGEST_LENGTH]; CCHmac(kCCHmacAlgSHA1, cKey, strlen(cKey), cData, [data length], cHMAC); NSData *hmacData = [[NSData alloc] initWithBytes:cHMAC length:sizeof(cHMAC)]; return [self urlsafeBase64EncodeData:hmacData]; } @end