UNPKG

7.25 kBJavaScriptView Raw
1import { spawn, spawnSync } from 'node:child_process';
2import { Plist, ValueDict, ValueArray, ValueString, ValueBoolean } from '@shockpkg/plist-dom';
3/**
4 * Mounter object.
5 */
6export class Mounter {
7 /**
8 * The path to hdiutil.
9 */
10
11 /**
12 * Mounter constructor.
13 *
14 * @param options Options object.
15 */
16 constructor(options = null) {
17 this.hdiutil = (options ? options.hdiutil : null) || 'hdiutil';
18 }
19
20 /**
21 * Attach a disk image.
22 *
23 * @param file Path to disk image.
24 * @param options Options object.
25 * @returns Info object.
26 */
27 async attach(file, options = null) {
28 const devices = await this._runAttach(this._argsAttach(file, options));
29 const {
30 eject,
31 ejectSync
32 } = this._createEjects(devices);
33 return {
34 devices,
35 eject,
36 ejectSync
37 };
38 }
39
40 /**
41 * Attach a disk image.
42 *
43 * @param file Path to disk image.
44 * @param options Options object.
45 * @returns Info object.
46 */
47 attachSync(file, options = null) {
48 // eslint-disable-next-line no-sync
49 const devices = this._runAttachSync(this._argsAttach(file, options));
50 const {
51 eject,
52 ejectSync
53 } = this._createEjects(devices);
54 return {
55 devices,
56 eject,
57 ejectSync
58 };
59 }
60
61 /**
62 * Eject a disk image.
63 *
64 * @param file Path to device file or volume mount point.
65 * @param options Options object.
66 */
67 async eject(file, options = null) {
68 await this._runEject(this._argsEject(file, options));
69 }
70
71 /**
72 * Eject a disk image.
73 *
74 * @param file Path to device file or volume mount point.
75 * @param options Options object.
76 */
77 ejectSync(file, options = null) {
78 // eslint-disable-next-line no-sync
79 this._runEjectSync(this._argsEject(file, options));
80 }
81
82 /**
83 * Create args for attach.
84 *
85 * @param file Path to disk image.
86 * @param options Options object.
87 * @returns Argument list.
88 */
89 _argsAttach(file, options = null) {
90 const args = ['attach', '-plist'];
91 if (options) {
92 if (options.readonly) {
93 args.push('-readonly');
94 }
95 if (options.nobrowse) {
96 args.push('-nobrowse');
97 }
98 }
99 args.push(this._fileArg(file));
100 return args;
101 }
102
103 /**
104 * Create args for eject.
105 *
106 * @param file Path to device file or volume mount point.
107 * @param options Options object.
108 * @returns Argument list.
109 */
110 _argsEject(file, options = null) {
111 const args = ['eject'];
112 if (options && options.force) {
113 args.push('-force');
114 }
115 args.push(this._fileArg(file));
116 return args;
117 }
118
119 /**
120 * Run hdiutil attach command, returning the devices list on success.
121 *
122 * @param args CLI args.
123 * @returns Devices list.
124 */
125 async _runAttach(args) {
126 const stdouts = [];
127 const proc = spawn(this.hdiutil, args);
128 proc.stdout.on('data', data => {
129 stdouts.push(data);
130 });
131 const code = await new Promise((resolve, reject) => {
132 proc.once('exit', resolve);
133 proc.once('error', reject);
134 });
135 if (code) {
136 throw new Error(`Attach failed: hdiutil exit code: ${code}`);
137 }
138 return this._parseDevices(Buffer.concat(stdouts).toString());
139 }
140
141 /**
142 * Run hdiutil attach command, returning the devices list on success.
143 *
144 * @param args CLI args.
145 * @returns Devices list.
146 */
147 _runAttachSync(args) {
148 const {
149 status,
150 error,
151 stdout
152 } = spawnSync(this.hdiutil, args);
153 if (error) {
154 throw error;
155 }
156 if (status) {
157 throw new Error(`Attach failed: hdiutil exit code: ${status}`);
158 }
159 return this._parseDevices(stdout.toString());
160 }
161
162 /**
163 * Run hdiutil eject command.
164 *
165 * @param args CLI args.
166 */
167 async _runEject(args) {
168 const proc = spawn(this.hdiutil, args);
169 const status = await new Promise((resolve, reject) => {
170 proc.once('exit', resolve);
171 proc.once('error', reject);
172 });
173 if (status) {
174 throw new Error(`Eject failed: hdiutil exit code: ${status}`);
175 }
176 }
177
178 /**
179 * Run hdiutil eject command.
180 *
181 * @param args CLI args.
182 */
183 _runEjectSync(args) {
184 const {
185 status,
186 error
187 } = spawnSync(this.hdiutil, args);
188 if (error) {
189 throw error;
190 }
191 if (status) {
192 throw new Error(`Eject failed: hdiutil exit code: ${status}`);
193 }
194 }
195
196 /**
197 * Create file argument from file path.
198 *
199 * @param file File path.
200 * @returns A path for use as argument.
201 */
202 _fileArg(file) {
203 // Make sure it will not be recognized as option argument.
204 return file.startsWith('-') ? `./${file}` : file;
205 }
206
207 /**
208 * Parse devices plist into devices list.
209 *
210 * @param xml XML plist.
211 * @returns Devices list.
212 */
213 _parseDevices(xml) {
214 const plist = new Plist();
215 plist.fromXml(xml);
216 const systemEntities = plist.valueCastAs(ValueDict).getValue('system-entities').castAs(ValueArray);
217 const r = [];
218 for (const value of systemEntities.value) {
219 const dict = value.castAs(ValueDict);
220 const devEntry = dict.getValue('dev-entry').castAs(ValueString).value;
221 const potentiallyMountable = dict.getValue('potentially-mountable').castAs(ValueBoolean).value;
222 const contentHint = dict.get('content-hint');
223 const unmappedContentHint = dict.get('unmapped-content-hint');
224 const volumeKind = dict.get('volume-kind');
225 const mountPoint = dict.get('mount-point');
226 const device = {
227 devEntry,
228 potentiallyMountable
229 };
230 if (contentHint) {
231 device.contentHint = contentHint.castAs(ValueString).value;
232 }
233 if (unmappedContentHint) {
234 device.unmappedContentHint = unmappedContentHint.castAs(ValueString).value;
235 }
236 if (volumeKind) {
237 device.volumeKind = volumeKind.castAs(ValueString).value;
238 }
239 if (mountPoint) {
240 device.mountPoint = mountPoint.castAs(ValueString).value;
241 }
242 r.push(device);
243 }
244 return r;
245 }
246
247 /**
248 * Find the root device, null on empty list.
249 *
250 * @param devices Device list.
251 * @returns Root device or null if an empty list.
252 */
253 _findRootDevice(devices) {
254 let r = null;
255 for (const device of devices) {
256 if (r === null || r.devEntry.length > device.devEntry.length) {
257 r = device;
258 }
259 }
260 return r;
261 }
262
263 /**
264 * Create ejects callback from a list of devices.
265 *
266 * @param devices Device list.
267 * @returns Callback function.
268 */
269 _createEjects(devices) {
270 // Find the root device, to use to eject (none possible in theory).
271 let devEntry = this._findRootDevice(devices)?.devEntry;
272 return {
273 /**
274 * The eject callback function.
275 *
276 * @param options Eject options.
277 */
278 eject: async (options = null) => {
279 if (devEntry) {
280 await this.eject(devEntry, options);
281 devEntry = '';
282 }
283 },
284 /**
285 * The eject callback function.
286 *
287 * @param options Eject options.
288 */
289 ejectSync: (options = null) => {
290 if (devEntry) {
291 // eslint-disable-next-line no-sync
292 this.ejectSync(devEntry, options);
293 devEntry = '';
294 }
295 }
296 };
297 }
298}
299//# sourceMappingURL=mounter.mjs.map
\No newline at end of file