Bladeren bron

scripting: add import command
import new vm from a file

Naveenraj M 10 maanden geleden
bovenliggende
commit
bfecac3ca3

+ 51 - 0
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)

+ 11 - 0
Scripting/UTM.sdef

@@ -92,6 +92,17 @@
             </parameter>
         </command>
         
+        <command name="import" code="coreimpo" description="Import a new virtual machine from a file.">
+            <cocoa class="UTMScriptingImportCommand"/>
+            <parameter name="new" code="imcl" type="type" description="Specify 'virtual machine' here.">
+                <cocoa key="ObjectClass"/>
+            </parameter>
+            <parameter name="from" code="ifil" type="file" description="The virtual machine file (.utm) to import.">
+                <cocoa key="file"/>
+            </parameter>
+            <result type="specifier" description="The new virtual machine (as a specifier)."/>
+        </command>
+        
         <class name="virtual machine" code="UTMv" description="A virtual machine registered in UTM." plural="virtual machines">
           <cocoa class="UTMScriptingVirtualMachineImpl"/>
 

+ 1 - 0
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?

+ 72 - 0
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")
+            }
+        }
+    }
+}

+ 4 - 0
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 = "<group>"; };
 		C03453B02709E35200AD51AD /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/Localizable.strings"; sourceTree = "<group>"; };
 		C8958B6D243634DA002D86B4 /* ko */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ko; path = ko.lproj/Localizable.strings; sourceTree = "<group>"; };
+		CD77BE432CB38F060074ADD2 /* UTMScriptingImportCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UTMScriptingImportCommand.swift; sourceTree = "<group>"; };
 		CE020BA224AEDC7C00B44AB6 /* UTMData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UTMData.swift; sourceTree = "<group>"; };
 		CE020BAA24AEE00000B44AB6 /* UTMLoggingSwift.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UTMLoggingSwift.swift; sourceTree = "<group>"; };
 		CE020BB524B14F8400B44AB6 /* UTMVirtualMachine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UTMVirtualMachine.swift; sourceTree = "<group>"; };
@@ -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 */,