UTMConfigurationDrive.swift 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124
  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 QEMUKitInternal
  18. /// Settings for single disk device
  19. protocol UTMConfigurationDrive: Codable, Hashable, Identifiable {
  20. /// If not removable, this is the name of the file in the bundle.
  21. var imageName: String? { get set }
  22. /// Size of the image when creating a new image (in MiB).
  23. var sizeMib: Int { get }
  24. /// If true, the drive image will be mounted as read-only.
  25. var isReadOnly: Bool { get }
  26. /// If true, a bookmark is stored in the package.
  27. var isExternal: Bool { get }
  28. /// If true, the created image will be raw format and not QCOW2. Not saved.
  29. var isRawImage: Bool { get }
  30. /// If valid, will point to the actual location of the drive image. Not saved.
  31. var imageURL: URL? { get set }
  32. /// Unique identifier for this drive
  33. var id: String { get }
  34. /// Create a new copy with a unique ID
  35. /// - Returns: Copy
  36. func clone() -> Self
  37. }
  38. extension UTMConfigurationDrive {
  39. static func == (lhs: Self, rhs: Self) -> Bool {
  40. lhs.hashValue == rhs.hashValue
  41. }
  42. }
  43. // MARK: - Saving data
  44. extension UTMConfigurationDrive {
  45. private var bytesInMib: UInt64 { 1048576 }
  46. @MainActor mutating func saveData(to dataURL: URL) async throws -> [URL] {
  47. guard !isExternal else {
  48. return [] // nothing to save
  49. }
  50. let fileManager = FileManager.default
  51. if let imageURL = imageURL {
  52. #if os(macOS)
  53. let newURL = try await UTMQemuConfiguration.copyItemIfChanged(from: imageURL, to: dataURL, customCopy: isRawImage ? nil : convertQcow2Image)
  54. #else
  55. let newURL = try await UTMQemuConfiguration.copyItemIfChanged(from: imageURL, to: dataURL)
  56. #endif
  57. self.imageName = newURL.lastPathComponent
  58. self.imageURL = newURL
  59. return [newURL]
  60. } else if imageName == nil {
  61. let newName = "\(id).\(isRawImage ? "img" : "qcow2")"
  62. let newURL = dataURL.appendingPathComponent(newName)
  63. guard !fileManager.fileExists(atPath: newURL.path) else {
  64. throw UTMConfigurationError.driveAlreadyExists(newURL)
  65. }
  66. if isRawImage {
  67. try await createRawImage(at: newURL, size: sizeMib)
  68. } else {
  69. try await createQcow2Image(at: newURL, size: sizeMib)
  70. }
  71. self.imageName = newName
  72. self.imageURL = newURL
  73. return [newURL]
  74. } else {
  75. let existingURL = dataURL.appendingPathComponent(imageName!)
  76. return [existingURL]
  77. }
  78. }
  79. private func createRawImage(at newURL: URL, size sizeMib: Int) async throws {
  80. let fileManager = FileManager.default
  81. let size = UInt64(sizeMib) * bytesInMib
  82. try await Task.detached {
  83. guard fileManager.createFile(atPath: newURL.path, contents: nil, attributes: nil) else {
  84. throw UTMConfigurationError.cannotCreateDiskImage
  85. }
  86. let handle = try FileHandle(forWritingTo: newURL)
  87. try handle.truncate(atOffset: size)
  88. try handle.close()
  89. }.value
  90. }
  91. private func createQcow2Image(at newURL: URL, size sizeMib: Int) async throws {
  92. try await Task.detached {
  93. if !QEMUGenerateDefaultQcow2File(newURL as CFURL, sizeMib) {
  94. throw UTMConfigurationError.cannotCreateDiskImage
  95. }
  96. }.value
  97. }
  98. #if os(macOS)
  99. private func convertQcow2Image(at sourceURL: URL, to destFolderURL: URL) async throws -> URL {
  100. let fileManager = FileManager.default
  101. let destQcow2 = UTMData.newImage(from: sourceURL,
  102. to: destFolderURL,
  103. withExtension: "qcow2")
  104. try await UTMQemuImage.convert(from: sourceURL, toQcow2: destQcow2)
  105. return destQcow2
  106. }
  107. #endif
  108. }