LNFileDownloader.swift 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256
  1. //
  2. // LNFileDownloader.swift
  3. // Lanu
  4. //
  5. // Created by OneeChan on 2025/12/5.
  6. //
  7. import Foundation
  8. private class LNFileDownloadTask {
  9. let sourceUrl: String
  10. let destinationPath: URL
  11. var isRunning = false
  12. var progressHandler: [((Float) -> Void)] = []
  13. var completionHandler: [((Result<URL, Error>) -> Void)] = []
  14. var totalBytesWritten: Int64 = 0
  15. var totalBytesExpectedToWrite: Int64 = 0
  16. init(url: String, destinationPath: URL) {
  17. self.sourceUrl = url
  18. self.destinationPath = destinationPath
  19. }
  20. }
  21. class LNFileDownloader: NSObject {
  22. static let shared = LNFileDownloader()
  23. private let maxConcurrentCount: Int = 3
  24. private lazy var session: URLSession = {
  25. let config = URLSessionConfiguration.default
  26. config.httpMaximumConnectionsPerHost = maxConcurrentCount
  27. return URLSession(configuration: config, delegate: self, delegateQueue: OperationQueue())
  28. }()
  29. private let lock = NSLock()
  30. private var taskList: [LNFileDownloadTask] = []
  31. private var runningTaskCount: Int = 0
  32. enum LNFileDownloadError: Error {
  33. case invalidURL
  34. case fileMoveFailed
  35. case beCancelled
  36. case networkError(Error)
  37. var errorDesc: String? {
  38. switch self {
  39. case .invalidURL: return "无效的下载链接"
  40. case .fileMoveFailed: return "文件保存失败"
  41. case .beCancelled: return "被取消"
  42. case .networkError: return "网络错误"
  43. }
  44. }
  45. }
  46. func startDownload(
  47. from urlString: String,
  48. destinationPath: URL? = nil,
  49. progressHandler: ((Float) -> Void)? = nil,
  50. completionHandler: ((Result<URL, Error>) -> Void)? = nil
  51. ) {
  52. guard let url = URL(string: urlString) else {
  53. completionHandler?(.failure(LNFileDownloadError.invalidURL))
  54. return
  55. }
  56. DispatchQueue.global().async { [weak self] in
  57. guard let self else { return }
  58. lock.lock()
  59. defer { lock.unlock() }
  60. let task = taskList.first { $0.sourceUrl == urlString }
  61. if let task {
  62. if let progressHandler {
  63. task.progressHandler.append(progressHandler)
  64. }
  65. if let completionHandler {
  66. task.completionHandler.append(completionHandler)
  67. }
  68. return
  69. }
  70. let targetPath = destinationPath ?? defaultDestinationPath(for: url)
  71. let newTask = LNFileDownloadTask(url: urlString, destinationPath: targetPath)
  72. if let progressHandler {
  73. newTask.progressHandler.append(progressHandler)
  74. }
  75. if let completionHandler {
  76. newTask.completionHandler.append(completionHandler)
  77. }
  78. taskList.append(newTask)
  79. if runningTaskCount >= maxConcurrentCount { return }
  80. let downloadTask = session.downloadTask(with: url)
  81. downloadTask.taskDescription = urlString
  82. runningTaskCount += 1
  83. newTask.isRunning = true
  84. downloadTask.resume()
  85. }
  86. }
  87. func cancelDownload(urlString: String) {
  88. DispatchQueue.global().async { [weak self] in
  89. guard let self else { return }
  90. lock.lock()
  91. defer { lock.unlock() }
  92. guard let task = taskList.first(where: { $0.sourceUrl == urlString }) else { return }
  93. session.getAllTasks(completionHandler: { tasks in
  94. tasks.first { $0.taskDescription == urlString }?.cancel()
  95. })
  96. removeTask(urlString: urlString)
  97. let handlers = task.completionHandler
  98. runOnMain {
  99. handlers.forEach { $0(.failure(LNFileDownloadError.beCancelled)) }
  100. }
  101. }
  102. }
  103. /// 取消所有下载任务
  104. func cancelAllDownloads() {
  105. DispatchQueue.global().async { [weak self] in
  106. guard let self else { return }
  107. lock.lock()
  108. defer { lock.unlock() }
  109. session.getAllTasks(completionHandler: { tasks in
  110. tasks.forEach { $0.cancel() }
  111. })
  112. taskList.removeAll()
  113. runningTaskCount = 0
  114. }
  115. }
  116. }
  117. extension LNFileDownloader {
  118. /// 生成默认保存路径(Documents目录+原文件名)
  119. private func defaultDestinationPath(for url: URL) -> URL {
  120. let fileName = url.lastPathComponent
  121. return URL.documentsDir.appendingPathComponent(fileName)
  122. }
  123. private func removeTask(urlString: String) {
  124. DispatchQueue.global().async { [weak self] in
  125. guard let self else { return }
  126. lock.lock()
  127. defer { lock.unlock() }
  128. taskList.removeAll(where: { $0.sourceUrl == urlString })
  129. runningTaskCount = max(0, self.runningTaskCount - 1)
  130. if let nextTask = taskList.first(where: { $0.isRunning == false }) {
  131. let downloadTask = session.downloadTask(with: URL(string: nextTask.sourceUrl)!)
  132. downloadTask.taskDescription = urlString
  133. runningTaskCount += 1
  134. nextTask.isRunning = true
  135. downloadTask.resume()
  136. }
  137. }
  138. }
  139. }
  140. extension LNFileDownloader: URLSessionDownloadDelegate {
  141. func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
  142. lock.lock()
  143. defer { lock.unlock() }
  144. guard let urlString = downloadTask.taskDescription,
  145. let taskModel = taskList.first(where: { $0.sourceUrl == urlString }) else {
  146. return
  147. }
  148. taskModel.totalBytesWritten = totalBytesWritten
  149. taskModel.totalBytesExpectedToWrite = totalBytesExpectedToWrite
  150. let progress = totalBytesExpectedToWrite > 0 ? Float(totalBytesWritten) / Float(totalBytesExpectedToWrite) : 0.0
  151. let handlers = taskModel.progressHandler
  152. runOnMain {
  153. handlers.forEach { $0(progress) }
  154. }
  155. }
  156. func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
  157. lock.lock()
  158. defer { lock.unlock() }
  159. guard FileManager.default.fileExists(atPath: location.path) else { return }
  160. guard let urlString = downloadTask.taskDescription,
  161. let taskModel = taskList.first(where: { $0.sourceUrl == urlString }) else {
  162. return
  163. }
  164. // 2. 移动临时文件到目标路径
  165. do {
  166. let destinationPath = taskModel.destinationPath
  167. // 检查目标文件是否存在,存在则删除(避免覆盖失败)
  168. if FileManager.default.fileExists(atPath: destinationPath.path) {
  169. try FileManager.default.removeItem(at: destinationPath)
  170. }
  171. // 移动文件
  172. let destinationDir = destinationPath.deletingLastPathComponent()
  173. if !FileManager.default.fileExists(atPath: destinationDir.path) {
  174. try FileManager.default.createDirectory(
  175. at: destinationDir,
  176. withIntermediateDirectories: true,
  177. attributes: nil
  178. )
  179. }
  180. try FileManager.default.moveItem(at: location, to: destinationPath)
  181. // 回调成功结果
  182. let handlers = taskModel.completionHandler
  183. runOnMain {
  184. handlers.forEach { $0(.success(destinationPath)) }
  185. }
  186. } catch {
  187. // 回调文件移动失败
  188. let handlers = taskModel.completionHandler
  189. runOnMain {
  190. handlers.forEach { $0(.failure(LNFileDownloadError.fileMoveFailed)) }
  191. }
  192. }
  193. removeTask(urlString: urlString)
  194. }
  195. }
  196. extension LNFileDownloader: URLSessionTaskDelegate {
  197. /// 任务完成(含错误处理)
  198. func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
  199. lock.lock()
  200. defer { lock.unlock() }
  201. guard let urlString = task.taskDescription,
  202. let taskModel = taskList.first(where: { $0.sourceUrl == urlString }) else {
  203. return
  204. }
  205. // 处理错误(排除用户取消的情况)
  206. if let error = error {
  207. let nsError = error as NSError
  208. // 取消下载在取消操作进行了回调,这里不处理
  209. if nsError.code == NSURLErrorCancelled {
  210. removeTask(urlString: urlString)
  211. return
  212. }
  213. // 其他错误:回调网络错误
  214. let handlers = taskModel.completionHandler
  215. runOnMain {
  216. handlers.forEach { $0(.failure(LNFileDownloadError.networkError(error))) }
  217. }
  218. }
  219. // 移除任务(无论成功与否,任务完成后都要移除)
  220. removeTask(urlString: urlString)
  221. }
  222. }