UNPKG

@shockpkg/dir-projector

Version:

Package for creating Shockwave Director projectors

493 lines (451 loc) 12.6 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.Bundle = void 0; var _nodeFs = require("node:fs"); var _promises = require("node:fs/promises"); var _nodeStream = require("node:stream"); var _promises2 = require("node:stream/promises"); var _nodePath = require("node:path"); var _archiveFiles = require("@shockpkg/archive-files"); var _queue = require("./queue.js"); const userExec = 0b001000000; /** * Options for adding resources. */ /** * Bundle object. */ class Bundle { /** * File and directory names to exclude when adding a directory. */ // eslint-disable-next-line unicorn/better-regex excludes = [/^\./, /^ehthumbs\.db$/i, /^Thumbs\.db$/i]; /** * Bundle main executable path. */ /** * Flat bundle. */ /** * Projector instance. */ /** * Open flag. */ _isOpen = false; /** * Close callbacks priority queue. */ _closeQueue = new _queue.Queue(); /** * Bundle constructor. * * @param path Output path for the main executable. * @param flat Flat bundle. */ constructor(path, flat = false) { this.path = path; this.flat = flat; } /** * Check if output open. * * @returns Returns true if open, else false. */ get isOpen() { return this._isOpen; } /** * Check if name is excluded file. * * @param name File name. * @returns Returns true if excluded, else false. */ isExcludedFile(name) { for (const exclude of this.excludes) { if (exclude.test(name)) { return true; } } return false; } /** * Open output. */ async open() { if (this._isOpen) { throw new Error('Already open'); } await this._checkOutput(); this._closeQueue.clear(); await this._open(); this._isOpen = true; } /** * Close output. */ async close() { this._assertIsOpen(); try { await this._close(); } finally { this._closeQueue.clear(); } this._isOpen = false; } /** * Write out the bundle. * Has a callback to write out the resources. * * @param func Async function. * @returns Return value of the async function. */ async write(func = null) { await this.open(); try { return func ? await func.call(this, this) : null; } finally { await this.close(); } } /** * Get path for resource. * * @param destination Resource destination. * @returns Destination path. */ resourcePath(destination) { return (0, _nodePath.join)((0, _nodePath.dirname)(this.projector.path), destination); } /** * Check if path for resource exists. * * @param destination Resource destination. * @returns True if destination exists, else false. */ async resourceExists(destination) { return !!(await (0, _archiveFiles.fsLstatExists)(this.resourcePath(destination))); } /** * Copy resource, detecting source type automatically. * * @param destination Resource destination. * @param source Source directory. * @param options Resource options. */ async copyResource(destination, source, options = null) { this._assertIsOpen(); const stat = await (0, _promises.lstat)(source); switch (true) { case stat.isSymbolicLink(): { await this.copyResourceSymlink(destination, source, options); break; } case stat.isFile(): { await this.copyResourceFile(destination, source, options); break; } case stat.isDirectory(): { await this.copyResourceDirectory(destination, source, options); break; } default: { throw new Error(`Unsupported resource type: ${source}`); } } } /** * Copy directory as resource, recursive copy. * * @param destination Resource destination. * @param source Source directory. * @param options Resource options. */ async copyResourceDirectory(destination, source, options = null) { this._assertIsOpen(); // Create directory. await this.createResourceDirectory(destination, options ? await this._expandResourceOptionsCopy(options, async () => (0, _promises.stat)(source)) : options); // If not recursive do not walk contents. if (options && options.noRecurse) { return; } // Any directories we add should not be recursive. const opts = { ...options, noRecurse: true }; await (0, _archiveFiles.fsWalk)(source, async (path, stat) => { // If this name is excluded, skip without descending. if (this.isExcludedFile((0, _nodePath.basename)(path))) { return false; } await this.copyResource((0, _nodePath.join)(destination, path), (0, _nodePath.join)(source, path), opts); return true; }); } /** * Copy file as resource. * * @param destination Resource destination. * @param source Source file. * @param options Resource options. */ async copyResourceFile(destination, source, options = null) { this._assertIsOpen(); await this.streamResourceFile(destination, (0, _nodeFs.createReadStream)(source), options ? await this._expandResourceOptionsCopy(options, async () => (0, _promises.stat)(source)) : options); } /** * Copy symlink as resource. * * @param destination Resource destination. * @param source Source symlink. * @param options Resource options. */ async copyResourceSymlink(destination, source, options = null) { this._assertIsOpen(); await this.createResourceSymlink(destination, await (0, _promises.readlink)(source), options ? await this._expandResourceOptionsCopy(options, async () => (0, _promises.lstat)(source)) : options); } /** * Create a resource directory. * * @param destination Resource destination. * @param options Resource options. */ async createResourceDirectory(destination, options = null) { this._assertIsOpen(); const dest = await this._assertNotResourceExists(destination, !!(options && options.merge)); await (0, _promises.mkdir)(dest, { recursive: true }); // If either is set, queue up change times when closing. if (options && (options.atime || options.mtime)) { // Get absolute path, use length for the priority. // Also copy the options object which the owner could change. const abs = (0, _nodePath.resolve)(dest); this._closeQueue.push(this._setResourceAttributes.bind(this, abs, { ...options }), abs.length); } } /** * Create a resource file. * * @param destination Resource destination. * @param data Resource data. * @param options Resource options. */ async createResourceFile(destination, data, options = null) { this._assertIsOpen(); await this.streamResourceFile(destination, new _nodeStream.Readable({ /** * Read method. */ read() { this.push(data); this.push(null); } }), options); } /** * Create a resource symlink. * * @param destination Resource destination. * @param target Symlink target. * @param options Resource options. */ async createResourceSymlink(destination, target, options = null) { this._assertIsOpen(); const dest = await this._assertNotResourceExists(destination); await (0, _promises.mkdir)((0, _nodePath.dirname)(dest), { recursive: true }); const t = typeof target === 'string' ? target : Buffer.from(target.buffer, target.byteOffset, target.byteLength); await (0, _promises.symlink)(t, dest); if (options) { await this._setResourceAttributes(dest, options); } } /** * Stream readable source to resource file. * * @param destination Resource destination. * @param data Resource stream. * @param options Resource options. */ async streamResourceFile(destination, data, options = null) { this._assertIsOpen(); const dest = await this._assertNotResourceExists(destination); await (0, _promises.mkdir)((0, _nodePath.dirname)(dest), { recursive: true }); await (0, _promises2.pipeline)(data, (0, _nodeFs.createWriteStream)(dest)); if (options) { await this._setResourceAttributes(dest, options); } } /** * Check that output path is valid, else throws. */ async _checkOutput() { if (this.flat) { const p = (0, _nodePath.dirname)(this.path); if (await (0, _archiveFiles.fsLstatExists)(p)) { for (const n of await (0, _promises.readdir)(p)) { if (!this.isExcludedFile(n)) { throw new Error(`Output path not empty: ${p}`); } } } return; } await Promise.all([this.path, this.resourcePath('')].map(async p => { if (await (0, _archiveFiles.fsLstatExists)(p)) { throw new Error(`Output path already exists: ${p}`); } })); } /** * Expand resource options copy properties with stat object from source. * * @param options Options object. * @param stat Stat function. * @returns Options copy with any values populated. */ async _expandResourceOptionsCopy(options, stat) { const r = { ...options }; let st; if (!r.atime && r.atimeCopy) { st = await stat(); r.atime = st.atime; } if (!r.mtime && r.mtimeCopy) { st ??= await stat(); r.mtime = st.mtime; } if (typeof r.executable !== 'boolean' && r.executableCopy) { st ??= await stat(); r.executable = this._getResourceModeExecutable(st.mode); } return r; } /** * Set resource attributes from options object. * * @param path File path. * @param options Options object. */ async _setResourceAttributes(path, options) { const { atime, mtime, executable } = options; const st = await (0, _promises.lstat)(path); // Maybe set executable if not a directory. if (typeof executable === 'boolean' && !st.isDirectory()) { // eslint-disable-next-line unicorn/prefer-ternary if (st.isSymbolicLink()) { await (0, _archiveFiles.fsLchmod)(path, this._setResourceModeExecutable( // Workaround for a legacy Node issue. // eslint-disable-next-line no-bitwise st.mode & 0b111111111, executable)); } else { await (0, _promises.chmod)(path, this._setResourceModeExecutable(st.mode, executable)); } } // Maybe change times if either is set. if (atime || mtime) { // eslint-disable-next-line unicorn/prefer-ternary if (st.isSymbolicLink()) { await (0, _archiveFiles.fsLutimes)(path, atime || st.atime, mtime || st.mtime); } else { await (0, _promises.utimes)(path, atime || st.atime, mtime || st.mtime); } } } /** * Get file mode executable. * * @param mode Current mode. * @returns Is executable. */ _getResourceModeExecutable(mode) { // eslint-disable-next-line no-bitwise return !!(mode & userExec); } /** * Set file mode executable. * * @param mode Current mode. * @param executable Is executable. * @returns File mode. */ _setResourceModeExecutable(mode, executable) { // eslint-disable-next-line no-bitwise return (executable ? mode | userExec : mode & ~userExec) >>> 0; } /** * Open output. */ async _open() { await this.projector.write(); } /** * Close output. */ async _close() { await this._closeQueue.run(); } /** * Assert bundle is open. */ _assertIsOpen() { if (!this._isOpen) { throw new Error('Not open'); } } /** * Assert resource does not exist, returning destination path. * * @param destination Resource destination. * @param ignoreDirectory Ignore directories. * @returns Destination path. */ async _assertNotResourceExists(destination, ignoreDirectory = false) { const dest = this.resourcePath(destination); const st = await (0, _archiveFiles.fsLstatExists)(dest); if (st && (!ignoreDirectory || !st.isDirectory())) { throw new Error(`Resource path exists: ${dest}`); } return dest; } /** * Get the projector path. * * @returns This path or the nested path. */ _getProjectorPath() { return this.flat ? this.path : this._getProjectorPathNested(); } /** * Get nested projector path. * * @returns Output path. */ /** * Create projector instance for the bundle. * * @returns Projector instance. */ } exports.Bundle = Bundle; //# sourceMappingURL=bundle.js.map