UNPKG

12.5 kBJavaScriptView Raw
1"use strict";
2
3Object.defineProperty(exports, "__esModule", {
4 value: true
5});
6exports.Bundle = void 0;
7var _nodeFs = require("node:fs");
8var _promises = require("node:fs/promises");
9var _nodeStream = require("node:stream");
10var _promises2 = require("node:stream/promises");
11var _nodePath = require("node:path");
12var _archiveFiles = require("@shockpkg/archive-files");
13var _queue = require("./queue.js");
14const userExec = 0b001000000;
15
16/**
17 * Options for adding resources.
18 */
19
20/**
21 * Bundle object.
22 */
23class Bundle {
24 /**
25 * File and directory names to exclude when adding a directory.
26 */
27 excludes = [/^\./, /^ehthumbs\.db$/i, /^Thumbs\.db$/i];
28
29 /**
30 * Bundle main executable path.
31 */
32
33 /**
34 * Flat bundle.
35 */
36
37 /**
38 * Projector instance.
39 */
40
41 /**
42 * Open flag.
43 */
44 _isOpen = false;
45
46 /**
47 * Close callbacks priority queue.
48 */
49 _closeQueue = new _queue.Queue();
50
51 /**
52 * Bundle constructor.
53 *
54 * @param path Output path for the main executable.
55 * @param flat Flat bundle.
56 */
57 constructor(path, flat = false) {
58 this.path = path;
59 this.flat = flat;
60 }
61
62 /**
63 * Check if output open.
64 *
65 * @returns Returns true if open, else false.
66 */
67 get isOpen() {
68 return this._isOpen;
69 }
70
71 /**
72 * Check if name is excluded file.
73 *
74 * @param name File name.
75 * @returns Returns true if excluded, else false.
76 */
77 isExcludedFile(name) {
78 for (const exclude of this.excludes) {
79 if (exclude.test(name)) {
80 return true;
81 }
82 }
83 return false;
84 }
85
86 /**
87 * Open output.
88 */
89 async open() {
90 if (this._isOpen) {
91 throw new Error('Already open');
92 }
93 await this._checkOutput();
94 this._closeQueue.clear();
95 await this._open();
96 this._isOpen = true;
97 }
98
99 /**
100 * Close output.
101 */
102 async close() {
103 this._assertIsOpen();
104 try {
105 await this._close();
106 } finally {
107 this._closeQueue.clear();
108 }
109 this._isOpen = false;
110 }
111
112 /**
113 * Write out the bundle.
114 * Has a callback to write out the resources.
115 *
116 * @param func Async function.
117 * @returns Return value of the async function.
118 */
119 async write(func = null) {
120 await this.open();
121 try {
122 return func ? await func.call(this, this) : null;
123 } finally {
124 await this.close();
125 }
126 }
127
128 /**
129 * Get path for resource.
130 *
131 * @param destination Resource destination.
132 * @returns Destination path.
133 */
134 resourcePath(destination) {
135 return (0, _nodePath.join)((0, _nodePath.dirname)(this.projector.path), destination);
136 }
137
138 /**
139 * Check if path for resource exists.
140 *
141 * @param destination Resource destination.
142 * @returns True if destination exists, else false.
143 */
144 async resourceExists(destination) {
145 return !!(await (0, _archiveFiles.fsLstatExists)(this.resourcePath(destination)));
146 }
147
148 /**
149 * Copy resource, detecting source type automatically.
150 *
151 * @param destination Resource destination.
152 * @param source Source directory.
153 * @param options Resource options.
154 */
155 async copyResource(destination, source, options = null) {
156 this._assertIsOpen();
157 const stat = await (0, _promises.lstat)(source);
158 switch (true) {
159 case stat.isSymbolicLink():
160 {
161 await this.copyResourceSymlink(destination, source, options);
162 break;
163 }
164 case stat.isFile():
165 {
166 await this.copyResourceFile(destination, source, options);
167 break;
168 }
169 case stat.isDirectory():
170 {
171 await this.copyResourceDirectory(destination, source, options);
172 break;
173 }
174 default:
175 {
176 throw new Error(`Unsupported resource type: ${source}`);
177 }
178 }
179 }
180
181 /**
182 * Copy directory as resource, recursive copy.
183 *
184 * @param destination Resource destination.
185 * @param source Source directory.
186 * @param options Resource options.
187 */
188 async copyResourceDirectory(destination, source, options = null) {
189 this._assertIsOpen();
190
191 // Create directory.
192 await this.createResourceDirectory(destination, options ? await this._expandResourceOptionsCopy(options, async () => (0, _promises.stat)(source)) : options);
193
194 // If not recursive do not walk contents.
195 if (options && options.noRecurse) {
196 return;
197 }
198
199 // Any directories we add should not be recursive.
200 const opts = {
201 ...(options || {}),
202 noRecurse: true
203 };
204 await (0, _archiveFiles.fsWalk)(source, async (path, stat) => {
205 // If this name is excluded, skip without descending.
206 if (this.isExcludedFile((0, _nodePath.basename)(path))) {
207 return false;
208 }
209 await this.copyResource((0, _nodePath.join)(destination, path), (0, _nodePath.join)(source, path), opts);
210 return true;
211 });
212 }
213
214 /**
215 * Copy file as resource.
216 *
217 * @param destination Resource destination.
218 * @param source Source file.
219 * @param options Resource options.
220 */
221 async copyResourceFile(destination, source, options = null) {
222 this._assertIsOpen();
223 await this.streamResourceFile(destination, (0, _nodeFs.createReadStream)(source), options ? await this._expandResourceOptionsCopy(options, async () => (0, _promises.stat)(source)) : options);
224 }
225
226 /**
227 * Copy symlink as resource.
228 *
229 * @param destination Resource destination.
230 * @param source Source symlink.
231 * @param options Resource options.
232 */
233 async copyResourceSymlink(destination, source, options = null) {
234 this._assertIsOpen();
235 await this.createResourceSymlink(destination, await (0, _promises.readlink)(source), options ? await this._expandResourceOptionsCopy(options, async () => (0, _promises.lstat)(source)) : options);
236 }
237
238 /**
239 * Create a resource directory.
240 *
241 * @param destination Resource destination.
242 * @param options Resource options.
243 */
244 async createResourceDirectory(destination, options = null) {
245 this._assertIsOpen();
246 const dest = await this._assertNotResourceExists(destination, !!(options && options.merge));
247 await (0, _promises.mkdir)(dest, {
248 recursive: true
249 });
250
251 // If either is set, queue up change times when closing.
252 if (options && (options.atime || options.mtime)) {
253 // Get absolute path, use length for the priority.
254 // Also copy the options object which the owner could change.
255 const abs = (0, _nodePath.resolve)(dest);
256 this._closeQueue.push(this._setResourceAttributes.bind(this, abs, {
257 ...options
258 }), abs.length);
259 }
260 }
261
262 /**
263 * Create a resource file.
264 *
265 * @param destination Resource destination.
266 * @param data Resource data.
267 * @param options Resource options.
268 */
269 async createResourceFile(destination, data, options = null) {
270 this._assertIsOpen();
271 await this.streamResourceFile(destination, new _nodeStream.Readable({
272 /**
273 * Read method.
274 */
275 read() {
276 this.push(data);
277 this.push(null);
278 }
279 }), options);
280 }
281
282 /**
283 * Create a resource symlink.
284 *
285 * @param destination Resource destination.
286 * @param target Symlink target.
287 * @param options Resource options.
288 */
289 async createResourceSymlink(destination, target, options = null) {
290 this._assertIsOpen();
291 const dest = await this._assertNotResourceExists(destination);
292 await (0, _promises.mkdir)((0, _nodePath.dirname)(dest), {
293 recursive: true
294 });
295 const t = typeof target === 'string' ? target : Buffer.from(target.buffer, target.byteOffset, target.byteLength);
296 await (0, _promises.symlink)(t, dest);
297 if (options) {
298 await this._setResourceAttributes(dest, options);
299 }
300 }
301
302 /**
303 * Stream readable source to resource file.
304 *
305 * @param destination Resource destination.
306 * @param data Resource stream.
307 * @param options Resource options.
308 */
309 async streamResourceFile(destination, data, options = null) {
310 this._assertIsOpen();
311 const dest = await this._assertNotResourceExists(destination);
312 await (0, _promises.mkdir)((0, _nodePath.dirname)(dest), {
313 recursive: true
314 });
315 await (0, _promises2.pipeline)(data, (0, _nodeFs.createWriteStream)(dest));
316 if (options) {
317 await this._setResourceAttributes(dest, options);
318 }
319 }
320
321 /**
322 * Check that output path is valid, else throws.
323 */
324 async _checkOutput() {
325 if (this.flat) {
326 const p = (0, _nodePath.dirname)(this.path);
327 if (await (0, _archiveFiles.fsLstatExists)(p)) {
328 for (const n of await (0, _promises.readdir)(p)) {
329 if (!this.isExcludedFile(n)) {
330 throw new Error(`Output path not empty: ${p}`);
331 }
332 }
333 }
334 return;
335 }
336 await Promise.all([this.path, this.resourcePath('')].map(async p => {
337 if (await (0, _archiveFiles.fsLstatExists)(p)) {
338 throw new Error(`Output path already exists: ${p}`);
339 }
340 }));
341 }
342
343 /**
344 * Expand resource options copy properties with stat object from source.
345 *
346 * @param options Options object.
347 * @param stat Stat function.
348 * @returns Options copy with any values populated.
349 */
350 async _expandResourceOptionsCopy(options, stat) {
351 const r = {
352 ...options
353 };
354 let st;
355 if (!r.atime && r.atimeCopy) {
356 st = await stat();
357 r.atime = st.atime;
358 }
359 if (!r.mtime && r.mtimeCopy) {
360 st ??= await stat();
361 r.mtime = st.mtime;
362 }
363 if (typeof r.executable !== 'boolean' && r.executableCopy) {
364 st ??= await stat();
365 r.executable = this._getResourceModeExecutable(st.mode);
366 }
367 return r;
368 }
369
370 /**
371 * Set resource attributes from options object.
372 *
373 * @param path File path.
374 * @param options Options object.
375 */
376 async _setResourceAttributes(path, options) {
377 const {
378 atime,
379 mtime,
380 executable
381 } = options;
382 const st = await (0, _promises.lstat)(path);
383
384 // Maybe set executable if not a directory.
385 if (typeof executable === 'boolean' && !st.isDirectory()) {
386 if (st.isSymbolicLink()) {
387 await (0, _archiveFiles.fsLchmod)(path, this._setResourceModeExecutable(
388 // Workaround for a legacy Node issue.
389 // eslint-disable-next-line no-bitwise
390 st.mode & 0b111111111, executable));
391 } else {
392 await (0, _promises.chmod)(path, this._setResourceModeExecutable(st.mode, executable));
393 }
394 }
395
396 // Maybe change times if either is set.
397 if (atime || mtime) {
398 if (st.isSymbolicLink()) {
399 await (0, _archiveFiles.fsLutimes)(path, atime || st.atime, mtime || st.mtime);
400 } else {
401 await (0, _promises.utimes)(path, atime || st.atime, mtime || st.mtime);
402 }
403 }
404 }
405
406 /**
407 * Get file mode executable.
408 *
409 * @param mode Current mode.
410 * @returns Is executable.
411 */
412 _getResourceModeExecutable(mode) {
413 // eslint-disable-next-line no-bitwise
414 return !!(mode & userExec);
415 }
416
417 /**
418 * Set file mode executable.
419 *
420 * @param mode Current mode.
421 * @param executable Is executable.
422 * @returns File mode.
423 */
424 _setResourceModeExecutable(mode, executable) {
425 // eslint-disable-next-line no-bitwise
426 return (executable ? mode | userExec : mode & ~userExec) >>> 0;
427 }
428
429 /**
430 * Open output.
431 */
432 async _open() {
433 await this.projector.write();
434 }
435
436 /**
437 * Close output.
438 */
439 async _close() {
440 await this._closeQueue.run();
441 }
442
443 /**
444 * Assert bundle is open.
445 */
446 _assertIsOpen() {
447 if (!this._isOpen) {
448 throw new Error('Not open');
449 }
450 }
451
452 /**
453 * Assert resource does not exist, returning destination path.
454 *
455 * @param destination Resource destination.
456 * @param ignoreDirectory Ignore directories.
457 * @returns Destination path.
458 */
459 async _assertNotResourceExists(destination, ignoreDirectory = false) {
460 const dest = this.resourcePath(destination);
461 const st = await (0, _archiveFiles.fsLstatExists)(dest);
462 if (st && (!ignoreDirectory || !st.isDirectory())) {
463 throw new Error(`Resource path exists: ${dest}`);
464 }
465 return dest;
466 }
467
468 /**
469 * Get the projector path.
470 *
471 * @returns This path or the nested path.
472 */
473 _getProjectorPath() {
474 return this.flat ? this.path : this._getProjectorPathNested();
475 }
476
477 /**
478 * Get nested projector path.
479 *
480 * @returns Output path.
481 */
482
483 /**
484 * Create projector instance for the bundle.
485 *
486 * @returns Projector instance.
487 */
488}
489exports.Bundle = Bundle;
490//# sourceMappingURL=bundle.js.map
\No newline at end of file