node-exiftool
Version:
A Node.js interface to exiftool command-line application.
228 lines (204 loc) • 6.71 kB
JavaScript
const cp = require('child_process')
const EOL = require('os').EOL
const isStream = require('is-stream')
function writeStdIn(proc, data, encoding) {
// console.log('write stdin', data)
proc.stdin.write(data, encoding)
proc.stdin.write(EOL, encoding)
}
function close(proc) {
let errHandler
return new Promise((resolve, reject) => {
errHandler = (err) => {
reject(new Error(`Could not write to stdin: ${err.message}`))
}
proc.once('close', resolve)
proc.stdin.once('error', errHandler)
writeStdIn(proc, '-stay_open')
writeStdIn(proc, 'false')
})
.then(() => {
proc.stdin.removeListener('error', errHandler)
})
}
function isString(s) {
return (typeof s).toLowerCase() === 'string'
}
function isObject(o) {
return (typeof o).toLowerCase() === 'object' && o !== null
}
/**
* Get arguments. Split by new line to write to exiftool
*/
function getArgs(args, noSplit) {
if(!(Array.isArray(args) && args.length)) {
return []
}
return args
.filter(isString)
.map(arg => `-${arg}`)
.reduce((acc, arg) =>
[].concat(acc, noSplit ? [arg] : arg.split(/\s+/))
, [])
}
/**
* Write command data to the exiftool's stdin.
* @param {ChildProcess} process - exiftool process executed with -stay_open True -@ -
* @param {string} command - which command to execute
* @param {string} commandNumber - text which will be echoed before and after results
* @param {string[]} args - any additional arguments
* @param {string[]} noSplitArgs - arguments which should not be broken up like args
* @param {string} encoding - which encoding to write in. default no encoding
*/
function execute(proc, command, commandNumber, args, noSplitArgs, encoding) {
const extendedArgs = getArgs(args)
const extendedArgsNoSplit = getArgs(noSplitArgs, true)
command = command !== undefined ? command : ''
const allArgs = [].concat(
extendedArgsNoSplit,
extendedArgs,
['-json', '-s'],
[
command,
'-echo1',
`{begin${commandNumber}}`,
'-echo2',
`{begin${commandNumber}}`,
'-echo4',
`{ready${commandNumber}}`,
`-execute${commandNumber}`,
]
)
if (process.env.DEBUG) {
console.log(JSON.stringify(allArgs, null, 2))
}
allArgs.forEach(arg => writeStdIn(proc, arg, encoding))
}
let currentCommand = 0
function genCommandNumber() {
return String(++currentCommand)
}
function executeCommand(proc, stdoutRws, stderrRws, command, args, noSplitArgs, encoding) {
const commandNumber = genCommandNumber()
if (proc === process) { // debugging
execute(proc, command, commandNumber, args, noSplitArgs, encoding)
return Promise.resolve({ data: 'debug', error: null })
}
let dataFinishHandler
let errFinishHandler
let dataErr
let errErr
const dataPromise = new Promise((resolve, reject) => {
dataFinishHandler = () => {
reject(new Error('stdout stream finished before operation was complete'))
}
stdoutRws.once('finish', dataFinishHandler)
stdoutRws.addToResolveMap(commandNumber, resolve)
}).catch(error => { dataErr = error })
const errPromise = new Promise((resolve, reject) => {
errFinishHandler = () => {
reject(new Error('stderr stream finished before operation was complete'))
}
stderrRws.once('finish', errFinishHandler)
stderrRws.addToResolveMap(commandNumber, resolve)
}).catch(error => { errErr = error })
execute(proc, command, commandNumber, args, noSplitArgs, encoding)
return Promise.all([
dataPromise,
errPromise,
])
.then((res) => {
stderrRws.removeListener('finish', errFinishHandler)
stdoutRws.removeListener('finish', dataFinishHandler)
if (dataErr && !errErr) {
throw dataErr
} else if (errErr && !dataErr) {
throw errErr
} else if (dataErr && errErr) {
throw new Error('stdout and stderr finished before operation was complete')
}
return {
data: res[0] ? JSON.parse(res[0]) : null,
error: res[1] || null,
}
})
}
function isReadable(stream) {
return isStream.readable(stream)
}
function isWritable(stream) {
return isStream.writable(stream)
}
/**
* Spawn exiftool.
* @param {string} bin Path to the binary
* @param {object} [options] options to pass to child_process.spawn method
* @returns {Promise.<ChildProcess>} A promise resolved with the process pointer, or rejected on error.
*/
function spawn(bin, options) {
const echoString = Date.now().toString()
const proc = cp.spawn(bin, ['-echo2', echoString, '-stay_open', 'True', '-@', '-'], options)
if (!isReadable(proc.stderr)) {
killProcess(proc)
return Promise.reject(new Error('Process was not spawned with a readable stderr, check stdio options.'))
}
return new Promise((resolve, reject) => {
const echoHandler = (data) => {
const d = data.toString().trim()
// listening for echo2 in stderr (echo and echo1 won't work)
if (d === echoString) {
resolve(proc)
} else {
reject(new Error(`Unexpected string on start: ${d}`))
}
}
proc.stderr.once('data', echoHandler)
proc.once('error', reject)
})
}
function checkDataObject(data) {
return data === Object(data) && !Array.isArray(data)
}
function mapDataToTagArray(data, array) {
const res = Array.isArray(array) ? array : []
Object
.keys(data)
.forEach(tag => {
const value = data[tag]
if (Array.isArray(value)) {
value.forEach((v) => {
const arg = `${tag}=${v}`
res.push(arg)
})
} else {
res.push(`${tag}=${value}`)
}
})
return res
}
/**
* Use process.kill on POSIX or terminate process with taskkill on Windows.
* @param {ChildProcess} proc Process to terminate
*/
function killProcess(proc) {
if (process.platform === 'win32') {
cp.exec(`taskkill /t /F /PID ${proc.pid}`)
} else {
proc.kill()
}
}
module.exports = {
spawn,
close,
executeCommand,
checkDataObject,
mapDataToTagArray,
getArgs,
execute,
isString,
isObject,
isReadable,
isWritable,
killProcess,
}