wake-ios/wake/View/Components/Upload/ImageUploaderGetID.swift
2025-08-21 19:38:06 +08:00

589 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: -
///
<<<<<<< HEAD
<<<<<<< HEAD
public struct UploadResult: Codable {
=======
public struct UploadResult {
>>>>>>> a207b78 (feat: )
=======
public struct UploadResult: Codable {
>>>>>>> 5edee64 (feat: )
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
<<<<<<< HEAD
<<<<<<< HEAD
case invalidResponseData
=======
>>>>>>> a207b78 (feat: )
=======
case invalidResponseData
>>>>>>> 5edee64 (feat: )
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"
<<<<<<< HEAD
<<<<<<< HEAD
case .invalidResponseData:
return "无效的响应数据"
=======
>>>>>>> a207b78 (feat: )
=======
case .invalidResponseData:
return "无效的响应数据"
>>>>>>> 5edee64 (feat: )
}
}
}
// 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:
<<<<<<< HEAD
<<<<<<< HEAD
=======
>>>>>>> 8e641fd (feat: )
/// - progress: (0.0 1.0)
/// - completion:
public func uploadImage(
_ image: UIImage,
progress: @escaping (Double) -> Void,
completion: @escaping (Result<UploadResult, Error>) -> Void
) {
<<<<<<< HEAD
=======
/// - completion: Result
public func uploadImage(_ image: UIImage, completion: @escaping (Result<UploadResult, Error>) -> Void) {
>>>>>>> a207b78 (feat: )
=======
>>>>>>> 8e641fd (feat: )
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) { [weak self] result in
switch result {
case .success((let fileId, let uploadURL)):
<<<<<<< HEAD
<<<<<<< HEAD
=======
>>>>>>> 8e641fd (feat: )
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: "avatar_\(UUID().uuidString).jpg",
fileSize: imageData.count,
completion: completion
)
case .failure(let error):
print("❌ 文件上传失败: \(error.localizedDescription)")
completion(.failure(error))
}
}
)
<<<<<<< HEAD
case .failure(let error):
print("❌ 获取上传URL失败: \(error.localizedDescription)")
=======
// 3.
self?.confirmUpload(fileId: fileId, fileName: "avatar_\(UUID().uuidString).jpg", fileSize: imageData.count) { confirmResult in
completion(confirmResult)
}
case .failure(let error):
>>>>>>> a207b78 (feat: )
=======
case .failure(let error):
print("❌ 获取上传URL失败: \(error.localizedDescription)")
>>>>>>> 8e641fd (feat: )
completion(.failure(error))
}
}
}
// MARK: -
/// URL
private func getUploadURL(for imageData: Data, completion: @escaping (Result<(fileId: String, uploadURL: URL), Error>) -> Void) {
let fileName = "avatar_\(UUID().uuidString).jpg"
let parameters: [String: Any] = [
"filename": fileName,
"content_type": "image/jpeg",
"file_size": imageData.count
]
let urlString = "\(apiConfig.baseURL)/file/generate-upload-url"
guard let url = URL(string: urlString) else {
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)
print("📤 准备上传请求,文件名: \(fileName), 大小: \(Double(imageData.count) / 1024.0) KB")
} catch {
print("❌ 序列化请求参数失败: \(error.localizedDescription)")
completion(.failure(error))
return
}
let task = session.dataTask(with: request) { data, response, error in
if let error = error {
completion(.failure(UploadError.uploadFailed(error)))
return
}
guard let httpResponse = response as? HTTPURLResponse else {
completion(.failure(UploadError.invalidResponse))
return
}
guard let data = data else {
completion(.failure(UploadError.invalidResponse))
return
}
//
if let responseString = String(data: data, encoding: .utf8) {
print("📥 获取上传URL响应: \(responseString)")
}
do {
guard let json = try JSONSerialization.jsonObject(with: data) as? [String: Any],
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 {
throw UploadError.invalidResponse
}
completion(.success((fileId: fileId, uploadURL: uploadURL)))
} catch {
completion(.failure(UploadError.invalidResponse))
}
}
task.resume()
}
///
private func confirmUpload(fileId: String, fileName: String, fileSize: Int, completion: @escaping (Result<UploadResult, Error>) -> Void) {
<<<<<<< HEAD
<<<<<<< HEAD
let endpoint = "\(apiConfig.baseURL)/file/confirm-upload"
guard let url = URL(string: endpoint) else {
=======
let urlString = "\(apiConfig.baseURL)/file/confirm-upload"
guard let url = URL(string: urlString) else {
>>>>>>> a207b78 (feat: )
=======
let endpoint = "\(apiConfig.baseURL)/file/confirm-upload"
guard let url = URL(string: endpoint) else {
>>>>>>> 5edee64 (feat: )
completion(.failure(UploadError.invalidURL))
return
}
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.allHTTPHeaderFields = apiConfig.authHeaders
<<<<<<< HEAD
<<<<<<< HEAD
=======
>>>>>>> 5edee64 (feat: )
let body: [String: Any] = [
"file_id": fileId,
"file_name": fileName,
"file_size": fileSize
]
<<<<<<< HEAD
do {
request.httpBody = try JSONSerialization.data(withJSONObject: body)
print("📤 确认上传请求fileId: \(fileId), 文件名: \(fileName)")
} catch {
print("❌ 序列化确认上传参数失败: \(error.localizedDescription)")
=======
let requestBody: [String: Any] = ["file_id": fileId]
=======
>>>>>>> 5edee64 (feat: )
do {
request.httpBody = try JSONSerialization.data(withJSONObject: body)
print("📤 确认上传请求fileId: \(fileId), 文件名: \(fileName)")
} catch {
<<<<<<< HEAD
>>>>>>> a207b78 (feat: )
=======
print("❌ 序列化确认上传参数失败: \(error.localizedDescription)")
>>>>>>> 5edee64 (feat: )
completion(.failure(error))
return
}
let task = session.dataTask(with: request) { data, response, error in
if let error = error {
<<<<<<< HEAD
<<<<<<< HEAD
print("❌ 确认上传请求失败: \(error.localizedDescription)")
=======
>>>>>>> a207b78 (feat: )
=======
print("❌ 确认上传请求失败: \(error.localizedDescription)")
>>>>>>> 5edee64 (feat: )
completion(.failure(UploadError.uploadFailed(error)))
return
}
<<<<<<< HEAD
<<<<<<< HEAD
=======
>>>>>>> 5edee64 (feat: )
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)))
<<<<<<< HEAD
=======
guard let httpResponse = response as? HTTPURLResponse,
(200...299).contains(httpResponse.statusCode) else {
completion(.failure(UploadError.serverError("确认上传失败,状态码: \((response as? HTTPURLResponse)?.statusCode ?? -1)")))
>>>>>>> a207b78 (feat: )
=======
>>>>>>> 5edee64 (feat: )
return
}
//
let uploadResult = UploadResult(
fileUrl: "\(self.apiConfig.baseURL)/files/\(fileId)",
fileName: fileName,
fileSize: fileSize,
fileId: fileId
)
print("✅ 图片上传并确认成功fileId: \(fileId)")
completion(.success(uploadResult))
}
task.resume()
}
<<<<<<< HEAD
<<<<<<< HEAD
=======
>>>>>>> 8e641fd (feat: )
/// URL
/// - Parameters:
/// - fileData:
/// - uploadURL: URL
/// - mimeType: MIME
/// - onProgress: 0.0 1.0
/// - completion:
public func uploadFile(
fileData: Data,
to uploadURL: URL,
mimeType: String = "application/octet-stream",
onProgress: @escaping (Double) -> Void,
completion: @escaping (Result<Void, Error>) -> Void
) -> URLSessionUploadTask {
var request = URLRequest(url: uploadURL)
request.httpMethod = "PUT"
request.setValue(mimeType, forHTTPHeaderField: "Content-Type")
let task = session.uploadTask(
with: request,
from: fileData
) { _, response, error in
if let error = error {
completion(.failure(error))
return
}
<<<<<<< HEAD
<<<<<<< HEAD
=======
>>>>>>> 5edee64 (feat: )
guard let httpResponse = response as? HTTPURLResponse else {
completion(.failure(UploadError.invalidResponse))
return
}
guard httpResponse.statusCode >= 200 && httpResponse.statusCode < 300 else {
let statusCode = httpResponse.statusCode
<<<<<<< HEAD
=======
guard let httpResponse = response as? HTTPURLResponse,
(200...299).contains(httpResponse.statusCode) else {
let statusCode = (response as? HTTPURLResponse)?.statusCode ?? -1
>>>>>>> 8e641fd (feat: )
=======
>>>>>>> 5edee64 (feat: )
completion(.failure(UploadError.serverError("上传失败,状态码: \(statusCode)")))
return
}
completion(.success(()))
}
//
if #available(iOS 11.0, *) {
let progressObserver = task.progress.observe(\.fractionCompleted) { (progressValue, _) in
DispatchQueue.main.async {
onProgress(progressValue.fractionCompleted)
}
}
task.addCompletionHandler { [weak task] in
progressObserver.invalidate()
task?.progress.cancel()
}
} else {
<<<<<<< HEAD
<<<<<<< HEAD
// iOS 11 使
=======
// Fallback for earlier iOS versions
>>>>>>> 8e641fd (feat: )
=======
// iOS 11 使
>>>>>>> 5edee64 (feat: )
var lastProgress: Double = 0
let timer = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { timer in
let bytesSent = task.countOfBytesSent
let totalBytes = task.countOfBytesExpectedToSend
let currentProgress = totalBytes > 0 ? Double(bytesSent) / Double(totalBytes) : 0
// UI
if abs(currentProgress - lastProgress) > 0.01 || currentProgress >= 1.0 {
lastProgress = currentProgress
DispatchQueue.main.async {
onProgress(min(currentProgress, 1.0))
}
}
if currentProgress >= 1.0 {
timer.invalidate()
}
}
task.addCompletionHandler {
timer.invalidate()
}
}
task.resume()
return task
}
// 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()
}
}
}
<<<<<<< HEAD
=======
>>>>>>> a207b78 (feat: )
=======
>>>>>>> 8e641fd (feat: )
}
// 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"
}
}
}