// // QiniuUploader.swift // ylgamehall // // 七牛上传 actor wrapper。包 `QNUploadManager` 为 async/await。 // // ⚠️ canImport(QiniuSDK) 守卫:在 Xcode 把 https://github.com/qiniu/objc-sdk // 添加到 Swift Package Dependencies 之前,本文件编译为 no-op,upload 永远抛 // QiniuUploadError.sdkNotLinked。后续 handler 可降级到 stub 路径。 // // 上传完成后返回 UploadedFile,字段映射给 H5 `getaudiourl` / `recordSuccess`。 // import Foundation #if canImport(QiniuSDK) import QiniuSDK #endif public struct UploadedFile: Sendable { public let fileUrl: String // 公网访问 URL(http://{domain}/{key}) public let fileName: String // 原始本地文件名 public let fileKey: String // 七牛 key(path-on-cdn) public let timeSec: Int // 录音时长(秒) } public enum QiniuUploadError: Error, Sendable { case sdkNotLinked case fileNotFound(String) case uploadFailed(any Error) case responseInvalid } public actor QiniuUploader { public static let shared = QiniuUploader() public init() {} /// 上传文件。 /// - Parameters: /// - localFile: 本地文件 URL(amr 录音文件) /// - timeSec: 录音时长,回传给 H5 `getaudiourl` 用 /// - Returns: UploadedFile(含公网 URL) public func upload(_ localFile: URL, timeSec: Int) async throws -> UploadedFile { #if canImport(QiniuSDK) let fm = FileManager.default guard fm.fileExists(atPath: localFile.path) else { throw QiniuUploadError.fileNotFound(localFile.path) } // 生成 key:`{recordingDirectory}{uuid}.{ext}`(与 msext QiniuManager.m:55-60 等价) let uuid = UUID().uuidString.lowercased().replacingOccurrences(of: "-", with: "") let ext = localFile.pathExtension let key = QiniuConfig.recordingDirectory + uuid + (ext.isEmpty ? "" : ".\(ext)") // 客户端自签 token(参 QiniuTokenSigner / msext QiniuManager 同款算法) let token = QiniuTokenSigner.uploadToken(key: key) return try await withCheckedThrowingContinuation { (cont: CheckedContinuation) in let mgr = QNUploadManager() let fileName = localFile.lastPathComponent mgr.putFile( localFile.path, key: key, token: token, complete: { info, savedKey, _ in if let info, info.isOK, let savedKey { let url = QiniuConfig.publicURL(forKey: savedKey) cont.resume(returning: UploadedFile( fileUrl: url, fileName: fileName, fileKey: savedKey, timeSec: timeSec )) } else if let error = info?.error { cont.resume(throwing: QiniuUploadError.uploadFailed(error)) } else { cont.resume(throwing: QiniuUploadError.responseInvalid) } }, option: nil ) } #else throw QiniuUploadError.sdkNotLinked #endif } /// 取消所有进行中任务(接 BackGameDataHandler 清理钩子用)。 /// QNUploadManager 没有「取消全部」API,单任务用 QNUploadOption.cancellationSignal 实现; /// 当前实现以 task 粒度由 Swift Task.cancel 传递(withCheckedThrowingContinuation /// 已在 cancellation 时 throw CancellationError),无需额外状态。 public func cancelInFlight() { // no-op(依赖 Task.cancel 传播);如未来需要硬 cancel,改为持有 QNUploadOption 并触发 cancellationSignal } }