From f083b65b034066c616093729b0df99bc8f765ea8 Mon Sep 17 00:00:00 2001 From: osy <50960678+osy@users.noreply.github.com> Date: Sat, 7 Oct 2023 17:54:54 -0700 Subject: [PATCH] downloader: support caching tools download Last-Modified Resolves #5692 --- Platform/UTMData.swift | 2 +- Platform/UTMDownloadIPSWTask.swift | 2 +- Platform/UTMDownloadSupportToolsTask.swift | 25 +++++++++++------ Platform/UTMDownloadTask.swift | 32 ++++++++++++++++++++-- Platform/UTMDownloadVMTask.swift | 2 +- 5 files changed, 50 insertions(+), 13 deletions(-) diff --git a/Platform/UTMData.swift b/Platform/UTMData.swift index 0fe886c8b..77d4b81ce 100644 --- a/Platform/UTMData.swift +++ b/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 { diff --git a/Platform/UTMDownloadIPSWTask.swift b/Platform/UTMDownloadIPSWTask.swift index 05a33fd43..f915477e7 100644 --- a/Platform/UTMDownloadIPSWTask.swift +++ b/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) } diff --git a/Platform/UTMDownloadSupportToolsTask.swift b/Platform/UTMDownloadSupportToolsTask.swift index ed1c218d1..c08b34a8b 100644 --- a/Platform/UTMDownloadSupportToolsTask.swift +++ b/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() } diff --git a/Platform/UTMDownloadTask.swift b/Platform/UTMDownloadTask.swift index 7a06ac2fc..cd532eed2 100644 --- a/Platform/UTMDownloadTask.swift +++ b/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) + } } diff --git a/Platform/UTMDownloadVMTask.swift b/Platform/UTMDownloadVMTask.swift index 75cb0f941..9f9edfaf9 100644 --- a/Platform/UTMDownloadVMTask.swift +++ b/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)