UTMDownloadVMTask.swift 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112
  1. //
  2. // Copyright © 2022 osy. All rights reserved.
  3. //
  4. // Licensed under the Apache License, Version 2.0 (the "License");
  5. // you may not use this file except in compliance with the License.
  6. // You may obtain a copy of the License at
  7. //
  8. // http://www.apache.org/licenses/LICENSE-2.0
  9. //
  10. // Unless required by applicable law or agreed to in writing, software
  11. // distributed under the License is distributed on an "AS IS" BASIS,
  12. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. // See the License for the specific language governing permissions and
  14. // limitations under the License.
  15. //
  16. import Foundation
  17. import Logging
  18. import ZIPFoundation
  19. /// Downloads a VM and creates a pending VM placeholder.
  20. class UTMDownloadVMTask: UTMDownloadTask {
  21. init(for url: URL) {
  22. super.init(for: url, named: UTMDownloadVMTask.name(for: url))
  23. }
  24. static private func name(for url: URL) -> String {
  25. /// try to detect the filename from the URL
  26. let filename = url.lastPathComponent
  27. var nameWithoutZIP = "UTM Virtual Machine"
  28. /// Try to get the start index of the `.zip` part of the filename
  29. if let index = filename.range(of: ".zip", options: [])?.lowerBound {
  30. nameWithoutZIP = String(filename[..<index])
  31. }
  32. return nameWithoutZIP
  33. }
  34. override func processCompletedDownload(at location: URL, response: URLResponse?) async throws -> any UTMVirtualMachine {
  35. let tempDir = fileManager.temporaryDirectory
  36. let originalFilename = url.lastPathComponent
  37. let downloadedZip = tempDir.appendingPathComponent(originalFilename)
  38. var fileURL: URL? = nil
  39. do {
  40. if fileManager.fileExists(atPath: downloadedZip.path) {
  41. try fileManager.removeItem(at: downloadedZip)
  42. }
  43. try fileManager.moveItem(at: location, to: downloadedZip)
  44. let utmURL = try partialUnzipOnlyUtmVM(zipFileURL: downloadedZip, destinationFolder: UTMData.defaultStorageUrl, fileManager: fileManager)
  45. /// set the url so we know, if it fails after this step the UTM in the ZIP is corrupted
  46. fileURL = utmURL
  47. /// remove the downloaded ZIP file
  48. try fileManager.removeItem(at: downloadedZip)
  49. /// load the downloaded VM into the UI
  50. let vm = try await VMData(url: utmURL)
  51. return await vm.wrapped!
  52. } catch {
  53. logger.error(Logger.Message(stringLiteral: error.localizedDescription))
  54. if let fileURL = fileURL {
  55. /// remove imported UTM, as it is corrupted
  56. try? fileManager.removeItem(at: fileURL)
  57. } else {
  58. /// failed earlier
  59. try? fileManager.removeItem(at: downloadedZip)
  60. }
  61. throw error
  62. }
  63. }
  64. private func partialUnzipOnlyUtmVM(zipFileURL: URL, destinationFolder: URL, fileManager: FileManager) throws -> URL {
  65. let utmFileEnding = ".utm"
  66. let utmDirectoryEnding = "\(utmFileEnding)/"
  67. if let archive = Archive(url: zipFileURL, accessMode: .read),
  68. /// find the UTM directory and its contents
  69. let utmFolderInZip = archive.first(where: { $0.path.hasSuffix(utmDirectoryEnding) }) {
  70. /// get the UTM package filename
  71. let originalFileName = URL(fileURLWithPath: utmFolderInZip.path).lastPathComponent
  72. var destinationUtmDirectory = originalFileName
  73. /// check if the UTM already exists
  74. var duplicateIndex = 2
  75. while fileManager.fileExists(atPath: destinationFolder.appendingPathComponent(destinationUtmDirectory).path) {
  76. destinationUtmDirectory = originalFileName.replacingOccurrences(of: utmFileEnding, with: " (\(duplicateIndex))\(utmFileEnding)")
  77. duplicateIndex += 1
  78. }
  79. /// got destination folder name
  80. let destinationURL = destinationFolder.appendingPathComponent(destinationUtmDirectory, isDirectory: true)
  81. /// create the .utm directory
  82. try fileManager.createDirectory(at: destinationURL, withIntermediateDirectories: false)
  83. /// get and extract all files contained in the UTM directory, except the `__MACOSX` folder
  84. let containedFiles = archive.filter({ $0.path.contains(utmDirectoryEnding) && !$0.path.hasSuffix(utmDirectoryEnding) && !$0.path.contains("__MACOSX") })
  85. for file in containedFiles {
  86. let relativePath = file.path.replacingOccurrences(of: utmFolderInZip.path, with: "")
  87. let isDirectory = file.path.hasSuffix("/")
  88. _ = try archive.extract(file, to: destinationURL.appendingPathComponent(relativePath, isDirectory: isDirectory), skipCRC32: true)
  89. }
  90. return destinationURL
  91. } else {
  92. throw UnzipNoUTMFileError()
  93. }
  94. }
  95. private class UnzipNoUTMFileError: Error {
  96. var errorDescription: String? {
  97. NSLocalizedString("There is no UTM file in the downloaded ZIP archive.", comment: "Error shown when importing a ZIP file from web that doesn't contain a UTM Virtual Machine.")
  98. }
  99. }
  100. private class CreateUTMFailed: Error {
  101. var errorDescription: String? {
  102. NSLocalizedString("Failed to parse the downloaded VM.", comment: "UTMDownloadVMTask")
  103. }
  104. }
  105. }