Просмотр исходного кода

downloader: support caching tools download Last-Modified

Resolves #5692
osy 1 год назад
Родитель
Сommit
f083b65b03

+ 1 - 1
Platform/UTMData.swift

@@ -679,7 +679,7 @@ struct AlertMessage: Identifiable {
     
     func mountSupportTools(for vm: UTMQemuVirtualMachine) async throws {
         let task = UTMDownloadSupportToolsTask(for: vm)
-        if task.hasExistingSupportTools {
+        if await task.hasExistingSupportTools {
             vm.config.qemu.isGuestToolsInstallRequested = false
             _ = try await task.mountTools()
         } else {

+ 1 - 1
Platform/UTMDownloadIPSWTask.swift

@@ -32,7 +32,7 @@ class UTMDownloadIPSWTask: UTMDownloadTask {
         super.init(for: config.system.boot.macRecoveryIpswURL!, named: config.information.name)
     }
     
-    override func processCompletedDownload(at location: URL) async throws -> any UTMVirtualMachine {
+    override func processCompletedDownload(at location: URL, response: URLResponse?) async throws -> any UTMVirtualMachine {
         if !fileManager.fileExists(atPath: cacheUrl.path) {
             try fileManager.createDirectory(at: cacheUrl, withIntermediateDirectories: false)
         }

+ 17 - 8
Platform/UTMDownloadSupportToolsTask.swift

@@ -22,16 +22,24 @@ class UTMDownloadSupportToolsTask: UTMDownloadTask {
     
     private static let supportToolsDownloadUrl = URL(string: "https://getutm.app/downloads/utm-guest-tools-latest.iso")!
     
-    private var cacheUrl: URL {
-        fileManager.urls(for: .cachesDirectory, in: .userDomainMask).first!
+    private var toolsUrl: URL {
+        fileManager.urls(for: .applicationSupportDirectory, in: .userDomainMask).first!.appendingPathComponent("GuestSupportTools")
     }
     
     private var supportToolsLocalUrl: URL {
-        cacheUrl.appendingPathComponent(Self.supportToolsDownloadUrl.lastPathComponent)
+        toolsUrl.appendingPathComponent(Self.supportToolsDownloadUrl.lastPathComponent)
     }
-    
+
+    @Setting("LastDownloadedGuestTools")
+    private var lastDownloadGuestTools: Int = 0
+
     var hasExistingSupportTools: Bool {
-        fileManager.fileExists(atPath: supportToolsLocalUrl.path)
+        get async {
+            guard fileManager.fileExists(atPath: supportToolsLocalUrl.path) else {
+                return false
+            }
+            return await lastModifiedTimestamp <= lastDownloadGuestTools
+        }
     }
     
     init(for vm: UTMQemuVirtualMachine) {
@@ -40,14 +48,15 @@ class UTMDownloadSupportToolsTask: UTMDownloadTask {
         super.init(for: Self.supportToolsDownloadUrl, named: name)
     }
     
-    override func processCompletedDownload(at location: URL) async throws -> any UTMVirtualMachine {
-        if !fileManager.fileExists(atPath: cacheUrl.path) {
-            try fileManager.createDirectory(at: cacheUrl, withIntermediateDirectories: true)
+    override func processCompletedDownload(at location: URL, response: URLResponse?) async throws -> any UTMVirtualMachine {
+        if !fileManager.fileExists(atPath: toolsUrl.path) {
+            try fileManager.createDirectory(at: toolsUrl, withIntermediateDirectories: true)
         }
         if fileManager.fileExists(atPath: supportToolsLocalUrl.path) {
             try fileManager.removeItem(at: supportToolsLocalUrl)
         }
         try fileManager.moveItem(at: location, to: supportToolsLocalUrl)
+        lastDownloadGuestTools = lastModifiedTimestamp(for: response) ?? 0
         return try await mountTools()
     }
     

+ 30 - 2
Platform/UTMDownloadTask.swift

@@ -32,6 +32,18 @@ class UTMDownloadTask: NSObject, URLSessionDelegate, URLSessionDownloadDelegate
         FileManager.default
     }
     
+    /// Find the Last-Modified date as a Unix timestamp
+    var lastModifiedTimestamp: Int {
+        get async {
+            var request = URLRequest(url: url)
+            request.httpMethod = "HEAD"
+            guard let (_, response) = try? await URLSession.shared.data(for: request) else {
+                return 0
+            }
+            return lastModifiedTimestamp(for: response) ?? 0
+        }
+    }
+
     init(for url: URL, named name: String) {
         self.url = url
         self.name = name
@@ -39,8 +51,9 @@ class UTMDownloadTask: NSObject, URLSessionDelegate, URLSessionDownloadDelegate
     
     /// Called by subclass when download is completed
     /// - Parameter location: Downloaded file location
+    /// - Parameter response: URL response of the download
     /// - Returns: Processed UTM virtual machine
-    func processCompletedDownload(at location: URL) async throws -> any UTMVirtualMachine {
+    func processCompletedDownload(at location: URL, response: URLResponse?) async throws -> any UTMVirtualMachine {
         throw "Not Implemented"
     }
     
@@ -67,7 +80,7 @@ class UTMDownloadTask: NSObject, URLSessionDelegate, URLSessionDownloadDelegate
         Task {
             await pendingVM.setDownloadFinishedNowProcessing()
             do {
-                let vm = try await processCompletedDownload(at: tmpUrl)
+                let vm = try await processCompletedDownload(at: tmpUrl, response: sessionTask.response)
                 taskContinuation.resume(returning: vm)
             } catch {
                 taskContinuation.resume(throwing: error)
@@ -165,4 +178,19 @@ class UTMDownloadTask: NSObject, URLSessionDelegate, URLSessionDownloadDelegate
     func cancel() {
         downloadTask?.cancel()
     }
+    
+    /// Get the Last-Modified header as a Unix timestamp
+    /// - Parameter response: URL response
+    /// - Returns: Unix timestamp
+    func lastModifiedTimestamp(for response: URLResponse?) -> Int? {
+        guard let headers = (response as? HTTPURLResponse)?.allHeaderFields, let lastModified = headers["Last-Modified"] as? String else {
+            return nil
+        }
+        let dateFormatter = DateFormatter()
+        dateFormatter.dateFormat = "EEE, dd MMM yyyy HH:mm:ss zzz"
+        guard let lastModifiedDate = dateFormatter.date(from: lastModified) else {
+            return nil
+        }
+        return Int(lastModifiedDate.timeIntervalSince1970)
+    }
 }

+ 1 - 1
Platform/UTMDownloadVMTask.swift

@@ -35,7 +35,7 @@ class UTMDownloadVMTask: UTMDownloadTask {
         return nameWithoutZIP
     }
     
-    override func processCompletedDownload(at location: URL) async throws -> any UTMVirtualMachine {
+    override func processCompletedDownload(at location: URL, response: URLResponse?) async throws -> any UTMVirtualMachine {
         let tempDir = fileManager.temporaryDirectory
         let originalFilename = url.lastPathComponent
         let downloadedZip = tempDir.appendingPathComponent(originalFilename)