@shockpkg/dir-projector
Version:
Package for creating Shockwave Director projectors
493 lines (451 loc) • 12.6 kB
JavaScript
"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