LNFileUploader.swift 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197
  1. //
  2. // LNFileUploader.swift
  3. // Lanu
  4. //
  5. // Created by OneeChan on 2025/12/2.
  6. //
  7. import Foundation
  8. private class LNFileUploadTask {
  9. let id: String
  10. let fileUrl: String
  11. let task: URLSessionUploadTask
  12. let progress: ((Float) -> Void)?
  13. let completion: ((String?, String?) -> Void)?
  14. init(id: String,
  15. fileUrl: String,
  16. task: URLSessionUploadTask,
  17. progress: ((Float) -> Void)? = nil,
  18. completion: ((String?, String?) -> Void)? = nil) {
  19. self.id = id
  20. self.fileUrl = fileUrl
  21. self.task = task
  22. self.progress = progress
  23. self.completion = completion
  24. }
  25. }
  26. class LNFileUploader: NSObject {
  27. static let shared = LNFileUploader()
  28. private lazy var session: URLSession = {
  29. let config = URLSessionConfiguration.default
  30. return URLSession(configuration: config, delegate: self, delegateQueue: OperationQueue())
  31. }()
  32. private var uploadTasks: [String: LNFileUploadTask] = [:]
  33. private let lock = NSLock()
  34. @discardableResult
  35. func startUpload(
  36. type: LNUploadFileType,
  37. fileURL: URL,
  38. headers: [String: String] = [:],
  39. progressHandler: ((Float) -> Void)?,
  40. completionHandler: ((String?, String?) -> Void)?
  41. ) -> String? {
  42. lock.lock()
  43. defer { lock.unlock() }
  44. let taskId = "\(type.rawValue)-\(fileURL.absoluteString.md5)"
  45. guard uploadTasks[taskId] == nil else {
  46. completionHandler?(nil, .init(key: "B00017"))
  47. return nil
  48. }
  49. guard FileManager.default.fileExists(atPath: fileURL.path) else {
  50. completionHandler?(nil, .init(key: "B00014"))
  51. return nil
  52. }
  53. LNHttpManager.shared.getUploadOssUrl(type: type, suffix: fileURL.pathExtension) { [weak self] res, err in
  54. guard let self else { return }
  55. guard err == nil, let res, let url = URL(string: res.preSignUrl) else {
  56. completionHandler?(nil, err?.errorDesc ?? LNHttpError.invalidResponse.errorDesc)
  57. return
  58. }
  59. var request = URLRequest(url: url)
  60. request.httpMethod = "PUT"
  61. let defaultHeaders: [String: String] = ["Content-Type": "application/octet-stream"]
  62. let allHeaders = defaultHeaders.merging(headers) { $1 }
  63. allHeaders.forEach { key, value in
  64. request.setValue(value, forHTTPHeaderField: key)
  65. }
  66. let task = session.uploadTask(with: request, fromFile: fileURL)
  67. let uploadTask = LNFileUploadTask(
  68. id: taskId, fileUrl: res.fileUrl,
  69. task: task, progress: progressHandler,
  70. completion: completionHandler)
  71. uploadTasks[taskId] = uploadTask
  72. task.resume()
  73. }
  74. return taskId
  75. }
  76. @discardableResult
  77. func startUpload(
  78. type: LNUploadFileType,
  79. fileData: Data,
  80. suffix: String,
  81. headers: [String: String] = [:],
  82. progressHandler: ((Float) -> Void)?,
  83. completionHandler: ((String?, String?) -> Void)?
  84. ) -> String? {
  85. lock.lock()
  86. defer { lock.unlock() }
  87. let taskId = "\(type.rawValue)-\(fileData.md5)"
  88. guard uploadTasks[taskId] == nil else {
  89. completionHandler?(nil, .init(key: "B00017"))
  90. return nil
  91. }
  92. LNHttpManager.shared.getUploadOssUrl(type: type, suffix: suffix) { [weak self] res, err in
  93. guard let self else { return }
  94. guard err == nil, let res, let url = URL(string: res.preSignUrl) else {
  95. runOnMain {
  96. completionHandler?(nil, err?.errorDesc ?? LNHttpError.invalidResponse.errorDesc)
  97. }
  98. return
  99. }
  100. var request = URLRequest(url: url)
  101. request.httpMethod = "PUT"
  102. let defaultHeaders: [String: String] = ["Content-Type": "application/octet-stream"]
  103. let allHeaders = defaultHeaders.merging(headers) { $1 }
  104. allHeaders.forEach { key, value in
  105. request.setValue(value, forHTTPHeaderField: key)
  106. }
  107. let task = session.uploadTask(with: request, from: fileData)
  108. let uploadTask = LNFileUploadTask(
  109. id: taskId, fileUrl: res.fileUrl,
  110. task: task, progress: progressHandler,
  111. completion: completionHandler)
  112. uploadTasks[taskId] = uploadTask
  113. task.resume()
  114. }
  115. return taskId
  116. }
  117. func cancelUpload(taskID: String) {
  118. lock.lock()
  119. defer { lock.unlock() }
  120. // 清理任务和回调
  121. let uploadTask = uploadTasks.removeValue(forKey: taskID)
  122. uploadTask?.task.cancel()
  123. }
  124. /// 取消所有上传任务
  125. func cancelAllUploads() {
  126. lock.lock()
  127. defer { lock.unlock() }
  128. uploadTasks.values.forEach { $0.task.cancel() }
  129. uploadTasks.removeAll()
  130. }
  131. }
  132. extension LNFileUploader: URLSessionTaskDelegate, URLSessionDataDelegate {
  133. func urlSession(_ session: URLSession, task: URLSessionTask, didSendBodyData bytesSent: Int64, totalBytesSent: Int64, totalBytesExpectedToSend: Int64) {
  134. lock.lock()
  135. defer { lock.unlock() }
  136. guard let progressHandler = uploadTasks.first(where: { $0.value.task == task })?.value.progress else { return }
  137. guard totalBytesExpectedToSend > 0 else { return }
  138. let progress = Float(totalBytesSent) / Float(totalBytesExpectedToSend)
  139. runOnMain {
  140. progressHandler(progress)
  141. }
  142. }
  143. func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
  144. lock.lock()
  145. defer { lock.unlock() }
  146. guard let uploadTask = uploadTasks.first(where: { $0.value.task == task })?.value else { return }
  147. uploadTasks.removeValue(forKey: uploadTask.id)
  148. guard let completionHandler = uploadTask.completion else { return }
  149. runOnMain {
  150. if let error = error {
  151. if (error as NSError).code == NSURLErrorCancelled {
  152. completionHandler(nil, .init(key: "B00015"))
  153. } else {
  154. completionHandler(nil, error.localizedDescription)
  155. }
  156. } else {
  157. if let httpResponse = task.response as? HTTPURLResponse, (200...201).contains(httpResponse.statusCode) {
  158. completionHandler(uploadTask.fileUrl, nil)
  159. } else {
  160. let statusCode = (task.response as? HTTPURLResponse)?.statusCode ?? -4
  161. completionHandler(nil, .init(key: "B00016"))
  162. }
  163. }
  164. }
  165. }
  166. }