enigma-bbs/core/archive_util.js

401 lines
12 KiB
JavaScript

/* jslint node: true */
'use strict';
// ENiGMA½
const Config = require('./config.js').get;
const stringFormat = require('./string_format.js');
const Errors = require('./enig_error.js').Errors;
const resolveMimeType = require('./mime_util.js').resolveMimeType;
const Events = require('./events.js');
// base/modules
const fs = require('graceful-fs');
const _ = require('lodash');
const pty = require('node-pty');
const paths = require('path');
let archiveUtil;
class Archiver {
constructor(config) {
this.compress = config.compress;
this.decompress = config.decompress;
this.list = config.list;
this.extract = config.extract;
}
ok() {
return this.canCompress() && this.canDecompress();
}
can(what) {
if (!_.has(this, [what, 'cmd']) || !_.has(this, [what, 'args'])) {
return false;
}
return (
_.isString(this[what].cmd) &&
Array.isArray(this[what].args) &&
this[what].args.length > 0
);
}
canCompress() {
return this.can('compress');
}
canDecompress() {
return this.can('decompress');
}
canList() {
return this.can('list');
} // :TODO: validate entryMatch
canExtract() {
return this.can('extract');
}
}
module.exports = class ArchiveUtil {
constructor() {
this.archivers = {};
this.longestSignature = 0;
}
// singleton access
static getInstance(hotReload = true) {
if (!archiveUtil) {
archiveUtil = new ArchiveUtil();
archiveUtil.init(hotReload);
}
return archiveUtil;
}
init(hotReload = true) {
this.reloadConfig();
if (hotReload) {
Events.on(Events.getSystemEvents().ConfigChanged, () => {
this.reloadConfig();
});
}
}
reloadConfig() {
const config = Config();
if (_.has(config, 'archives.archivers')) {
Object.keys(config.archives.archivers).forEach(archKey => {
const archConfig = config.archives.archivers[archKey];
const archiver = new Archiver(archConfig);
if (!archiver.ok()) {
// :TODO: Log warning - bad archiver/config
}
this.archivers[archKey] = archiver;
});
}
if (_.isObject(config.fileTypes)) {
const updateSig = ft => {
ft.sig = Buffer.from(ft.sig, 'hex');
ft.offset = ft.offset || 0;
// :TODO: this is broken: sig is NOT this long, it's sig.length long; offset needs to allow for -negative values as well
const sigLen = ft.offset + ft.sig.length;
if (sigLen > this.longestSignature) {
this.longestSignature = sigLen;
}
};
Object.keys(config.fileTypes).forEach(mimeType => {
const fileType = config.fileTypes[mimeType];
if (Array.isArray(fileType)) {
fileType.forEach(ft => {
if (ft.sig) {
updateSig(ft);
}
});
} else if (fileType.sig) {
updateSig(fileType);
}
});
}
}
getArchiver(mimeTypeOrExtension, justExtention) {
const mimeType = resolveMimeType(mimeTypeOrExtension);
if (!mimeType) {
// lookup returns false on failure
return;
}
const config = Config();
let fileType = _.get(config, ['fileTypes', mimeType]);
if (Array.isArray(fileType)) {
if (!justExtention) {
// need extention for lookup; ambiguous as-is :(
return;
}
// further refine by extention
fileType = fileType.find(ft => justExtention === ft.ext);
}
if (!_.isObject(fileType)) {
return;
}
if (fileType.archiveHandler) {
return _.get(config, ['archives', 'archivers', fileType.archiveHandler]);
}
}
haveArchiver(archType) {
return this.getArchiver(archType) ? true : false;
}
// :TODO: implement me:
/*
detectTypeWithBuf(buf, cb) {
}
*/
detectType(path, cb) {
const closeFile = fd => {
fs.close(fd, () => {
/* sadface */
});
};
fs.open(path, 'r', (err, fd) => {
if (err) {
return cb(err);
}
const buf = Buffer.alloc(this.longestSignature);
fs.read(fd, buf, 0, buf.length, 0, (err, bytesRead) => {
if (err) {
closeFile(fd);
return cb(err);
}
const archFormat = _.findKey(Config().fileTypes, fileTypeInfo => {
const fileTypeInfos = Array.isArray(fileTypeInfo)
? fileTypeInfo
: [fileTypeInfo];
return fileTypeInfos.find(fti => {
if (!fti.sig || !fti.archiveHandler) {
return false;
}
const lenNeeded = fti.offset + fti.sig.length;
if (bytesRead < lenNeeded) {
return false;
}
const comp = buf.slice(fti.offset, fti.offset + fti.sig.length);
return fti.sig.equals(comp);
});
});
closeFile(fd);
return cb(archFormat ? null : Errors.General('Unknown type'), archFormat);
});
});
}
spawnHandler(proc, action, cb) {
// pty.js doesn't currently give us a error when things fail,
// so we have this horrible, horrible hack:
let err;
proc.once('data', d => {
if (_.isString(d) && d.startsWith('execvp(3) failed.')) {
err = Errors.ExternalProcess(`${action} failed: ${d.trim()}`);
}
});
proc.once('exit', exitCode => {
return cb(
exitCode
? Errors.ExternalProcess(
`${action} failed with exit code: ${exitCode}`
)
: err
);
});
}
compressTo(archType, archivePath, files, workDir, cb) {
const archiver = this.getArchiver(archType, paths.extname(archivePath));
if (!archiver) {
return cb(Errors.Invalid(`Unknown archive type: ${archType}`));
}
if (!cb && _.isFunction(workDir)) {
cb = workDir;
workDir = null;
}
const fmtObj = {
archivePath: archivePath,
fileList: files.join(' '), // :TODO: probably need same hack as extractTo here!
};
// :TODO: DRY with extractTo()
const args = archiver.compress.args.map(arg => {
return '{fileList}' === arg ? arg : stringFormat(arg, fmtObj);
});
const fileListPos = args.indexOf('{fileList}');
if (fileListPos > -1) {
// replace {fileList} with 0:n sep file list arguments
args.splice.apply(args, [fileListPos, 1].concat(files));
}
let proc;
try {
proc = pty.spawn(archiver.compress.cmd, args, this.getPtyOpts(workDir));
} catch (e) {
return cb(
Errors.ExternalProcess(
`Error spawning archiver process "${
archiver.compress.cmd
}" with args "${args.join(' ')}": ${e.message}`
)
);
}
return this.spawnHandler(proc, 'Compression', cb);
}
extractTo(archivePath, extractPath, archType, fileList, cb) {
let haveFileList;
if (!cb && _.isFunction(fileList)) {
cb = fileList;
fileList = [];
haveFileList = false;
} else {
haveFileList = true;
}
const archiver = this.getArchiver(archType, paths.extname(archivePath));
if (!archiver) {
return cb(Errors.Invalid(`Unknown archive type: ${archType}`));
}
const fmtObj = {
archivePath: archivePath,
extractPath: extractPath,
};
let action = haveFileList ? 'extract' : 'decompress';
if ('extract' === action && !_.isObject(archiver[action])) {
// we're forced to do a full decompress
action = 'decompress';
haveFileList = false;
}
// we need to treat {fileList} special in that it should be broken up to 0:n args
const args = archiver[action].args.map(arg => {
return '{fileList}' === arg ? arg : stringFormat(arg, fmtObj);
});
const fileListPos = args.indexOf('{fileList}');
if (fileListPos > -1) {
// replace {fileList} with 0:n sep file list arguments
args.splice.apply(args, [fileListPos, 1].concat(fileList));
}
let proc;
try {
proc = pty.spawn(archiver[action].cmd, args, this.getPtyOpts(extractPath));
} catch (e) {
return cb(
Errors.ExternalProcess(
`Error spawning archiver process "${
archiver[action].cmd
}" with args "${args.join(' ')}": ${e.message}`
)
);
}
return this.spawnHandler(proc, haveFileList ? 'Extraction' : 'Decompression', cb);
}
listEntries(archivePath, archType, cb) {
const archiver = this.getArchiver(archType, paths.extname(archivePath));
if (!archiver) {
return cb(Errors.Invalid(`Unknown archive type: ${archType}`));
}
const fmtObj = {
archivePath: archivePath,
};
const args = archiver.list.args.map(arg => stringFormat(arg, fmtObj));
let proc;
try {
proc = pty.spawn(archiver.list.cmd, args, this.getPtyOpts());
} catch (e) {
return cb(
Errors.ExternalProcess(
`Error spawning archiver process "${
archiver.list.cmd
}" with args "${args.join(' ')}": ${e.message}`
)
);
}
let output = '';
proc.onData(data => {
// :TODO: hack for: execvp(3) failed.: No such file or directory
output += data;
});
proc.once('exit', exitCode => {
if (exitCode) {
return cb(
Errors.ExternalProcess(`List failed with exit code: ${exitCode}`)
);
}
const entryGroupOrder = archiver.list.entryGroupOrder || {
byteSize: 1,
fileName: 2,
};
const entries = [];
const entryMatchRe = new RegExp(archiver.list.entryMatch, 'gm');
let m;
while ((m = entryMatchRe.exec(output))) {
entries.push({
byteSize: parseInt(m[entryGroupOrder.byteSize]),
fileName: m[entryGroupOrder.fileName].trim(),
});
}
return cb(null, entries);
});
}
getPtyOpts(cwd) {
const opts = {
name: 'enigma-archiver',
cols: 80,
rows: 24,
env: process.env,
};
if (cwd) {
opts.cwd = cwd;
}
// :TODO: set cwd to supplied temp path if not sepcific extract
return opts;
}
};