@shockpkg/dir-projector
Version:
Package for creating Shockwave Director projectors
522 lines (424 loc) • 12.4 kB
JavaScript
import { Readable, pipeline } from 'stream';
import { join as pathJoin, dirname, basename, resolve } from 'path';
import { promisify } from 'util';
import fse from 'fs-extra';
import { fsLchmodSupported, fsLchmod, fsLutimesSupported, fsLutimes, fsWalk, fsLstatExists } from "@shockpkg/archive-files/module.mjs";
import { once } from "./util.mjs";
import { Queue } from "./queue.mjs";
const pipelineP = promisify(pipeline);
const userExec = 0b001000000;
/**
* Options for adding resources.
*/
/**
* Bundle constructor.
*
* @param path Output path for the main executable.
*/
export class Bundle extends Object {
/**
* File and directory names to exclude when adding a directory.
*/
/**
* Bundle main executable path.
*/
/**
* Projector instance.
*/
/**
* Open flag.
*/
/**
* Close callbacks priority queue.
*/
constructor(path) {
super();
this.excludes = [/^\./, /^ehthumbs\.db$/, /^Thumbs\.db$/];
this.path = void 0;
this.projector = void 0;
this._isOpen = false;
this._closeQueue = new Queue();
this.path = path;
}
/**
* 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 with file.
*
* @param player Player path.
* @param configFile Config file.
*/
async openFile(player, configFile) {
const configData = configFile ? await fse.readFile(configFile) : null;
await this.openData(player, configData);
}
/**
* Open output with data.
*
* @param player Player path.
* @param configData Config data.
*/
async openData(player, configData) {
if (this._isOpen) {
throw new Error('Already open');
}
await this._checkOutput();
this._closeQueue.clear();
await this._openData(player, configData);
this._isOpen = true;
}
/**
* Close output.
*/
async close() {
this._assertIsOpen();
try {
await this._close();
} finally {
this._closeQueue.clear();
}
this._isOpen = false;
}
/**
* Write out projector with player and file.
* Has a callback to write out the resources.
*
* @param player Player path.
* @param configFile Config file.
* @param func Async function.
* @returns Return value of the async function.
*/
async withFile(player, configFile, func = null) {
const configData = configFile ? await fse.readFile(configFile) : null;
return this.withData(player, configData, func);
}
/**
* Write out projector with player and data.
* Has a callback to write out the resources.
*
* @param player Player path.
* @param configData Config data.
* @param func Async function.
* @returns Return value of the async function.
*/
async withData(player, configData, func = null) {
await this.openData(player, configData);
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 pathJoin(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 !!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 fse.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 () => fse.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 fsWalk(source, async (path, stat) => {
// If this name is excluded, skip without descending.
if (this.isExcludedFile(basename(path))) {
return false;
}
await this.copyResource(pathJoin(destination, path), pathJoin(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, fse.createReadStream(source), options ? await this._expandResourceOptionsCopy(options, async () => fse.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 fse.readlink(source), options ? await this._expandResourceOptionsCopy(options, async () => fse.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 fse.ensureDir(dest); // 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 = 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 Readable({
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 fse.ensureDir(dirname(dest));
await fse.symlink(target, 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 fse.ensureDir(dirname(dest));
await pipelineP(data, fse.createWriteStream(dest));
if (options) {
await this._setResourceAttributes(dest, options);
}
}
/**
* Check that output path is valid, else throws.
*/
async _checkOutput() {
for (const p of [this.path, this.resourcePath('')]) {
// eslint-disable-next-line no-await-in-loop
if (await fse.pathExists(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
};
const st = once(stat);
if (!r.atime && r.atimeCopy) {
r.atime = (await st()).atime;
}
if (!r.mtime && r.mtimeCopy) {
r.mtime = (await st()).mtime;
}
if (typeof r.executable !== 'boolean' && r.executableCopy) {
r.executable = this._getResourceModeExecutable((await 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 fse.lstat(path); // Maybe set executable if not a directory and supported.
if (typeof executable === 'boolean' && !st.isDirectory()) {
if (!st.isSymbolicLink()) {
await fse.chmod(path, this._setResourceModeExecutable(st.mode, executable));
} else if (fsLchmodSupported) {
await fsLchmod(path, this._setResourceModeExecutable( // Workaround for a legacy Node issue.
// eslint-disable-next-line no-bitwise
st.mode & 0b111111111, executable));
}
} // Maybe change times if either is set and supported.
if (atime || mtime) {
if (!st.isSymbolicLink()) {
await fse.utimes(path, atime || st.atime, mtime || st.mtime);
} else if (fsLutimesSupported) {
await fsLutimes(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 with data.
*
* @param player Player path.
* @param configData Config data.
*/
async _openData(player, configData) {
await this.projector.withData(player, configData);
}
/**
* Close output.
*/
async _close() {
await this._writeLauncher();
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 fsLstatExists(dest);
if (st && (!ignoreDirectory || !st.isDirectory())) {
throw new Error(`Resource path exists: ${dest}`);
}
return dest;
}
/**
* Main application file extension.
*
* @returns File extension.
*/
}
//# sourceMappingURL=bundle.mjs.map