Skip to content

Commit 4c14814

Browse files
authored
Merge pull request #6734 from naveenrajm7/utm-import
scripting: add import command
2 parents 28a14b9 + f11cda7 commit 4c14814

File tree

5 files changed

+139
-0
lines changed

5 files changed

+139
-0
lines changed

Platform/UTMData.swift

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -680,6 +680,57 @@ enum AlertItem: Identifiable {
680680
listAdd(vm: vm)
681681
listSelect(vm: vm)
682682
}
683+
684+
/// Handles UTM file URLs similar to importUTM, with few differences
685+
///
686+
/// Always creates new VM (no shortcuts)
687+
/// Copies VM file with a unique name to default storage (to avoid duplicates)
688+
/// Returns VM data Object (to access UUID)
689+
/// - Parameter url: File URL to read from
690+
func importNewUTM(from url: URL) async throws -> VMData {
691+
guard url.isFileURL else {
692+
throw UTMDataError.importFailed
693+
}
694+
let isScopedAccess = url.startAccessingSecurityScopedResource()
695+
defer {
696+
if isScopedAccess {
697+
url.stopAccessingSecurityScopedResource()
698+
}
699+
}
700+
701+
logger.info("importing: \(url)")
702+
// attempt to turn temp URL to presistent bookmark early otherwise,
703+
// when stopAccessingSecurityScopedResource() is called, we lose access
704+
let bookmark = try url.persistentBookmarkData()
705+
let url = try URL(resolvingPersistentBookmarkData: bookmark)
706+
707+
// get unique filename, for every import we create a new VM
708+
let newUrl = UTMData.newImage(from: url, to: documentsURL)
709+
let fileName = newUrl.lastPathComponent
710+
// create destination name (default storage + file name)
711+
let dest = documentsURL.appendingPathComponent(fileName, isDirectory: true)
712+
713+
// check if VM is valid
714+
guard let _ = try? VMData(url: url) else {
715+
throw UTMDataError.importFailed
716+
}
717+
718+
// Copy file to documents
719+
let vm: VMData?
720+
logger.info("copying to Documents")
721+
try fileManager.copyItem(at: url, to: dest)
722+
vm = try VMData(url: dest)
723+
724+
guard let vm = vm else {
725+
throw UTMDataError.importParseFailed
726+
}
727+
728+
// Add vm to the list
729+
listAdd(vm: vm)
730+
listSelect(vm: vm)
731+
732+
return vm
733+
}
683734

684735
private func copyItemWithCopyfile(at srcURL: URL, to dstURL: URL) async throws {
685736
let totalSize = computeSize(recursiveFor: srcURL)

Scripting/UTM.sdef

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,17 @@
9292
</parameter>
9393
</command>
9494

95+
<command name="import" code="coreimpo" description="Import a new virtual machine from a file.">
96+
<cocoa class="UTMScriptingImportCommand"/>
97+
<parameter name="new" code="imcl" type="type" description="Specify 'virtual machine' here.">
98+
<cocoa key="ObjectClass"/>
99+
</parameter>
100+
<parameter name="from" code="ifil" type="file" description="The virtual machine file (.utm) to import.">
101+
<cocoa key="file"/>
102+
</parameter>
103+
<result type="specifier" description="The new virtual machine (as a specifier)."/>
104+
</command>
105+
95106
<command name="export" code="coreexpo" description="Export a virtual machine to a specified location.">
96107
<cocoa class="UTMScriptingExportCommand"/>
97108
<access-group identifier="*"/>

Scripting/UTMScripting.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,7 @@ import ScriptingBridge
159159
@objc optional func print(_ x: Any!, withProperties: [AnyHashable : Any]!, printDialog: Bool) // Print a document.
160160
@objc optional func quitSaving(_ saving: UTMScriptingSaveOptions) // Quit the application.
161161
@objc optional func exists(_ x: Any!) -> Bool // Verify that an object exists.
162+
@objc optional func importNew(_ new_: NSNumber!, from: URL!) -> SBObject // Import a new virtual machine from a file.
162163
@objc optional func virtualMachines() -> SBElementArray
163164
@objc optional var autoTerminate: Bool { get } // Auto terminate the application when all windows are closed?
164165
@objc optional func setAutoTerminate(_ autoTerminate: Bool) // Auto terminate the application when all windows are closed?
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
//
2+
// Copyright © 2024 naveenrajm7. 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+
17+
import Foundation
18+
19+
@MainActor
20+
@objc(UTMScriptingImportCommand)
21+
class UTMScriptingImportCommand: NSCreateCommand, UTMScriptable {
22+
23+
private var data: UTMData? {
24+
(NSApp.scriptingDelegate as? AppDelegate)?.data
25+
}
26+
27+
@objc override func performDefaultImplementation() -> Any? {
28+
if createClassDescription.implementationClassName == "UTMScriptingVirtualMachineImpl" {
29+
withScriptCommand(self) { [self] in
30+
// Retrieve the import file URL from the evaluated arguments
31+
guard let fileUrl = evaluatedArguments?["file"] as? URL else {
32+
throw ScriptingError.fileNotSpecified
33+
}
34+
35+
// Validate the file (UTM is a directory) path
36+
guard FileManager.default.fileExists(atPath: fileUrl.path) else {
37+
throw ScriptingError.fileNotFound
38+
}
39+
return try await importVirtualMachine(from: fileUrl).objectSpecifier
40+
}
41+
return nil
42+
} else {
43+
return super.performDefaultImplementation()
44+
}
45+
}
46+
47+
private func importVirtualMachine(from url: URL) async throws -> UTMScriptingVirtualMachineImpl {
48+
guard let data = data else {
49+
throw ScriptingError.notReady
50+
}
51+
52+
// import the VM
53+
let vm = try await data.importNewUTM(from: url)
54+
55+
// return VM scripting object
56+
return UTMScriptingVirtualMachineImpl(for: vm, data: data)
57+
}
58+
59+
enum ScriptingError: Error, LocalizedError {
60+
case notReady
61+
case fileNotFound
62+
case fileNotSpecified
63+
64+
var errorDescription: String? {
65+
switch self {
66+
case .notReady: return NSLocalizedString("UTM is not ready to accept commands.", comment: "UTMScriptingAppDelegate")
67+
case .fileNotFound: return NSLocalizedString("A valid UTM file must be specified.", comment: "UTMScriptingAppDelegate")
68+
case .fileNotSpecified: return NSLocalizedString("No file specified in the command.", comment: "UTMScriptingAppDelegate")
69+
}
70+
}
71+
}
72+
}

UTM.xcodeproj/project.pbxproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,7 @@
270270
85EC516627CC8D10004A51DE /* VMConfigAdvancedNetworkView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85EC516327CC8C98004A51DE /* VMConfigAdvancedNetworkView.swift */; };
271271
B329049C270FE136002707AC /* AltKit in Frameworks */ = {isa = PBXBuildFile; productRef = B329049B270FE136002707AC /* AltKit */; };
272272
B3DDF57226E9BBA300CE47F0 /* AltKit in Frameworks */ = {isa = PBXBuildFile; productRef = B3DDF57126E9BBA300CE47F0 /* AltKit */; };
273+
CD77BE442CB38F060074ADD2 /* UTMScriptingImportCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD77BE432CB38F060074ADD2 /* UTMScriptingImportCommand.swift */; };
273274
CD77BE422CAB51B40074ADD2 /* UTMScriptingExportCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD77BE412CAB519F0074ADD2 /* UTMScriptingExportCommand.swift */; };
274275
CE020BA324AEDC7C00B44AB6 /* UTMData.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE020BA224AEDC7C00B44AB6 /* UTMData.swift */; };
275276
CE020BA424AEDC7C00B44AB6 /* UTMData.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE020BA224AEDC7C00B44AB6 /* UTMData.swift */; };
@@ -1776,6 +1777,7 @@
17761777
C03453AF2709E35100AD51AD /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
17771778
C03453B02709E35200AD51AD /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/Localizable.strings"; sourceTree = "<group>"; };
17781779
C8958B6D243634DA002D86B4 /* ko */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ko; path = ko.lproj/Localizable.strings; sourceTree = "<group>"; };
1780+
CD77BE432CB38F060074ADD2 /* UTMScriptingImportCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UTMScriptingImportCommand.swift; sourceTree = "<group>"; };
17791781
CD77BE412CAB519F0074ADD2 /* UTMScriptingExportCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UTMScriptingExportCommand.swift; sourceTree = "<group>"; };
17801782
CE020BA224AEDC7C00B44AB6 /* UTMData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UTMData.swift; sourceTree = "<group>"; };
17811783
CE020BAA24AEE00000B44AB6 /* UTMLoggingSwift.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UTMLoggingSwift.swift; sourceTree = "<group>"; };
@@ -3027,6 +3029,7 @@
30273029
CE25124629BFDB87000790AB /* UTMScriptingGuestProcessImpl.swift */,
30283030
CE25124C29C55816000790AB /* UTMScriptingConfigImpl.swift */,
30293031
CE25125429C80CD4000790AB /* UTMScriptingCreateCommand.swift */,
3032+
CD77BE432CB38F060074ADD2 /* UTMScriptingImportCommand.swift */,
30303033
CE25125029C806AF000790AB /* UTMScriptingDeleteCommand.swift */,
30313034
CE25125229C80A18000790AB /* UTMScriptingCloneCommand.swift */,
30323035
CD77BE412CAB519F0074ADD2 /* UTMScriptingExportCommand.swift */,
@@ -3844,6 +3847,7 @@
38443847
CEF0305D26A2AFDF00667B63 /* VMWizardOSOtherView.swift in Sources */,
38453848
CEEC811B24E48EC700ACB0B3 /* SettingsView.swift in Sources */,
38463849
8443EFF42845641600B2E6E2 /* UTMQemuConfigurationDrive.swift in Sources */,
3850+
CD77BE442CB38F060074ADD2 /* UTMScriptingImportCommand.swift in Sources */,
38473851
CEFE96772B69A7CC000F00C9 /* VMRemoteSessionState.swift in Sources */,
38483852
CEEF26A72CEDAEEA003F7B8C /* UTMDownloadMacSupportToolsTask.swift in Sources */,
38493853
CE2D957024AD4F990059923A /* VMRemovableDrivesView.swift in Sources */,

0 commit comments

Comments
 (0)