@shockpkg/dir-projector
Version:
Package for creating Shockwave Director projectors
540 lines (431 loc) • 13 kB
JavaScript
"use strict";
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.Bundle = void 0;
var _stream = require("stream");
var _path = require("path");
var _util = require("util");
var _fsExtra = _interopRequireDefault(require("fs-extra"));
var _archiveFiles = require("@shockpkg/archive-files");
var _util2 = require("./util");
var _queue = require("./queue");
const pipelineP = (0, _util.promisify)(_stream.pipeline);
const userExec = 0b001000000;
/**
* Options for adding resources.
*/
/**
* Bundle constructor.
*
* @param path Output path for the main executable.
*/
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.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 _fsExtra.default.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 _fsExtra.default.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 (0, _path.join)((0, _path.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 !!(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 _fsExtra.default.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 () => _fsExtra.default.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, _path.basename)(path))) {
return false;
}
await this.copyResource((0, _path.join)(destination, path), (0, _path.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, _fsExtra.default.createReadStream(source), options ? await this._expandResourceOptionsCopy(options, async () => _fsExtra.default.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 _fsExtra.default.readlink(source), options ? await this._expandResourceOptionsCopy(options, async () => _fsExtra.default.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 _fsExtra.default.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 = (0, _path.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 _stream.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 _fsExtra.default.ensureDir((0, _path.dirname)(dest));
await _fsExtra.default.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 _fsExtra.default.ensureDir((0, _path.dirname)(dest));
await pipelineP(data, _fsExtra.default.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 _fsExtra.default.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 = (0, _util2.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 _fsExtra.default.lstat(path); // Maybe set executable if not a directory and supported.
if (typeof executable === 'boolean' && !st.isDirectory()) {
if (!st.isSymbolicLink()) {
await _fsExtra.default.chmod(path, this._setResourceModeExecutable(st.mode, executable));
} else if (_archiveFiles.fsLchmodSupported) {
await (0, _archiveFiles.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 _fsExtra.default.utimes(path, atime || st.atime, mtime || st.mtime);
} else if (_archiveFiles.fsLutimesSupported) {
await (0, _archiveFiles.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 (0, _archiveFiles.fsLstatExists)(dest);
if (st && (!ignoreDirectory || !st.isDirectory())) {
throw new Error(`Resource path exists: ${dest}`);
}
return dest;
}
/**
* Main application file extension.
*
* @returns File extension.
*/
}
exports.Bundle = Bundle;
//# sourceMappingURL=bundle.js.map