wake-ios/wake/View/Components/Upload/ImageUploaderGetID.swift
2025-09-02 20:31:13 +08:00

568 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
///
/// file_id
public class ImageUploaderGetID: ObservableObject {
// MARK: -
///
public struct UploadResult: Codable {
public let fileUrl: String
public let fileName: String
public let fileSize: Int
public let fileId: String
public let previewFileId: String?
public init(fileUrl: String, fileName: String, fileSize: Int, fileId: String, previewFileId: String? = nil) {
self.fileUrl = fileUrl
self.fileName = fileName
self.fileSize = fileSize
self.fileId = fileId
self.previewFileId = previewFileId
}
}
///
public enum UploadError: LocalizedError {
case invalidImageData
case invalidURL
case serverError(String)
case invalidResponse
case uploadFailed(Error?)
case invalidFileId
case invalidResponseData
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 "无效的响应数据"
}
}
}
// MARK: -
private let session: URLSession
private let apiConfig: APIConfig.Type
// MARK: -
///
/// - Parameters:
/// - session: URLSession
/// - apiConfig: API
public init(session: URLSession = .shared, apiConfig: APIConfig.Type = APIConfig.self) {
self.session = session
self.apiConfig = apiConfig
}
// MARK: -
///
/// - Parameters:
/// - image:
/// - progress: (0.0 1.0)
/// - completion:
public func uploadImage(
_ image: UIImage,
progress: @escaping (Double) -> Void,
completion: @escaping (Result<UploadResult, Error>) -> Void
) {
print("🔄 开始准备上传图片...")
// 1. Data
guard let imageData = image.jpegData(compressionQuality: 0.7) else {
let error = UploadError.invalidImageData
print("❌ 错误:\(error.localizedDescription)")
completion(.failure(error))
return
}
// 2. URL
getUploadURL(
for: imageData,
mimeType: "image/jpeg",
originalFilename: "image_\(UUID().uuidString).jpg"
) { [weak self] result in
switch result {
case .success((let fileId, let uploadURL)):
print("📤 获取到上传URL开始上传文件...")
// 3.
_ = self?.uploadFile(
fileData: imageData,
to: uploadURL,
mimeType: "image/jpeg",
onProgress: { uploadProgress in
print("📊 上传进度: \(Int(uploadProgress * 100))%")
progress(uploadProgress)
},
completion: { uploadResult in
switch uploadResult {
case .success:
// 4.
self?.confirmUpload(
fileId: fileId,
fileName: "image_\(UUID().uuidString).jpg",
fileSize: imageData.count,
completion: completion
)
case .failure(let error):
print("❌ 文件上传失败: \(error.localizedDescription)")
completion(.failure(error))
}
}
)
case .failure(let error):
print("❌ 获取上传URL失败: \(error.localizedDescription)")
completion(.failure(error))
}
}
}
// MARK: - Video Upload
///
/// - Parameters:
/// - videoURL: URL
/// - progress: (0.0 1.0)
/// - completion:
public func uploadVideo(
_ videoURL: URL,
progress: @escaping (Double) -> Void,
completion: @escaping (Result<UploadResult, Error>) -> Void
) {
print("🔄 开始准备上传视频...")
// 1.
do {
let videoData = try Data(contentsOf: videoURL)
let fileExtension = videoURL.pathExtension.lowercased()
let mimeType: String
// MIME
switch fileExtension {
case "mp4":
mimeType = "video/mp4"
case "mov":
mimeType = "video/quicktime"
case "m4v":
mimeType = "video/x-m4v"
case "avi":
mimeType = "video/x-msvideo"
default:
mimeType = "video/mp4" // 使mp4
}
// 2. URL
getUploadURL(
for: videoData,
mimeType: mimeType,
originalFilename: videoURL.lastPathComponent
) { [weak self] result in
switch result {
case .success((let fileId, let uploadURL)):
print("📤 获取到视频上传URL开始上传文件...")
// 3.
_ = self?.uploadFile(
fileData: videoData,
to: uploadURL,
mimeType: mimeType,
onProgress: progress,
completion: { result in
switch result {
case .success:
// 4.
self?.confirmUpload(
fileId: fileId,
fileName: videoURL.lastPathComponent,
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(error))
}
}
// MARK: -
/// URL
private func getUploadURL(
for fileData: Data,
mimeType: String,
originalFilename: String? = nil,
completion: @escaping (Result<(fileId: String, uploadURL: URL), Error>) -> Void
) {
let fileName = originalFilename ?? "file_\(UUID().uuidString)"
let parameters: [String: Any] = [
"filename": fileName,
"content_type": mimeType,
"file_size": fileData.count
]
let urlString = "\(apiConfig.baseURL)/file/generate-upload-url"
print("🌐 准备请求上传URL...")
print(" - 目标URL: \(urlString)")
print(" - 文件名: \(fileName)")
print(" - 文件大小: \(Double(fileData.count) / 1024.0) KB")
print(" - MIME类型: \(mimeType)")
guard let url = URL(string: urlString) else {
print("❌ 错误: 无效的URL: \(urlString)")
completion(.failure(UploadError.invalidURL))
return
}
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.allHTTPHeaderFields = apiConfig.authHeaders
do {
request.httpBody = try JSONSerialization.data(withJSONObject: parameters)
if let bodyString = String(data: request.httpBody ?? Data(), encoding: .utf8) {
print("📤 请求体: \(bodyString)")
}
} catch {
print("❌ 序列化请求参数失败: \(error.localizedDescription)")
completion(.failure(error))
return
}
let task = session.dataTask(with: request) { data, response, error in
if let error = error {
print("❌ 获取上传URL请求失败: \(error.localizedDescription)")
completion(.failure(UploadError.uploadFailed(error)))
return
}
guard let httpResponse = response as? HTTPURLResponse else {
print("❌ 无效的服务器响应")
completion(.failure(UploadError.invalidResponse))
return
}
print("📥 收到上传URL响应")
print(" - 状态码: \(httpResponse.statusCode)")
guard let data = data else {
print("❌ 响应数据为空")
completion(.failure(UploadError.invalidResponse))
return
}
//
print(" - 响应头: \(httpResponse.allHeaderFields)")
//
if let responseString = String(data: data, encoding: .utf8) {
print(" - 响应体: \(responseString)")
}
do {
let json = try JSONSerialization.jsonObject(with: data) as? [String: Any]
print(" - 解析的JSON: \(String(describing: json))")
guard let code = json?["code"] as? Int, code == 0 else {
let errorMessage = json?["message"] as? String ?? "未知错误"
print("❌ 服务器返回错误: \(errorMessage)")
completion(.failure(UploadError.serverError(errorMessage)))
return
}
guard let dataDict = json?["data"] as? [String: Any],
let fileId = dataDict["file_id"] as? String,
let uploadURLString = dataDict["upload_url"] as? String,
let uploadURL = URL(string: uploadURLString) else {
print("❌ 响应数据格式错误")
completion(.failure(UploadError.invalidResponse))
return
}
print("✅ 成功获取上传URL")
print(" ❗️❗️❗️❗️❗️❗️❗️❗️❗️❗️❗️❗️❗️❗️❗️❗️❗️❗️❗️ - 文件ID: \(fileId)")
print(" - 上传URL: \(uploadURLString)")
completion(.success((fileId: fileId, uploadURL: uploadURL)))
} catch {
print("❌ 解析响应数据失败: \(error.localizedDescription)")
completion(.failure(UploadError.invalidResponse))
}
}
task.resume()
}
///
private func confirmUpload(
fileId: String,
fileName: String,
fileSize: Int,
completion: @escaping (Result<UploadResult, Error>) -> Void
) {
let endpoint = "\(apiConfig.baseURL)/file/confirm-upload"
guard let url = URL(string: endpoint) else {
completion(.failure(UploadError.invalidURL))
return
}
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.allHTTPHeaderFields = apiConfig.authHeaders
let body: [String: Any] = [
"file_id": fileId,
"file_name": fileName,
"file_size": fileSize
]
do {
request.httpBody = try JSONSerialization.data(withJSONObject: body)
print("📤 确认上传请求fileId: \(fileId), 文件名: \(fileName)")
} catch {
print("❌ 序列化确认上传参数失败: \(error.localizedDescription)")
completion(.failure(error))
return
}
let task = session.dataTask(with: request) { data, response, error in
if let error = error {
print("❌ 确认上传请求失败: \(error.localizedDescription)")
completion(.failure(UploadError.uploadFailed(error)))
return
}
guard let httpResponse = response as? HTTPURLResponse else {
print("❌ 无效的服务器响应")
completion(.failure(UploadError.invalidResponse))
return
}
guard (200...299).contains(httpResponse.statusCode) else {
let statusCode = httpResponse.statusCode
let errorMessage = "确认上传失败,状态码: \(statusCode)"
print("\(errorMessage)")
completion(.failure(UploadError.serverError(errorMessage)))
return
}
//
let uploadResult = UploadResult(
fileUrl: "\(self.apiConfig.baseURL)/files/\(fileId)",
fileName: fileName,
fileSize: fileSize,
fileId: fileId
)
print("✅ 图片上传并确认成功fileId: \(fileId)")
completion(.success(uploadResult))
}
task.resume()
}
/// URL
public func uploadFile(
fileData: Data,
to uploadURL: URL,
mimeType: String,
onProgress: @escaping (Double) -> Void,
completion: @escaping (Result<Void, Error>) -> Void
) -> URLSessionUploadTask {
print("📤 开始上传文件...")
var request = URLRequest(url: uploadURL)
request.httpMethod = "PUT"
request.setValue(mimeType, forHTTPHeaderField: "Content-Type")
let task = session.uploadTask(
with: request,
from: fileData
) { data, response, error in
if let error = error {
print("❌ 文件上传失败: \(error.localizedDescription)")
completion(.failure(UploadError.uploadFailed(error)))
return
}
guard let httpResponse = response as? HTTPURLResponse else {
print("❌ 无效的响应")
completion(.failure(UploadError.invalidResponse))
return
}
guard (200...299).contains(httpResponse.statusCode) else {
let statusCode = httpResponse.statusCode
print("❌ 服务器返回错误状态码: \(statusCode)")
completion(.failure(UploadError.serverError("HTTP \(statusCode)")))
return
}
print("✅ 文件上传成功")
completion(.success(()))
}
//
let progressObserver = task.progress.observe(\.fractionCompleted) { progress, _ in
let percentComplete = progress.fractionCompleted
print("📊 文件上传进度: \(Int(percentComplete * 100))%")
onProgress(percentComplete)
}
//
objc_setAssociatedObject(task, &AssociatedKeys.progressObserver, progressObserver, .OBJC_ASSOCIATION_RETAIN)
task.resume()
return task
}
private struct AssociatedKeys {
static var progressObserver = "progressObserver"
}
// MARK: -
///
public struct FileStatus {
public let file: Data
public var status: UploadStatus
public var progress: Double
public enum UploadStatus {
case pending
case uploading
case completed
case failed(Error)
}
public init(file: Data, status: UploadStatus = .pending, progress: Double = 0) {
self.file = file
self.status = status
self.progress = progress
}
}
}
// 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 {
// iOS 11 使
let name = NSNotification.Name("TaskCompleted\(self.taskIdentifier)")
NotificationCenter.default.addObserver(
forName: name,
object: self,
queue: .main
) { _ in
handler()
}
}
}
}
// MARK: -
struct UploadURLResponse: Codable {
let code: Int
let message: String
let data: UploadData
struct UploadData: Codable {
let fileId: String
let uploadUrl: String
enum CodingKeys: String, CodingKey {
case fileId = "file_id"
case uploadUrl = "upload_url"
}
}
}