Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

scripting: add import command #6734

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions Platform/UTMData.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
11 changes: 11 additions & 0 deletions Scripting/UTM.sdef
Original file line number Diff line number Diff line change
Expand Up @@ -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"/>

Expand Down
1 change: 1 addition & 0 deletions Scripting/UTMScripting.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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?
Expand Down
72 changes: 72 additions & 0 deletions Scripting/UTMScriptingImportCommand.swift
Original file line number Diff line number Diff line change
@@ -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 changes: 4 additions & 0 deletions UTM.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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 */; };
Expand Down Expand Up @@ -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>"; };
Expand Down Expand Up @@ -3004,6 +3006,7 @@
CE25124629BFDB87000790AB /* UTMScriptingGuestProcessImpl.swift */,
CE25124C29C55816000790AB /* UTMScriptingConfigImpl.swift */,
CE25125429C80CD4000790AB /* UTMScriptingCreateCommand.swift */,
CD77BE432CB38F060074ADD2 /* UTMScriptingImportCommand.swift */,
CE25125029C806AF000790AB /* UTMScriptingDeleteCommand.swift */,
CE25125229C80A18000790AB /* UTMScriptingCloneCommand.swift */,
);
Expand Down Expand Up @@ -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 */,
Expand Down