From bfecac3ca3d4b29ec3c3effcbb5e2bef9fb0f30f Mon Sep 17 00:00:00 2001
From: Naveenraj M <22456988+naveenrajm7@users.noreply.github.com>
Date: Wed, 9 Oct 2024 21:13:34 -0600
Subject: [PATCH] scripting: add import command import new vm from a file
---
Platform/UTMData.swift | 51 ++++++++++++++++
Scripting/UTM.sdef | 11 ++++
Scripting/UTMScripting.swift | 1 +
Scripting/UTMScriptingImportCommand.swift | 72 +++++++++++++++++++++++
UTM.xcodeproj/project.pbxproj | 4 ++
5 files changed, 139 insertions(+)
create mode 100644 Scripting/UTMScriptingImportCommand.swift
diff --git a/Platform/UTMData.swift b/Platform/UTMData.swift
index b1364bcb8..271d7c710 100644
--- a/Platform/UTMData.swift
+++ b/Platform/UTMData.swift
@@ -670,6 +670,57 @@ struct AlertMessage: Identifiable {
listAdd(vm: vm)
listSelect(vm: vm)
}
+
+ /// Handles UTM file URLs similar to importUTM, with few differences
+ ///
+ /// Always creates new VM (no shortcuts)
+ /// Copies VM file with a unique name to default storage (to avoid duplicates)
+ /// Returns VM data Object (to access UUID)
+ /// - Parameter url: File URL to read from
+ func importNewUTM(from url: URL) async throws -> VMData {
+ guard url.isFileURL else {
+ throw UTMDataError.importFailed
+ }
+ let isScopedAccess = url.startAccessingSecurityScopedResource()
+ defer {
+ if isScopedAccess {
+ url.stopAccessingSecurityScopedResource()
+ }
+ }
+
+ logger.info("importing: \(url)")
+ // attempt to turn temp URL to presistent bookmark early otherwise,
+ // when stopAccessingSecurityScopedResource() is called, we lose access
+ let bookmark = try url.persistentBookmarkData()
+ let url = try URL(resolvingPersistentBookmarkData: bookmark)
+
+ // get unique filename, for every import we create a new VM
+ let newUrl = UTMData.newImage(from: url, to: documentsURL)
+ let fileName = newUrl.lastPathComponent
+ // create destination name (default storage + file name)
+ let dest = documentsURL.appendingPathComponent(fileName, isDirectory: true)
+
+ // check if VM is valid
+ guard let _ = try? VMData(url: url) else {
+ throw UTMDataError.importFailed
+ }
+
+ // Copy file to documents
+ let vm: VMData?
+ logger.info("copying to Documents")
+ try fileManager.copyItem(at: url, to: dest)
+ vm = try VMData(url: dest)
+
+ guard let vm = vm else {
+ throw UTMDataError.importParseFailed
+ }
+
+ // Add vm to the list
+ listAdd(vm: vm)
+ listSelect(vm: vm)
+
+ return vm
+ }
private func copyItemWithCopyfile(at srcURL: URL, to dstURL: URL) async throws {
let totalSize = computeSize(recursiveFor: srcURL)
diff --git a/Scripting/UTM.sdef b/Scripting/UTM.sdef
index 506164656..80000bb73 100644
--- a/Scripting/UTM.sdef
+++ b/Scripting/UTM.sdef
@@ -92,6 +92,17 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Scripting/UTMScripting.swift b/Scripting/UTMScripting.swift
index 3814b1f4f..9cdec6d0f 100644
--- a/Scripting/UTMScripting.swift
+++ b/Scripting/UTMScripting.swift
@@ -159,6 +159,7 @@ import ScriptingBridge
@objc optional func print(_ x: Any!, withProperties: [AnyHashable : Any]!, printDialog: Bool) // Print a document.
@objc optional func quitSaving(_ saving: UTMScriptingSaveOptions) // Quit the application.
@objc optional func exists(_ x: Any!) -> Bool // Verify that an object exists.
+ @objc optional func importNew(_ new_: NSNumber!, from: URL!) -> SBObject // Import a new virtual machine from a file.
@objc optional func virtualMachines() -> SBElementArray
@objc optional var autoTerminate: Bool { get } // Auto terminate the application when all windows are closed?
@objc optional func setAutoTerminate(_ autoTerminate: Bool) // Auto terminate the application when all windows are closed?
diff --git a/Scripting/UTMScriptingImportCommand.swift b/Scripting/UTMScriptingImportCommand.swift
new file mode 100644
index 000000000..d445731f3
--- /dev/null
+++ b/Scripting/UTMScriptingImportCommand.swift
@@ -0,0 +1,72 @@
+//
+// Copyright © 2024 naveenrajm7. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+import Foundation
+
+@MainActor
+@objc(UTMScriptingImportCommand)
+class UTMScriptingImportCommand: NSCreateCommand, UTMScriptable {
+
+ private var data: UTMData? {
+ (NSApp.scriptingDelegate as? AppDelegate)?.data
+ }
+
+ @objc override func performDefaultImplementation() -> Any? {
+ if createClassDescription.implementationClassName == "UTMScriptingVirtualMachineImpl" {
+ withScriptCommand(self) { [self] in
+ // Retrieve the import file URL from the evaluated arguments
+ guard let fileUrl = evaluatedArguments?["file"] as? URL else {
+ throw ScriptingError.fileNotSpecified
+ }
+
+ // Validate the file (UTM is a directory) path
+ guard FileManager.default.fileExists(atPath: fileUrl.path) else {
+ throw ScriptingError.fileNotFound
+ }
+ return try await importVirtualMachine(from: fileUrl).objectSpecifier
+ }
+ return nil
+ } else {
+ return super.performDefaultImplementation()
+ }
+ }
+
+ private func importVirtualMachine(from url: URL) async throws -> UTMScriptingVirtualMachineImpl {
+ guard let data = data else {
+ throw ScriptingError.notReady
+ }
+
+ // import the VM
+ let vm = try await data.importNewUTM(from: url)
+
+ // return VM scripting object
+ return UTMScriptingVirtualMachineImpl(for: vm, data: data)
+ }
+
+ enum ScriptingError: Error, LocalizedError {
+ case notReady
+ case fileNotFound
+ case fileNotSpecified
+
+ var errorDescription: String? {
+ switch self {
+ case .notReady: return NSLocalizedString("UTM is not ready to accept commands.", comment: "UTMScriptingAppDelegate")
+ case .fileNotFound: return NSLocalizedString("A valid UTM file must be specified.", comment: "UTMScriptingAppDelegate")
+ case .fileNotSpecified: return NSLocalizedString("No file specified in the command.", comment: "UTMScriptingAppDelegate")
+ }
+ }
+ }
+}
diff --git a/UTM.xcodeproj/project.pbxproj b/UTM.xcodeproj/project.pbxproj
index 5f2e09525..f1bb27e1b 100644
--- a/UTM.xcodeproj/project.pbxproj
+++ b/UTM.xcodeproj/project.pbxproj
@@ -270,6 +270,7 @@
85EC516627CC8D10004A51DE /* VMConfigAdvancedNetworkView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85EC516327CC8C98004A51DE /* VMConfigAdvancedNetworkView.swift */; };
B329049C270FE136002707AC /* AltKit in Frameworks */ = {isa = PBXBuildFile; productRef = B329049B270FE136002707AC /* AltKit */; };
B3DDF57226E9BBA300CE47F0 /* AltKit in Frameworks */ = {isa = PBXBuildFile; productRef = B3DDF57126E9BBA300CE47F0 /* AltKit */; };
+ CD77BE442CB38F060074ADD2 /* UTMScriptingImportCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD77BE432CB38F060074ADD2 /* UTMScriptingImportCommand.swift */; };
CE020BA324AEDC7C00B44AB6 /* UTMData.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE020BA224AEDC7C00B44AB6 /* UTMData.swift */; };
CE020BA424AEDC7C00B44AB6 /* UTMData.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE020BA224AEDC7C00B44AB6 /* UTMData.swift */; };
CE020BA724AEDEF000B44AB6 /* Logging in Frameworks */ = {isa = PBXBuildFile; productRef = CE020BA624AEDEF000B44AB6 /* Logging */; };
@@ -1762,6 +1763,7 @@
C03453AF2709E35100AD51AD /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/InfoPlist.strings"; sourceTree = ""; };
C03453B02709E35200AD51AD /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/Localizable.strings"; sourceTree = ""; };
C8958B6D243634DA002D86B4 /* ko */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ko; path = ko.lproj/Localizable.strings; sourceTree = ""; };
+ CD77BE432CB38F060074ADD2 /* UTMScriptingImportCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UTMScriptingImportCommand.swift; sourceTree = ""; };
CE020BA224AEDC7C00B44AB6 /* UTMData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UTMData.swift; sourceTree = ""; };
CE020BAA24AEE00000B44AB6 /* UTMLoggingSwift.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UTMLoggingSwift.swift; sourceTree = ""; };
CE020BB524B14F8400B44AB6 /* UTMVirtualMachine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UTMVirtualMachine.swift; sourceTree = ""; };
@@ -3004,6 +3006,7 @@
CE25124629BFDB87000790AB /* UTMScriptingGuestProcessImpl.swift */,
CE25124C29C55816000790AB /* UTMScriptingConfigImpl.swift */,
CE25125429C80CD4000790AB /* UTMScriptingCreateCommand.swift */,
+ CD77BE432CB38F060074ADD2 /* UTMScriptingImportCommand.swift */,
CE25125029C806AF000790AB /* UTMScriptingDeleteCommand.swift */,
CE25125229C80A18000790AB /* UTMScriptingCloneCommand.swift */,
);
@@ -3819,6 +3822,7 @@
CEF0305D26A2AFDF00667B63 /* VMWizardOSOtherView.swift in Sources */,
CEEC811B24E48EC700ACB0B3 /* SettingsView.swift in Sources */,
8443EFF42845641600B2E6E2 /* UTMQemuConfigurationDrive.swift in Sources */,
+ CD77BE442CB38F060074ADD2 /* UTMScriptingImportCommand.swift in Sources */,
CEFE96772B69A7CC000F00C9 /* VMRemoteSessionState.swift in Sources */,
CE2D957024AD4F990059923A /* VMRemovableDrivesView.swift in Sources */,
CE25124B29BFE273000790AB /* UTMScriptable.swift in Sources */,