2016-02-24 04:56:22 +00:00
|
|
|
/* jslint node: true */
|
|
|
|
'use strict';
|
|
|
|
|
|
|
|
// ENiGMA½
|
2016-09-05 03:36:26 +00:00
|
|
|
const Config = require('./config.js').config;
|
|
|
|
const stringFormat = require('./string_format.js');
|
2016-10-06 05:22:59 +00:00
|
|
|
const Errors = require('./enig_error.js').Errors;
|
2016-02-24 04:56:22 +00:00
|
|
|
|
|
|
|
// base/modules
|
2016-08-06 22:30:56 +00:00
|
|
|
const fs = require('fs');
|
|
|
|
const _ = require('lodash');
|
|
|
|
const pty = require('ptyw.js');
|
2016-02-24 04:56:22 +00:00
|
|
|
|
2016-10-03 03:39:29 +00:00
|
|
|
let archiveUtil;
|
|
|
|
|
|
|
|
class Archiver {
|
|
|
|
constructor(config) {
|
|
|
|
this.compress = config.compress;
|
|
|
|
this.decompress = config.decompress;
|
|
|
|
this.list = config.list;
|
|
|
|
this.extract = config.extract;
|
|
|
|
|
2016-10-06 05:22:59 +00:00
|
|
|
/*this.sig = new Buffer(config.sig, 'hex');
|
|
|
|
this.offset = config.offset || 0;*/
|
2016-10-03 03:39:29 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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'); }
|
2016-10-03 04:21:37 +00:00
|
|
|
canList() { return this.can('list'); } // :TODO: validate entryMatch
|
2016-10-03 03:39:29 +00:00
|
|
|
canExtract() { return this.can('extract'); }
|
|
|
|
}
|
|
|
|
|
2016-02-24 04:56:22 +00:00
|
|
|
module.exports = class ArchiveUtil {
|
|
|
|
|
|
|
|
constructor() {
|
|
|
|
this.archivers = {};
|
|
|
|
this.longestSignature = 0;
|
|
|
|
}
|
|
|
|
|
2016-10-03 03:39:29 +00:00
|
|
|
// singleton access
|
|
|
|
static getInstance() {
|
|
|
|
if(!archiveUtil) {
|
|
|
|
archiveUtil = new ArchiveUtil();
|
|
|
|
archiveUtil.init();
|
|
|
|
}
|
2016-10-03 04:21:37 +00:00
|
|
|
return archiveUtil;
|
2016-10-03 03:39:29 +00:00
|
|
|
}
|
|
|
|
|
2016-02-24 04:56:22 +00:00
|
|
|
init() {
|
|
|
|
//
|
|
|
|
// Load configuration
|
|
|
|
//
|
2016-10-06 05:22:59 +00:00
|
|
|
if(_.has(Config, 'archives.archivers')) {
|
|
|
|
Object.keys(Config.archives.archivers).forEach(archKey => {
|
2016-10-03 03:39:29 +00:00
|
|
|
|
2016-10-06 05:22:59 +00:00
|
|
|
const archConfig = Config.archives.archivers[archKey];
|
2016-10-03 03:39:29 +00:00
|
|
|
const archiver = new Archiver(archConfig);
|
|
|
|
|
|
|
|
if(!archiver.ok()) {
|
|
|
|
// :TODO: Log warning - bad archiver/config
|
2016-02-24 04:56:22 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
this.archivers[archKey] = archiver;
|
2016-10-06 05:22:59 +00:00
|
|
|
});
|
|
|
|
}
|
2016-10-03 03:39:29 +00:00
|
|
|
|
2016-10-06 05:22:59 +00:00
|
|
|
if(_.has(Config, 'archives.formats')) {
|
|
|
|
Object.keys(Config.archives.formats).forEach(fmtKey => {
|
|
|
|
|
|
|
|
Config.archives.formats[fmtKey].sig = new Buffer(Config.archives.formats[fmtKey].sig, 'hex');
|
|
|
|
Config.archives.formats[fmtKey].offset = Config.archives.formats[fmtKey].offset || 0;
|
|
|
|
|
|
|
|
const sigLen = Config.archives.formats[fmtKey].offset + Config.archives.formats[fmtKey].sig.length;
|
|
|
|
if(sigLen > this.longestSignature) {
|
|
|
|
this.longestSignature = sigLen;
|
|
|
|
}
|
2016-02-24 04:56:22 +00:00
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
2016-02-29 05:04:03 +00:00
|
|
|
|
2016-10-06 05:22:59 +00:00
|
|
|
/*
|
2016-03-04 05:54:32 +00:00
|
|
|
getArchiver(archType) {
|
2016-10-06 05:22:59 +00:00
|
|
|
if(!archType || 0 === archType.length) {
|
2016-03-04 05:54:32 +00:00
|
|
|
return;
|
2016-02-29 05:04:03 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
archType = archType.toLowerCase();
|
2016-03-04 05:54:32 +00:00
|
|
|
return this.archivers[archType];
|
2016-10-06 05:22:59 +00:00
|
|
|
}*/
|
|
|
|
|
|
|
|
getArchiver(archType) {
|
|
|
|
if(!archType || 0 === archType.length) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if(_.has(Config, [ 'archives', 'formats', archType, 'handler' ] ) &&
|
|
|
|
_.has(Config, [ 'archives', 'archivers', Config.archives.formats[archType].handler ] ))
|
|
|
|
{
|
|
|
|
return Config.archives.archivers[ Config.archives.formats[archType].handler ];
|
|
|
|
}
|
2016-03-04 05:54:32 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
haveArchiver(archType) {
|
|
|
|
return this.getArchiver(archType) ? true : false;
|
2016-02-29 05:04:03 +00:00
|
|
|
}
|
2016-02-24 04:56:22 +00:00
|
|
|
|
2016-10-03 03:39:29 +00:00
|
|
|
detectTypeWithBuf(buf, cb) {
|
|
|
|
// :TODO: implement me!
|
|
|
|
}
|
|
|
|
|
2016-02-24 04:56:22 +00:00
|
|
|
detectType(path, cb) {
|
2016-10-06 05:22:59 +00:00
|
|
|
if(!_.has(Config, 'archives.formats')) {
|
|
|
|
return cb(Errors.DoesNotExist('No formats configured'));
|
|
|
|
}
|
|
|
|
|
2016-02-24 04:56:22 +00:00
|
|
|
fs.open(path, 'r', (err, fd) => {
|
|
|
|
if(err) {
|
2016-10-06 05:22:59 +00:00
|
|
|
return cb(err);
|
2016-02-24 04:56:22 +00:00
|
|
|
}
|
|
|
|
|
2016-10-06 05:22:59 +00:00
|
|
|
const buf = new Buffer(this.longestSignature);
|
2016-02-24 04:56:22 +00:00
|
|
|
fs.read(fd, buf, 0, buf.length, 0, (err, bytesRead) => {
|
|
|
|
if(err) {
|
2016-08-06 22:30:56 +00:00
|
|
|
return cb(err);
|
2016-02-24 04:56:22 +00:00
|
|
|
}
|
|
|
|
|
2016-10-06 05:22:59 +00:00
|
|
|
const archFormat = _.findKey(Config.archives.formats, archFormat => {
|
|
|
|
const lenNeeded = archFormat.offset + archFormat.sig.length;
|
|
|
|
|
2016-08-06 22:30:56 +00:00
|
|
|
if(bytesRead < lenNeeded) {
|
2016-02-24 04:56:22 +00:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2016-10-06 05:22:59 +00:00
|
|
|
const comp = buf.slice(archFormat.offset, archFormat.offset + archFormat.sig.length);
|
|
|
|
return (archFormat.sig.equals(comp));
|
2016-02-24 04:56:22 +00:00
|
|
|
});
|
|
|
|
|
2016-10-06 05:22:59 +00:00
|
|
|
return cb(archFormat ? null : Errors.General('Unknown type'), archFormat);
|
2016-02-24 04:56:22 +00:00
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2016-10-03 04:21:37 +00:00
|
|
|
spawnHandler(proc, action, cb) {
|
2016-08-06 22:30:56 +00:00
|
|
|
// pty.js doesn't currently give us a error when things fail,
|
|
|
|
// so we have this horrible, horrible hack:
|
|
|
|
let err;
|
2016-10-03 04:21:37 +00:00
|
|
|
proc.once('data', d => {
|
2016-08-06 22:30:56 +00:00
|
|
|
if(_.isString(d) && d.startsWith('execvp(3) failed.: No such file or directory')) {
|
|
|
|
err = new Error(`${action} failed: ${d.trim()}`);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2016-10-03 04:21:37 +00:00
|
|
|
proc.once('exit', exitCode => {
|
2016-08-06 22:30:56 +00:00
|
|
|
if(exitCode) {
|
|
|
|
return cb(new Error(`${action} failed with exit code: ${exitCode}`));
|
|
|
|
}
|
|
|
|
if(err) {
|
|
|
|
return cb(err);
|
|
|
|
}
|
|
|
|
return cb(null);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2016-02-24 04:56:22 +00:00
|
|
|
compressTo(archType, archivePath, files, cb) {
|
2016-03-04 05:54:32 +00:00
|
|
|
const archiver = this.getArchiver(archType);
|
2016-02-29 05:04:03 +00:00
|
|
|
|
2016-02-24 04:56:22 +00:00
|
|
|
if(!archiver) {
|
2016-03-04 05:54:32 +00:00
|
|
|
return cb(new Error(`Unknown archive type: ${archType}`));
|
2016-02-24 04:56:22 +00:00
|
|
|
}
|
|
|
|
|
2016-10-03 03:39:29 +00:00
|
|
|
const fmtObj = {
|
|
|
|
archivePath : archivePath,
|
2016-10-06 05:22:59 +00:00
|
|
|
fileList : files.join(' '), // :TODO: probably need same hack as extractTo here!
|
2016-10-03 03:39:29 +00:00
|
|
|
};
|
2016-02-24 04:56:22 +00:00
|
|
|
|
2016-10-03 03:39:29 +00:00
|
|
|
const args = archiver.compress.args.map( arg => stringFormat(arg, fmtObj) );
|
2016-10-03 04:21:37 +00:00
|
|
|
const proc = pty.spawn(archiver.compress.cmd, args, this.getPtyOpts());
|
2016-02-24 04:56:22 +00:00
|
|
|
|
2016-10-03 04:21:37 +00:00
|
|
|
return this.spawnHandler(proc, 'Compression', cb);
|
2016-02-24 04:56:22 +00:00
|
|
|
}
|
|
|
|
|
2016-10-06 05:22:59 +00:00
|
|
|
extractTo(archivePath, extractPath, archType, fileList, cb) {
|
|
|
|
let haveFileList;
|
|
|
|
|
|
|
|
if(!cb && _.isFunction(fileList)) {
|
|
|
|
cb = fileList;
|
|
|
|
fileList = [];
|
|
|
|
haveFileList = false;
|
|
|
|
} else {
|
|
|
|
haveFileList = true;
|
|
|
|
}
|
|
|
|
|
2016-03-04 05:54:32 +00:00
|
|
|
const archiver = this.getArchiver(archType);
|
|
|
|
|
|
|
|
if(!archiver) {
|
|
|
|
return cb(new Error(`Unknown archive type: ${archType}`));
|
|
|
|
}
|
2016-10-03 03:39:29 +00:00
|
|
|
|
|
|
|
const fmtObj = {
|
|
|
|
archivePath : archivePath,
|
|
|
|
extractPath : extractPath,
|
|
|
|
};
|
|
|
|
|
2016-10-06 05:22:59 +00:00
|
|
|
const action = haveFileList ? 'extract' : 'decompress';
|
|
|
|
|
|
|
|
// 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));
|
|
|
|
}
|
|
|
|
|
|
|
|
const proc = pty.spawn(archiver[action].cmd, args, this.getPtyOpts());
|
2016-08-06 22:30:56 +00:00
|
|
|
|
2016-10-06 05:22:59 +00:00
|
|
|
return this.spawnHandler(proc, (haveFileList ? 'Extraction' : 'Decompression'), cb);
|
2016-10-03 04:21:37 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
listEntries(archivePath, archType, cb) {
|
|
|
|
const archiver = this.getArchiver(archType);
|
|
|
|
|
|
|
|
if(!archiver) {
|
|
|
|
return cb(new Error(`Unknown archive type: ${archType}`));
|
|
|
|
}
|
|
|
|
|
|
|
|
const fmtObj = {
|
|
|
|
archivePath : archivePath,
|
|
|
|
};
|
|
|
|
|
|
|
|
const args = archiver.list.args.map( arg => stringFormat(arg, fmtObj) );
|
|
|
|
const proc = pty.spawn(archiver.list.cmd, args, this.getPtyOpts());
|
|
|
|
|
|
|
|
let output = '';
|
|
|
|
proc.on('data', data => {
|
|
|
|
// :TODO: hack for: execvp(3) failed.: No such file or directory
|
|
|
|
|
|
|
|
output += data;
|
|
|
|
});
|
|
|
|
|
|
|
|
proc.once('exit', exitCode => {
|
|
|
|
if(exitCode) {
|
|
|
|
return cb(new Error(`List failed with exit code: ${exitCode}`));
|
|
|
|
}
|
|
|
|
//if(err) {
|
|
|
|
// return cb(err);
|
|
|
|
// }
|
|
|
|
|
|
|
|
const entries = [];
|
2016-10-04 04:03:32 +00:00
|
|
|
const entryMatchRe = new RegExp(archiver.list.entryMatch, 'gm');
|
2016-10-03 04:21:37 +00:00
|
|
|
let m;
|
2016-10-04 04:03:32 +00:00
|
|
|
while((m = entryMatchRe.exec(output))) {
|
2016-10-03 04:21:37 +00:00
|
|
|
// :TODO: allow alternate ordering!!!
|
|
|
|
entries.push({
|
2016-10-13 04:07:22 +00:00
|
|
|
byteSize : parseInt(m[1]),
|
2016-10-03 04:21:37 +00:00
|
|
|
fileName : m[2],
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
return cb(null, entries);
|
|
|
|
});
|
2016-03-04 05:54:32 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
getPtyOpts() {
|
|
|
|
return {
|
|
|
|
// :TODO: cwd
|
|
|
|
name : 'enigma-archiver',
|
|
|
|
cols : 80,
|
|
|
|
rows : 24,
|
|
|
|
env : process.env,
|
|
|
|
};
|
2016-02-24 04:56:22 +00:00
|
|
|
}
|
2016-08-06 22:30:56 +00:00
|
|
|
};
|