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 */,