// // KMResourceDownloadManager.swift // PDF Reader Pro // // Created by lizhe on 2023/8/29. // import Cocoa import Foundation import ZipArchive // 请确保已导入 SSZipArchive 或其他合适的解压库 import ComDocumentAIKit import ComPDFKit_Conversion //#if DEBUG //let xmlURLString = "http://test-pdf-pro.kdan.cn:3021/downloads/DocumentAI.xml" //#else //let xmlURLString = "https://www.pdfreaderpro.com/downloads/DocumentAI.xml" //#endif //let documentAIString = "http://test-pdf-pro.kdan.cn:3021/downloads/DocumentAI.bundle.zip" let xmlURLString = KMServerConfig().kResourceServerURL let kResourcePath: String = kAppSupportOfBundleIdentifierDirectory.path let kLocalFilePath: String = kAppSupportOfBundleIdentifierDirectory.appendingPathComponent("DocumentAI.bundle").path enum KMResourceDownloadState { case none case unzipFailed case moveFailed case success case retry case cancel } struct Version: Comparable { let components: [Int] init?(_ version: String) { components = version.split(separator: ".").compactMap { Int($0) } guard !components.isEmpty else { return nil } } static func < (lhs: Version, rhs: Version) -> Bool { for (l, r) in zip(lhs.components, rhs.components) { if l != r { return l < r } } return lhs.components.count < rhs.components.count } } class KMResourceDownloadManager: NSObject { static let manager = KMResourceDownloadManager() var downloadTask: URLSessionDownloadTask? var progressBlock: ((Double) -> Void)? var downloadResultBlock: ((Bool, KMResourceDownloadState) -> Void)? var reachabilityAlert: NSAlert? func downloadFramework(progress: @escaping (Double) -> Void, result: @escaping (Bool, KMResourceDownloadState) -> Void) { self.progressBlock = progress self.downloadResultBlock = result KMRequestServer.requestServer.reachabilityStatusChange { [weak self] status in if status == .notReachable { KMPrint("无网络") self?.downloadTask?.cancel() self?.downloadTask = nil self?.downloadResultBlock?(false, .none) DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.2, execute: { [weak self] in if self?.reachabilityAlert == nil { self?.reachabilityAlert = NSAlert() self?.reachabilityAlert?.messageText = NSLocalizedString("Network Disconnected", comment: "") self?.reachabilityAlert?.informativeText = NSLocalizedString("Please connect to the internet and download again.", comment: "") self?.reachabilityAlert?.addButton(withTitle: NSLocalizedString("Retry", comment: "")) self?.reachabilityAlert?.addButton(withTitle: NSLocalizedString("Cancel", comment: "")) var window = NSWindow.currentWindow() if window != nil { self?.reachabilityAlert?.beginSheetModal(for: window) { result in if (result == .alertSecondButtonReturn) { self?.reachabilityAlert = nil self?.cancelDownload() } else if result == .alertFirstButtonReturn { self?.reachabilityAlert = nil self?.downloadResultBlock?(false, .retry) self?.cancelDownload() return } self?.reachabilityAlert = nil } } else { self?.reachabilityAlert = nil } } else { self?.reachabilityAlert = nil } }) } else { KMPrint("有网络") } } if self.downloadTask == nil { self.downloadXML { [unowned self] content in let urlString = self.dealXML(content: content) // let urlString = "http://test-pdf-pro.kdan.cn:3021/downloads/DocumentAI.bundle.zip" if let url = URL(string: urlString) { let configuration = URLSessionConfiguration.default let session = URLSession(configuration: configuration, delegate: self, delegateQueue: nil) let downloadTask = session.downloadTask(with: url) downloadTask.resume() self.downloadTask = downloadTask } else { dealDownloadResult(isSuccess: false, state: .none) } } } else { dealDownloadResult(isSuccess: false, state: .none) } } func documentAIBundleExists(complete:@escaping (_ result: Bool) -> Void) { let filePath: String = kLocalFilePath let fileManager = FileManager.default debugPrint(filePath) debugPrint(FileManager.default.temporaryDirectory.appendingPathComponent("XMLResources")) let exists = fileManager.fileExists(atPath: filePath as String) //如果存在则判断版本是否符合,如果不符合则删除后重新下载 if exists { self.checkDocumentAIVersion(complete: complete) self.loadDocumentAIBundle(bundlePath: kLocalFilePath) } else { self.removeDownloadResourcePath() complete(false) } } func cancelDownload() { downloadTask?.cancel() downloadTask = nil progressBlock = nil downloadResultBlock = nil self.downloadResultBlock?(false, .cancel) } //结果处理 func dealDownloadResult(isSuccess: Bool, state: KMResourceDownloadState) { DispatchQueue.main.async { self.downloadResultBlock?(isSuccess, state) self.cancelDownload() } } func checkDocumentAIVersion(complete: @escaping (_ results: Bool) -> Void) { self.downloadXML { [unowned self] content in let urlString = self.dealXML(content: content) if urlString.count != 0 { try?FileManager.default.removeItem(atPath: kLocalFilePath) complete(false) } else { complete(true) } } } func loadDocumentAIBundle(bundlePath: String) { guard FileManager.default.fileExists(atPath: bundlePath) else { print("Bundle does not exist at specified path.") return } if let bundle = Bundle(path: bundlePath) { if let resourcePath = bundle.path(forResource: "DocumentAI", ofType: "model") { print("Found resource at path: \(resourcePath)") //绑定资源包 CDocumentAIKit.sharedInstance().setOCRModelPath(bundlePath) CPDFConvertKit.setOCRModelPath(bundlePath) } else { print("Resource not found.") } } else { print("Failed to load bundle.") } } } //MARK: UI extension KMResourceDownloadManager { func downLoadOCRResource(window: NSWindow) { #if VERSION_DMG DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.3) { KMResourceDownloadManager.manager.documentAIBundleExists(complete: { [weak self] result in if result { } else { let alert = NSAlert() alert.alertStyle = .critical alert.messageText = KMLocalizedString("Need DownLoad OCR Resource", comment: "") alert.addButton(withTitle: KMLocalizedString("OK", comment: "")) alert.addButton(withTitle: KMLocalizedString("Cancel", comment: "")) alert.beginSheetModal(for: NSWindow.currentWindow()) {[weak self] response in if response == NSApplication.ModalResponse.alertFirstButtonReturn { self?.downLoad(window: window) } } } }) } #endif } #if VERSION_DMG func downLoad(window: NSWindow) { let controller = KMOCRDownloadViewController(nibName: "KMOCRDownloadViewController", bundle: nil) let tempWindow = NSWindow(contentViewController: controller) tempWindow.maxSize = CGSizeMake(480, 208) tempWindow.minSize = CGSizeMake(480, 208) tempWindow.styleMask.remove(.resizable) window.beginSheet(tempWindow) controller.closeAction = { [unowned self] controller2 in window.endSheet(tempWindow) } controller.completionAction = { [unowned self] controller2 in window.endSheet(tempWindow) } controller.begin() } #endif } extension KMResourceDownloadManager: XMLParserDelegate { func downloadXML(completion: @escaping (_ content: String) -> Void) { if let xmlURL = URL(string: xmlURLString) { let request = URLRequest(url: xmlURL) let session = URLSession.shared let task = session.dataTask(with: request) { (data, response, error) in if let error = error { print("Error: \(error)") } else if let data = data { if let xmlString = String(data: data, encoding: .utf8) { print("XML Data: \(xmlString)") completion(xmlString) } } } task.resume() } else { print("Invalid URL") completion("") } } func dealXML(content: String) -> String { // // 1. 定义 XML 内容,包括版本号 // let xmlContent = """ // // // // 1.3.0 // 1.1.0 // https://www.pdfreaderpro.com/downloads/DocumentAI.bundle.1.0.0.zip // // // 4.6.3 // 1.4.0 // https://www.pdfreaderpro.com/downloads/DocumentAI.bundle.2.0.0.zip // // // // """ let xmlContent = content let parser = ResourceParser() let resources = parser.parse(data: xmlContent.data(using: .utf8)!) // 从捆绑包的 Info.plist 文件中获取版本号 var appVersion = "1.0.0" if let version = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String { appVersion = version print("应用程序版本号:\(appVersion)") } else { print("无法获取应用程序版本号。") } if let resourceURL = shouldDownloadResource(for: appVersion, resources: resources) { if let local = self.getDownloadedResourcePath(for: kLocalFilePath) { if local != resourceURL { //如果本地存在两个地址不相同 则下载 print("No suitable resource found for the current version.") return resourceURL } else { //如果本地存在 两个地址相同 则不下载 print("No suitable resource found for the current version.") return "" } } else { //如果本地不存在 则下载 print("Download resource from: \(resourceURL)") return resourceURL } } else { //如果找不到下载链接 则不下载 print("No suitable resource found for the current version.") return "" } } func shouldDownloadResource(for currentVersion: String, resources: [Resource]) -> String? { for resource in resources { if isVersion(currentVersion, between: resource.minVersion, and: resource.maxVersion) { return resource.resourceURL } } return nil } func isVersion(_ version: String, between minVersion: String, and maxVersion: String) -> Bool { guard let current = Version(version), let min = Version(minVersion), let max = Version(maxVersion) else { return false } return current >= min && current <= max } func saveDownloadedResource(resourceURL: String, localPath: String) { var resources = UserDefaults.standard.dictionary(forKey: "downloadedResources") as? [String: String] ?? [:] resources[localPath] = resourceURL UserDefaults.standard.set(resources, forKey: "downloadedResources") } func getDownloadedResourcePath(for resourceURL: String) -> String? { let resources = UserDefaults.standard.dictionary(forKey: "downloadedResources") as? [String: String] return resources?[resourceURL] } func removeDownloadResourcePath() { UserDefaults.standard.removeObject(forKey: "downloadedResources") } func validateResource(for resourceURL: String) -> Bool { if let localPath = getDownloadedResourcePath(for: resourceURL) { return FileManager.default.fileExists(atPath: localPath) } return false } } extension KMResourceDownloadManager { //MARK: 解压 func unzipFramework(at zipURL: URL, to destinationPath: String) { let fileManager = FileManager.default var success = false if zipURL.pathExtension == "zip" { success = SSZipArchive.unzipFile(atPath: zipURL.path, toDestination: destinationPath) } else { // 如果是其他类型的压缩文件,可以使用其他解压库 // success = YourCustomUnzipLibrary.unzipFile(atPath: zipURL.path, toDestination: destinationPath) } if success { print("File unzipped successfully!") try? fileManager.removeItem(at: zipURL) } else { print("Failed to unzip file.") dealDownloadResult(isSuccess: false, state: .unzipFailed) } } } extension KMResourceDownloadManager: URLSessionDelegate, URLSessionDownloadDelegate { //MARK: 网络下载 func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) { let progress = Double(totalBytesWritten) / Double(totalBytesExpectedToWrite) print("Download progress: \(progress)") progressBlock?(progress) } func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) { let destinationPath = kResourcePath let fileManager = FileManager.default let destinationURL = URL(fileURLWithPath: destinationPath).appendingPathComponent("DocumentAI.bundle.zip") do { try fileManager.moveItem(at: location, to: destinationURL) print("Framework downloaded and installed successfully!") unzipFramework(at: destinationURL, to: destinationPath) dealDownloadResult(isSuccess: true, state: .success) if let sourceURL = downloadTask.originalRequest?.url { saveDownloadedResource(resourceURL: sourceURL.absoluteString, localPath: kLocalFilePath) } } catch { print("Failed to move framework: \(error)") dealDownloadResult(isSuccess: false, state: .moveFailed) } } } struct Resource { let maxVersion: String let minVersion: String let resourceURL: String } // 5. 定义一个 XML 解析器的代理类 class ResourceParser: NSObject, XMLParserDelegate { private var resources: [Resource] = [] private var currentElement = "" var currentMaxVersion = "" var currentMinVersion = "" var currentResourceURL = "" func parse(data: Data) -> [Resource] { let parser = XMLParser(data: data) parser.delegate = self parser.parse() return resources } func parser(_ parser: XMLParser, didStartElement elementName: String, namespaceURI: String?, qualifiedName qName: String?, attributes attributeDict: [String : String] = [:]) { currentElement = elementName } func parser(_ parser: XMLParser, foundCharacters string: String) { switch currentElement { case "maxVersion": currentMaxVersion += string case "minVersion": currentMinVersion += string case "resourceURL": currentResourceURL += string default: break } } func parser(_ parser: XMLParser, didEndElement elementName: String, namespaceURI: String?, qualifiedName qName: String?) { if elementName == "resource" { resources.append( Resource( maxVersion: currentMaxVersion.trimmingCharacters(in: .whitespacesAndNewlines), minVersion: currentMinVersion.trimmingCharacters(in: .whitespacesAndNewlines), resourceURL: currentResourceURL.trimmingCharacters(in: .whitespacesAndNewlines) ) ) currentMaxVersion = "" currentMinVersion = "" currentResourceURL = "" } } }