259 lines
10 KiB
Objective-C
259 lines
10 KiB
Objective-C
//
|
||
// QiniuManager.m
|
||
// msext
|
||
//
|
||
// Created on June 15, 2025.
|
||
//
|
||
|
||
#import "QiniuManager.h"
|
||
#import <QiniuSDK.h>
|
||
#import "QiniuConfig.h"
|
||
#import <objc/runtime.h>
|
||
#import <CommonCrypto/CommonHMAC.h>
|
||
#import <CommonCrypto/CommonDigest.h>
|
||
#import "FuncPublic.h"
|
||
|
||
@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 {
|
||
// 域名已在AppDelegate启动时从文件夹读取并赋值给kQiniuDomain
|
||
return [NSString stringWithFormat:@"http://%@/%@", 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
|