wake-ios/wake/View/Components/Upload/ImageUploaderGetID.swift
2025-08-25 18:58:10 +08:00

560 lines
20 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import SwiftUI
import PhotosUI
import CommonCrypto
///
/// file_id
public class ImageUploaderGetID: ObservableObject {
// MARK: -
private let session: URLSession
private let networkService: NetworkServiceProtocol
private let networkHandler: (_ path: String, _ parameters: [String: Any], _ completion: @escaping (Result<Data, UploadError>) -> Void) -> Void
public init(session: URLSession = .shared, networkService: NetworkServiceProtocol? = nil) {
self.session = session
let service = networkService ?? NetworkService.shared
self.networkService = service
self.networkHandler = { path, parameters, completion in
service.postWithToken(
path: path,
parameters: parameters,
completion: { (result: Result<UploadURLResponse, NetworkError>) in
switch result {
case .success(let response):
// Convert the response back to Data for the completion handler
do {
let data = try JSONEncoder().encode(response)
completion(.success(data))
} catch {
completion(.failure(.invalidResponseData))
}
case .failure(let error):
completion(.failure(.networkError(error)))
}
}
)
}
}
///
/// - Parameters:
/// - session: URLSession
/// - networkService: NetworkService
/// - networkHandler:
internal init(
session: URLSession,
networkService: NetworkServiceProtocol? = nil,
networkHandler: @escaping (String, [String: Any], @escaping (Result<Data, UploadError>) -> Void) -> Void
) {
self.session = session
self.networkService = networkService ?? NetworkService.shared
self.networkHandler = networkHandler
}
///
public struct UploadResult: Codable {
public let fileUrl: String
public let fileName: String
public let fileSize: Int
public let fileId: String
public init(fileUrl: String, fileName: String, fileSize: Int, fileId: String) {
self.fileUrl = fileUrl
self.fileName = fileName
self.fileSize = fileSize
self.fileId = fileId
}
}
///
public enum UploadError: LocalizedError {
case invalidImageData
case invalidURL
case serverError(String)
case invalidResponse
case uploadFailed(Error?)
case invalidFileId
case invalidResponseData
case networkError(Error)
case decodingError(Error)
case unauthorized
public var errorDescription: String? {
switch self {
case .invalidImageData:
return "无效的图片数据"
case .invalidURL:
return "无效的URL"
case .serverError(let message):
return "服务器错误: \(message)"
case .invalidResponse:
return "无效的服务器响应"
case .uploadFailed(let error):
return "上传失败: \(error?.localizedDescription ?? "未知错误")"
case .invalidFileId:
return "无效的文件ID"
case .invalidResponseData:
return "无效的响应数据"
case .networkError(let error):
return "网络错误: \(error.localizedDescription)"
case .decodingError(let error):
return "解码错误: \(error.localizedDescription)"
case .unauthorized:
return "认证失败,需要重新登录"
}
}
}
// MARK: -
///
public func uploadImage(
_ image: UIImage,
progress: @escaping (Double) -> Void,
completion: @escaping (Result<UploadResult, UploadError>) -> Void
) {
print("🔄 开始准备上传图片...")
guard let imageData = image.jpegData(compressionQuality: 0.7) else {
let error = UploadError.invalidImageData
print("❌ 错误:\(error.localizedDescription)")
completion(.failure(error))
return
}
requestUploadURL(fileName: "image_\(UUID().uuidString).jpg", fileData: imageData) { [weak self] result in
guard let self = self else { return }
switch result {
case .success(let response):
print("📤 获取到上传URL开始上传文件...")
guard let uploadURL = URL(string: response.data.uploadUrl) else {
let error = UploadError.invalidURL
print("❌ [ImageUploader] 上传URL格式无效: \(response.data.uploadUrl)")
completion(.failure(error))
return
}
self.uploadFile(
fileData: imageData,
to: uploadURL,
mimeType: "image/jpeg",
onProgress: { uploadProgress in
print("📊 上传进度: \(Int(uploadProgress * 100))%")
progress(uploadProgress)
},
completion: { [weak self] uploadResult in
switch uploadResult {
case .success:
self?.confirmUpload(
fileId: response.data.fileId,
fileName: "image_\(UUID().uuidString).jpg",
fileSize: imageData.count,
completion: completion
)
case .failure(let error):
completion(.failure(error))
}
}
)
case .failure(let error):
completion(.failure(error))
}
}
}
///
/// - Parameters:
/// - videoURL: URL
/// - progress: (0.0 1.0)
/// - completion:
public func uploadVideo(
_ videoURL: URL,
progress: @escaping (Double) -> Void,
completion: @escaping (Result<UploadResult, UploadError>) -> Void
) {
print("🎥 开始准备上传视频...")
do {
let videoData = try Data(contentsOf: videoURL)
let fileName = "video_\(UUID().uuidString).mp4"
requestUploadURL(
fileName: fileName,
fileData: videoData
) { [weak self] result in
guard let self = self else { return }
switch result {
case .success(let response):
print("📤 获取到视频上传URL开始上传文件...")
guard let uploadURL = URL(string: response.data.uploadUrl) else {
let error = UploadError.invalidURL
print("❌ [ImageUploader] 视频上传URL格式无效: \(response.data.uploadUrl)")
completion(.failure(error))
return
}
self.uploadFile(
fileData: videoData,
to: uploadURL,
mimeType: "video/mp4",
onProgress: progress,
completion: { [weak self] uploadResult in
guard let self = self else { return }
switch uploadResult {
case .success:
self.confirmUpload(
fileId: response.data.fileId,
fileName: fileName,
fileSize: videoData.count,
completion: completion
)
case .failure(let error):
completion(.failure(error))
}
}
)
case .failure(let error):
completion(.failure(error))
}
}
} catch {
print("❌ 读取视频文件失败: \(error.localizedDescription)")
completion(.failure(.uploadFailed(error)))
}
}
// MARK: -
///
private func request<T: Decodable>(
path: String,
parameters: [String: Any],
completion: @escaping (Result<T, UploadError>) -> Void
) {
networkHandler(path, parameters) { (result: Result<Data, UploadError>) in
switch result {
case .success(let data):
do {
let decoded = try JSONDecoder().decode(T.self, from: data)
completion(.success(decoded))
} catch {
print("❌ [ImageUploader] 解析响应失败: \(error)")
completion(.failure(.invalidResponseData))
}
case .failure(let error):
print("❌ [ImageUploader] 网络请求失败: \(error)")
completion(.failure(error))
}
}
}
/// URL
private func requestUploadURL(
fileName: String,
fileData: Data,
completion: @escaping (Result<UploadURLResponse, UploadError>) -> Void
) {
let parameters: [String: Any] = [
"filename": fileName,
"content_type": "image/jpeg",
"file_size": fileData.count
]
print("""
📝 [ImageUploader] 开始请求上传URL
文件名: \(fileName)
📏 文件大小: \(fileData.count) 字节
📋 参数: \(parameters)
""")
request(path: "/file/generate-upload-url", parameters: parameters, completion: completion)
}
/// URL
public func uploadFile(
fileData: Data,
to uploadURL: URL,
mimeType: String = "application/octet-stream",
onProgress: @escaping (Double) -> Void,
completion: @escaping (Result<Void, UploadError>) -> Void
) -> URLSessionUploadTask? {
print("""
⬆️ [ImageUploader] 开始上传文件
🔗 目标URL: \(uploadURL.absoluteString)
📏 文件大小: \(fileData.count) 字节
📋 MIME类型: \(mimeType)
""")
//
var request = URLRequest(url: uploadURL)
request.httpMethod = "PUT"
//
var headers: [String: String] = [
"Content-Type": mimeType,
"Content-Length": String(fileData.count)
]
//
if let token = KeychainHelper.getAccessToken() {
headers["Authorization"] = "Bearer \(token)"
print("🔑 [ImageUploader] 添加认证头Token: \(token.prefix(10))...")
} else {
print("⚠️ [ImageUploader] 未找到认证Token")
}
//
headers.forEach { key, value in
request.setValue(value, forHTTPHeaderField: key)
}
//
let task = networkService.upload(
request: request,
fileData: fileData,
onProgress: onProgress,
completion: { [weak self] result in
guard let self = self else { return }
switch result {
case .success(let data, let response):
guard let httpResponse = response as? HTTPURLResponse else {
print("❌ [ImageUploader] 无效的响应")
completion(.failure(.invalidResponse))
return
}
let statusCode = httpResponse.statusCode
let responseBody = data.flatMap { String(data: $0, encoding: .utf8) } ?? ""
print("""
📡 [ImageUploader] 服务器响应
🔢 状态码: \(statusCode)
🔗 请求URL: \(uploadURL.absoluteString)
📦 响应: \(responseBody)
""")
switch statusCode {
case 200...299:
print("✅ [ImageUploader] 文件上传成功")
completion(.success(()))
case 401:
print("🔑 [ImageUploader] 认证失败,需要重新登录")
completion(.failure(.unauthorized))
case 403:
let errorMessage = "访问被拒绝 (403) - URL: \(uploadURL.absoluteString)"
print("❌ [ImageUploader] \(errorMessage)")
completion(.failure(.serverError(errorMessage)))
default:
let errorMessage = "服务器返回错误: \(statusCode)"
print("❌ [ImageUploader] \(errorMessage)")
completion(.failure(.serverError(errorMessage)))
}
case .failure(let error):
print("❌ [ImageUploader] 上传失败: \(error.localizedDescription)")
completion(.failure(.networkError(error)))
}
}
)
//
task?.resume()
return task
}
///
private func confirmUpload(
fileId: String,
fileName: String,
fileSize: Int,
completion: @escaping (Result<UploadResult, UploadError>) -> Void
) {
let parameters: [String: Any] = [
"file_id": fileId,
"file_name": fileName,
"file_size": fileSize
]
print("""
📨 [ImageUploader] 开始确认上传完成
📁 文件ID: \(fileId)
📝 文件名: \(fileName)
📏 文件大小: \(fileSize) 字节
📋 参数: \(parameters)
""")
struct ConfirmUploadResponse: Codable {
let code: Int
let message: String
let data: [String: String]?
}
request(path: "/file/confirm-upload", parameters: parameters) { (result: Result<ConfirmUploadResponse, UploadError>) in
switch result {
case .success(_):
let uploadResult = UploadResult(
fileUrl: "\(APIConfig.baseURL)/files/\(fileId)",
fileName: fileName,
fileSize: fileSize,
fileId: fileId
)
print("""
✅ [ImageUploader] 文件上传确认成功
📁 文件ID: \(fileId)
🔗 文件URL: \(uploadResult.fileUrl)
📝 文件名: \(fileName)
📏 文件大小: \(fileSize) 字节
""")
completion(.success(uploadResult))
case .failure(let error):
print("❌ [ImageUploader] 文件上传确认失败: \(error.localizedDescription)")
completion(.failure(error))
}
}
}
// MARK: -
private func calculateSpeed(bytes: Int, seconds: TimeInterval) -> String {
guard seconds > 0 else { return "0 KB/s" }
let bytesPerSecond = Double(bytes) / seconds
if bytesPerSecond >= 1024 * 1024 {
return String(format: "%.1f MB/s", bytesPerSecond / (1024 * 1024))
} else {
return String(format: "%.1f KB/s", bytesPerSecond / 1024)
}
}
}
// MARK: - Data Extension for MD5
extension Data {
func md5Base64EncodedString() -> String? {
#if canImport(CommonCrypto)
var digest = [UInt8](repeating: 0, count: Int(CC_MD5_DIGEST_LENGTH))
_ = self.withUnsafeBytes { (ptr: UnsafeRawBufferPointer) in
if let baseAddress = ptr.baseAddress, ptr.count > 0 {
CC_MD5(baseAddress, CC_LONG(self.count), &digest)
}
}
return Data(digest).base64EncodedString()
#else
return nil
#endif
}
}
// MARK: -
private struct UploadURLResponse: Codable {
let code: Int
let message: String?
let data: UploadData
struct UploadData: Codable {
let fileId: String
let filePath: String
let uploadUrl: String
let expiresIn: Int
enum CodingKeys: String, CodingKey {
case fileId = "file_id"
case filePath = "file_path"
case uploadUrl = "upload_url"
case expiresIn = "expires_in"
}
}
}
private struct EmptyResponse: Codable {}
// MARK: - URLSessionTask
private class TaskObserver: NSObject {
private weak var task: URLSessionTask?
private var handlers: [() -> Void] = []
init(task: URLSessionTask) {
self.task = task
super.init()
task.addObserver(self, forKeyPath: #keyPath(URLSessionTask.state), options: .new, context: nil)
}
func addHandler(_ handler: @escaping () -> Void) {
handlers.append(handler)
}
override func observeValue(
forKeyPath keyPath: String?,
of object: Any?,
change: [NSKeyValueChangeKey: Any]?,
context: UnsafeMutableRawPointer?
) {
guard keyPath == #keyPath(URLSessionTask.state),
let task = task,
task.state == .completed else {
return
}
DispatchQueue.main.async { [weak self] in
self?.handlers.forEach { $0() }
self?.cleanup()
}
}
private func cleanup() {
task?.removeObserver(self, forKeyPath: #keyPath(URLSessionTask.state))
handlers.removeAll()
}
deinit {
cleanup()
}
}
private extension URLSessionTask {
private static var taskObserverKey: UInt8 = 0
private var taskObserver: TaskObserver? {
get {
return objc_getAssociatedObject(self, &Self.taskObserverKey) as? TaskObserver
}
set {
objc_setAssociatedObject(self, &Self.taskObserverKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
}
func addCompletionHandler(_ handler: @escaping () -> Void) {
if #available(iOS 11.0, *) {
if let observer = taskObserver {
observer.addHandler(handler)
} else {
let observer = TaskObserver(task: self)
observer.addHandler(handler)
taskObserver = observer
}
} else {
let name = NSNotification.Name("TaskCompleted\(self.taskIdentifier)")
NotificationCenter.default.addObserver(
forName: name,
object: self,
queue: .main
) { _ in
handler()
}
}
}
}