KMResourceDownloadManager.swift 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468
  1. //
  2. // KMResourceDownloadManager.swift
  3. // PDF Reader Pro
  4. //
  5. // Created by lizhe on 2023/8/29.
  6. //
  7. import Cocoa
  8. import Foundation
  9. import ZipArchive // 请确保已导入 SSZipArchive 或其他合适的解压库
  10. import ComDocumentAIKit
  11. import ComPDFKit_Conversion
  12. //#if DEBUG
  13. //let xmlURLString = "http://test-pdf-pro.kdan.cn:3021/downloads/DocumentAI.xml"
  14. //#else
  15. //let xmlURLString = "https://www.pdfreaderpro.com/downloads/DocumentAI.xml"
  16. //#endif
  17. //let documentAIString = "http://test-pdf-pro.kdan.cn:3021/downloads/DocumentAI.bundle.zip"
  18. let xmlURLString = KMServerConfig().kResourceServerURL
  19. let kResourcePath: String = kAppSupportOfBundleIdentifierDirectory.path
  20. let kLocalFilePath: String = kAppSupportOfBundleIdentifierDirectory.appendingPathComponent("DocumentAI.bundle").path
  21. enum KMResourceDownloadState {
  22. case none
  23. case unzipFailed
  24. case moveFailed
  25. case success
  26. case retry
  27. case cancel
  28. }
  29. struct Version: Comparable {
  30. let components: [Int]
  31. init?(_ version: String) {
  32. components = version.split(separator: ".").compactMap { Int($0) }
  33. guard !components.isEmpty else { return nil }
  34. }
  35. static func < (lhs: Version, rhs: Version) -> Bool {
  36. for (l, r) in zip(lhs.components, rhs.components) {
  37. if l != r { return l < r }
  38. }
  39. return lhs.components.count < rhs.components.count
  40. }
  41. }
  42. class KMResourceDownloadManager: NSObject {
  43. static let manager = KMResourceDownloadManager()
  44. var downloadTask: URLSessionDownloadTask?
  45. var progressBlock: ((Double) -> Void)?
  46. var downloadResultBlock: ((Bool, KMResourceDownloadState) -> Void)?
  47. var reachabilityAlert: NSAlert?
  48. func downloadFramework(progress: @escaping (Double) -> Void, result: @escaping (Bool, KMResourceDownloadState) -> Void) {
  49. self.progressBlock = progress
  50. self.downloadResultBlock = result
  51. KMRequestServer.requestServer.reachabilityStatusChange { [weak self] status in
  52. if status == .notReachable {
  53. KMPrint("无网络")
  54. self?.downloadTask?.cancel()
  55. self?.downloadTask = nil
  56. self?.downloadResultBlock?(false, .none)
  57. DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.2, execute: { [weak self] in
  58. if self?.reachabilityAlert == nil {
  59. self?.reachabilityAlert = NSAlert()
  60. self?.reachabilityAlert?.messageText = NSLocalizedString("Network Disconnected", comment: "")
  61. self?.reachabilityAlert?.informativeText = NSLocalizedString("Please connect to the internet and download again.", comment: "")
  62. self?.reachabilityAlert?.addButton(withTitle: NSLocalizedString("Retry", comment: ""))
  63. self?.reachabilityAlert?.addButton(withTitle: NSLocalizedString("Cancel", comment: ""))
  64. var window = NSWindow.currentWindow()
  65. if window != nil {
  66. self?.reachabilityAlert?.beginSheetModal(for: window) { result in
  67. if (result == .alertSecondButtonReturn) {
  68. self?.reachabilityAlert = nil
  69. self?.cancelDownload()
  70. } else if result == .alertFirstButtonReturn {
  71. self?.reachabilityAlert = nil
  72. self?.downloadResultBlock?(false, .retry)
  73. self?.cancelDownload()
  74. return
  75. }
  76. self?.reachabilityAlert = nil
  77. }
  78. } else {
  79. self?.reachabilityAlert = nil
  80. }
  81. } else {
  82. self?.reachabilityAlert = nil
  83. }
  84. })
  85. } else {
  86. KMPrint("有网络")
  87. }
  88. }
  89. if self.downloadTask == nil {
  90. self.downloadXML { [unowned self] content in
  91. let urlString = self.dealXML(content: content)
  92. // let urlString = "http://test-pdf-pro.kdan.cn:3021/downloads/DocumentAI.bundle.zip"
  93. if let url = URL(string: urlString) {
  94. let configuration = URLSessionConfiguration.default
  95. let session = URLSession(configuration: configuration, delegate: self, delegateQueue: nil)
  96. let downloadTask = session.downloadTask(with: url)
  97. downloadTask.resume()
  98. self.downloadTask = downloadTask
  99. } else {
  100. dealDownloadResult(isSuccess: false, state: .none)
  101. }
  102. }
  103. } else {
  104. dealDownloadResult(isSuccess: false, state: .none)
  105. }
  106. }
  107. func documentAIBundleExists(complete:@escaping (_ result: Bool) -> Void) {
  108. let filePath: String = kLocalFilePath
  109. let fileManager = FileManager.default
  110. debugPrint(filePath)
  111. debugPrint(FileManager.default.temporaryDirectory.appendingPathComponent("XMLResources"))
  112. let exists = fileManager.fileExists(atPath: filePath as String)
  113. //如果存在则判断版本是否符合,如果不符合则删除后重新下载
  114. if exists {
  115. self.checkDocumentAIVersion(complete: complete)
  116. self.loadDocumentAIBundle(bundlePath: kLocalFilePath)
  117. } else {
  118. self.removeDownloadResourcePath()
  119. complete(false)
  120. }
  121. }
  122. func cancelDownload() {
  123. downloadTask?.cancel()
  124. downloadTask = nil
  125. progressBlock = nil
  126. downloadResultBlock = nil
  127. self.downloadResultBlock?(false, .cancel)
  128. }
  129. //结果处理
  130. func dealDownloadResult(isSuccess: Bool, state: KMResourceDownloadState) {
  131. DispatchQueue.main.async {
  132. self.downloadResultBlock?(isSuccess, state)
  133. self.cancelDownload()
  134. }
  135. }
  136. func checkDocumentAIVersion(complete: @escaping (_ results: Bool) -> Void) {
  137. self.downloadXML { [unowned self] content in
  138. let urlString = self.dealXML(content: content)
  139. if urlString.count != 0 {
  140. try?FileManager.default.removeItem(atPath: kLocalFilePath)
  141. DispatchQueue.main.async {
  142. complete(false)
  143. }
  144. } else {
  145. DispatchQueue.main.async {
  146. complete(true)
  147. }
  148. }
  149. }
  150. }
  151. func loadDocumentAIBundle(bundlePath: String) {
  152. guard FileManager.default.fileExists(atPath: bundlePath) else {
  153. print("Bundle does not exist at specified path.")
  154. return
  155. }
  156. if let bundle = Bundle(path: bundlePath) {
  157. if let resourcePath = bundle.path(forResource: "DocumentAI", ofType: "model") {
  158. print("Found resource at path: \(resourcePath)")
  159. //绑定资源包
  160. CDocumentAIKit.sharedInstance().setOCRModelPath(bundlePath)
  161. CPDFConvertKit.setOCRModelPath(bundlePath)
  162. } else {
  163. print("Resource not found.")
  164. }
  165. } else {
  166. print("Failed to load bundle.")
  167. }
  168. }
  169. }
  170. //MARK: UI
  171. extension KMResourceDownloadManager {
  172. func downLoadOCRResource(window: NSWindow) {
  173. #if VERSION_DMG
  174. DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.3) {
  175. KMResourceDownloadManager.manager.documentAIBundleExists(complete: { [weak self] result in
  176. if result {
  177. } else {
  178. let alert = NSAlert()
  179. alert.alertStyle = .critical
  180. alert.messageText = KMLocalizedString("Need DownLoad OCR Resource", comment: "")
  181. alert.addButton(withTitle: KMLocalizedString("OK", comment: ""))
  182. alert.addButton(withTitle: KMLocalizedString("Cancel", comment: ""))
  183. alert.beginSheetModal(for: NSWindow.currentWindow()) {[weak self] response in
  184. if response == NSApplication.ModalResponse.alertFirstButtonReturn {
  185. self?.downLoad(window: window)
  186. }
  187. }
  188. }
  189. })
  190. }
  191. #endif
  192. }
  193. #if VERSION_DMG
  194. func downLoad(window: NSWindow) {
  195. let controller = KMOCRDownloadViewController(nibName: "KMOCRDownloadViewController", bundle: nil)
  196. let tempWindow = NSWindow(contentViewController: controller)
  197. tempWindow.maxSize = CGSizeMake(480, 208)
  198. tempWindow.minSize = CGSizeMake(480, 208)
  199. tempWindow.styleMask.remove(.resizable)
  200. window.beginSheet(tempWindow)
  201. controller.closeAction = { [unowned self] controller2 in
  202. window.endSheet(tempWindow)
  203. }
  204. controller.completionAction = { [unowned self] controller2 in
  205. window.endSheet(tempWindow)
  206. }
  207. controller.begin()
  208. }
  209. #endif
  210. }
  211. extension KMResourceDownloadManager: XMLParserDelegate {
  212. func downloadXML(completion: @escaping (_ content: String) -> Void) {
  213. if let xmlURL = URL(string: xmlURLString) {
  214. let request = URLRequest(url: xmlURL)
  215. let session = URLSession.shared
  216. let task = session.dataTask(with: request) { (data, response, error) in
  217. if let error = error {
  218. print("Error: \(error)")
  219. } else if let data = data {
  220. if let xmlString = String(data: data, encoding: .utf8) {
  221. print("XML Data: \(xmlString)")
  222. completion(xmlString)
  223. }
  224. }
  225. }
  226. task.resume()
  227. } else {
  228. print("Invalid URL")
  229. completion("")
  230. }
  231. }
  232. func dealXML(content: String) -> String {
  233. //
  234. // 1. 定义 XML 内容,包括版本号
  235. // let xmlContent = """
  236. // <root>
  237. // <resources>
  238. // <resource>
  239. // <maxVersion>1.3.0</maxVersion>
  240. // <minVersion>1.1.0</minVersion>
  241. // <resourceURL>https://www.pdfreaderpro.com/downloads/DocumentAI.bundle.1.0.0.zip</resourceURL>
  242. // </resource>
  243. // <resource>
  244. // <maxVersion>1.4.0</maxVersion>
  245. // <minVersion>1.3.0</minVersion>
  246. // <resourceURL>https://www.pdfreaderpro.com/downloads/DocumentAI.bundle.1.2.0.zip</resourceURL>
  247. // </resource>
  248. // <resource>
  249. // <maxVersion>1.5.1</maxVersion>
  250. // <minVersion>1.4.1</minVersion>
  251. // <resourceURL>https://www.pdfreaderpro.com/downloads/DocumentAI.bundle.2.0.0.zip</resourceURL>
  252. // </resource>
  253. // </resources>
  254. // </root>
  255. // """
  256. let xmlContent = content
  257. let parser = ResourceParser()
  258. let resources = parser.parse(data: xmlContent.data(using: .utf8)!)
  259. // 从捆绑包的 Info.plist 文件中获取版本号
  260. var appVersion = "1.0.0"
  261. if let version = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String {
  262. appVersion = version
  263. print("应用程序版本号:\(appVersion)")
  264. } else {
  265. print("无法获取应用程序版本号。")
  266. }
  267. if let resourceURL = shouldDownloadResource(for: appVersion, resources: resources) {
  268. if let local = self.getDownloadedResourcePath(for: kLocalFilePath) {
  269. if local != resourceURL {
  270. //如果本地存在两个地址不相同 则下载
  271. print("No suitable resource found for the current version.")
  272. return resourceURL
  273. } else {
  274. //如果本地存在 两个地址相同 则不下载
  275. print("No suitable resource found for the current version.")
  276. return ""
  277. }
  278. } else {
  279. //如果本地不存在 则下载
  280. print("Download resource from: \(resourceURL)")
  281. return resourceURL
  282. }
  283. } else {
  284. //如果找不到下载链接 则不下载
  285. print("No suitable resource found for the current version.")
  286. return ""
  287. }
  288. }
  289. func shouldDownloadResource(for currentVersion: String, resources: [Resource]) -> String? {
  290. for resource in resources {
  291. if isVersion(currentVersion, between: resource.minVersion, and: resource.maxVersion) {
  292. return resource.resourceURL
  293. }
  294. }
  295. return nil
  296. }
  297. func isVersion(_ version: String, between minVersion: String, and maxVersion: String) -> Bool {
  298. guard let current = Version(version),
  299. let min = Version(minVersion),
  300. let max = Version(maxVersion) else {
  301. return false
  302. }
  303. return current >= min && current <= max
  304. }
  305. func saveDownloadedResource(resourceURL: String, localPath: String) {
  306. var resources = UserDefaults.standard.dictionary(forKey: "downloadedResources") as? [String: String] ?? [:]
  307. resources[localPath] = resourceURL
  308. UserDefaults.standard.set(resources, forKey: "downloadedResources")
  309. }
  310. func getDownloadedResourcePath(for resourceURL: String) -> String? {
  311. let resources = UserDefaults.standard.dictionary(forKey: "downloadedResources") as? [String: String]
  312. return resources?[resourceURL]
  313. }
  314. func removeDownloadResourcePath() {
  315. UserDefaults.standard.removeObject(forKey: "downloadedResources")
  316. }
  317. func validateResource(for resourceURL: String) -> Bool {
  318. if let localPath = getDownloadedResourcePath(for: resourceURL) {
  319. return FileManager.default.fileExists(atPath: localPath)
  320. }
  321. return false
  322. }
  323. }
  324. extension KMResourceDownloadManager {
  325. //MARK: 解压
  326. func unzipFramework(at zipURL: URL, to destinationPath: String) {
  327. let fileManager = FileManager.default
  328. var success = false
  329. if zipURL.pathExtension == "zip" {
  330. success = SSZipArchive.unzipFile(atPath: zipURL.path, toDestination: destinationPath)
  331. } else {
  332. // 如果是其他类型的压缩文件,可以使用其他解压库
  333. // success = YourCustomUnzipLibrary.unzipFile(atPath: zipURL.path, toDestination: destinationPath)
  334. }
  335. if success {
  336. print("File unzipped successfully!")
  337. try? fileManager.removeItem(at: zipURL)
  338. } else {
  339. print("Failed to unzip file.")
  340. dealDownloadResult(isSuccess: false, state: .unzipFailed)
  341. }
  342. }
  343. }
  344. extension KMResourceDownloadManager: URLSessionDelegate, URLSessionDownloadDelegate {
  345. //MARK: 网络下载
  346. func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
  347. let progress = Double(totalBytesWritten) / Double(totalBytesExpectedToWrite)
  348. print("Download progress: \(progress)")
  349. progressBlock?(progress)
  350. }
  351. func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
  352. let destinationPath = kResourcePath
  353. let fileManager = FileManager.default
  354. let destinationURL = URL(fileURLWithPath: destinationPath).appendingPathComponent("DocumentAI.bundle.zip")
  355. do {
  356. try fileManager.moveItem(at: location, to: destinationURL)
  357. print("Framework downloaded and installed successfully!")
  358. unzipFramework(at: destinationURL, to: destinationPath)
  359. dealDownloadResult(isSuccess: true, state: .success)
  360. if let sourceURL = downloadTask.originalRequest?.url {
  361. saveDownloadedResource(resourceURL: sourceURL.absoluteString, localPath: kLocalFilePath)
  362. }
  363. } catch {
  364. print("Failed to move framework: \(error)")
  365. dealDownloadResult(isSuccess: false, state: .moveFailed)
  366. }
  367. }
  368. }
  369. struct Resource {
  370. let maxVersion: String
  371. let minVersion: String
  372. let resourceURL: String
  373. }
  374. // 5. 定义一个 XML 解析器的代理类
  375. class ResourceParser: NSObject, XMLParserDelegate {
  376. private var resources: [Resource] = []
  377. private var currentElement = ""
  378. var currentMaxVersion = ""
  379. var currentMinVersion = ""
  380. var currentResourceURL = ""
  381. func parse(data: Data) -> [Resource] {
  382. let parser = XMLParser(data: data)
  383. parser.delegate = self
  384. parser.parse()
  385. return resources
  386. }
  387. func parser(_ parser: XMLParser, didStartElement elementName: String, namespaceURI: String?, qualifiedName qName: String?, attributes attributeDict: [String : String] = [:]) {
  388. currentElement = elementName
  389. }
  390. func parser(_ parser: XMLParser, foundCharacters string: String) {
  391. switch currentElement {
  392. case "maxVersion":
  393. currentMaxVersion += string
  394. case "minVersion":
  395. currentMinVersion += string
  396. case "resourceURL":
  397. currentResourceURL += string
  398. default:
  399. break
  400. }
  401. }
  402. func parser(_ parser: XMLParser, didEndElement elementName: String, namespaceURI: String?, qualifiedName qName: String?) {
  403. if elementName == "resource" {
  404. resources.append(
  405. Resource(
  406. maxVersion: currentMaxVersion.trimmingCharacters(in: .whitespacesAndNewlines),
  407. minVersion: currentMinVersion.trimmingCharacters(in: .whitespacesAndNewlines),
  408. resourceURL: currentResourceURL.trimmingCharacters(in: .whitespacesAndNewlines)
  409. )
  410. )
  411. currentMaxVersion = ""
  412. currentMinVersion = ""
  413. currentResourceURL = ""
  414. }
  415. }
  416. }