// // LNFileUploader.swift // Lanu // // Created by OneeChan on 2025/12/2. // import Foundation private class LNFileUploadTask { let id: String let fileUrl: String let task: URLSessionUploadTask let progress: ((Float) -> Void)? let completion: ((String?, String?) -> Void)? init(id: String, fileUrl: String, task: URLSessionUploadTask, progress: ((Float) -> Void)? = nil, completion: ((String?, String?) -> Void)? = nil) { self.id = id self.fileUrl = fileUrl self.task = task self.progress = progress self.completion = completion } } class LNFileUploader: NSObject { static let shared = LNFileUploader() private lazy var session: URLSession = { let config = URLSessionConfiguration.default return URLSession(configuration: config, delegate: self, delegateQueue: OperationQueue()) }() private var uploadTasks: [String: LNFileUploadTask] = [:] private let lock = NSLock() @discardableResult func startUpload( type: LNUploadFileType, fileURL: URL, headers: [String: String] = [:], progressHandler: ((Float) -> Void)?, completionHandler: ((String?, String?) -> Void)? ) -> String? { lock.lock() defer { lock.unlock() } let taskId = "\(type.rawValue)-\(fileURL.absoluteString.md5)" guard uploadTasks[taskId] == nil else { completionHandler?(nil, .init(key: "B00017")) return nil } guard FileManager.default.fileExists(atPath: fileURL.path) else { completionHandler?(nil, .init(key: "B00014")) return nil } LNHttpManager.shared.getUploadOssUrl(type: type, suffix: fileURL.pathExtension) { [weak self] res, err in guard let self else { return } guard err == nil, let res, let url = URL(string: res.preSignUrl) else { completionHandler?(nil, err?.errorDesc ?? LNHttpError.invalidResponse.errorDesc) return } var request = URLRequest(url: url) request.httpMethod = "PUT" let defaultHeaders: [String: String] = ["Content-Type": "application/octet-stream"] let allHeaders = defaultHeaders.merging(headers) { $1 } allHeaders.forEach { key, value in request.setValue(value, forHTTPHeaderField: key) } let task = session.uploadTask(with: request, fromFile: fileURL) let uploadTask = LNFileUploadTask( id: taskId, fileUrl: res.fileUrl, task: task, progress: progressHandler, completion: completionHandler) uploadTasks[taskId] = uploadTask task.resume() } return taskId } @discardableResult func startUpload( type: LNUploadFileType, fileData: Data, suffix: String, headers: [String: String] = [:], progressHandler: ((Float) -> Void)?, completionHandler: ((String?, String?) -> Void)? ) -> String? { lock.lock() defer { lock.unlock() } let taskId = "\(type.rawValue)-\(fileData.md5)" guard uploadTasks[taskId] == nil else { completionHandler?(nil, .init(key: "B00017")) return nil } LNHttpManager.shared.getUploadOssUrl(type: type, suffix: suffix) { [weak self] res, err in guard let self else { return } guard err == nil, let res, let url = URL(string: res.preSignUrl) else { runOnMain { completionHandler?(nil, err?.errorDesc ?? LNHttpError.invalidResponse.errorDesc) } return } var request = URLRequest(url: url) request.httpMethod = "PUT" let defaultHeaders: [String: String] = ["Content-Type": "application/octet-stream"] let allHeaders = defaultHeaders.merging(headers) { $1 } allHeaders.forEach { key, value in request.setValue(value, forHTTPHeaderField: key) } let task = session.uploadTask(with: request, from: fileData) let uploadTask = LNFileUploadTask( id: taskId, fileUrl: res.fileUrl, task: task, progress: progressHandler, completion: completionHandler) uploadTasks[taskId] = uploadTask task.resume() } return taskId } func cancelUpload(taskID: String) { lock.lock() defer { lock.unlock() } // 清理任务和回调 let uploadTask = uploadTasks.removeValue(forKey: taskID) uploadTask?.task.cancel() } /// 取消所有上传任务 func cancelAllUploads() { lock.lock() defer { lock.unlock() } uploadTasks.values.forEach { $0.task.cancel() } uploadTasks.removeAll() } } extension LNFileUploader: URLSessionTaskDelegate, URLSessionDataDelegate { func urlSession(_ session: URLSession, task: URLSessionTask, didSendBodyData bytesSent: Int64, totalBytesSent: Int64, totalBytesExpectedToSend: Int64) { lock.lock() defer { lock.unlock() } guard let progressHandler = uploadTasks.first(where: { $0.value.task == task })?.value.progress else { return } guard totalBytesExpectedToSend > 0 else { return } let progress = Float(totalBytesSent) / Float(totalBytesExpectedToSend) runOnMain { progressHandler(progress) } } func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) { lock.lock() defer { lock.unlock() } guard let uploadTask = uploadTasks.first(where: { $0.value.task == task })?.value else { return } uploadTasks.removeValue(forKey: uploadTask.id) guard let completionHandler = uploadTask.completion else { return } runOnMain { if let error = error { if (error as NSError).code == NSURLErrorCancelled { completionHandler(nil, .init(key: "B00015")) } else { completionHandler(nil, error.localizedDescription) } } else { if let httpResponse = task.response as? HTTPURLResponse, (200...201).contains(httpResponse.statusCode) { completionHandler(uploadTask.fileUrl, nil) } else { let statusCode = (task.response as? HTTPURLResponse)?.statusCode ?? -4 completionHandler(nil, .init(key: "B00016")) } } } } }