fast-extract
Version:
Extract contents from various archive types (tar, tar.bz2, tar.gz, tar.xz, tgz, zip)
106 lines (105 loc) • 3.79 kB
JavaScript
import callOnce from 'call-once-fn';
import oo from 'on-one';
import writer from './compat/flush-write-stream.js';
import createPipeline from './createPipeline.js';
import exitCleanup from './exitCleanup.js';
export default function createWriteStream(dest, options_) {
if (typeof options_ === 'string') options_ = {
type: options_
};
const options = {
...options_
};
const streams = createPipeline(dest, options);
exitCleanup.add(dest);
// Get first and last streams
const first = streams[0];
const last = streams[streams.length - 1];
let error = null;
let ended = false;
let lastFinished = false;
let finishCallback = null;
let errorEmittedOnWrite = false;
// Manually pipe streams (instead of using pump which has complex completion semantics)
for(let i = 0; i < streams.length - 1; i++){
streams[i].pipe(streams[i + 1]);
}
// Handle errors from all streams - use on-one to ensure each stream only triggers once
let errorHandling = false;
function handleError(err) {
if (!err || errorHandling) return; // only handle actual errors, once
errorHandling = true;
error = err;
// Emit error immediately to prevent 'finish' from being emitted (Node 12 timing issue)
if (!errorEmittedOnWrite) {
errorEmittedOnWrite = true;
write.destroy(err);
}
// Note: Don't clean up dest on error - if user retries with force, safeRm will handle it.
// Background cleanup here causes race conditions where new files get deleted.
exitCleanup.remove(dest);
}
// Listen for errors on all streams (errors may not propagate through all pipe types)
for(let i = 0; i < streams.length; i++){
oo(streams[i], [
'error'
], handleError);
}
// Track when last stream finishes (use on-one for cross-version compatibility)
oo(last, [
'end',
'close',
'finish'
], ()=>{
if (error) return; // don't complete if errored
lastFinished = true;
if (finishCallback) {
finishCallback();
finishCallback = null;
}
});
function onEnd(callback) {
if (error || ended) return callback();
ended = true;
exitCleanup.remove(dest);
callback();
}
const write = writer(function write(chunk, encoding, callback) {
if (error) return callback(error);
first.write(chunk, encoding, (err)=>{
if (error) return; // skip if errored
if (err) {
error = err;
errorEmittedOnWrite = true; // error will be emitted by Writable base class via callback
callback(err);
} else {
callback();
}
});
}, function flush(callback) {
if (error) {
errorEmittedOnWrite = true; // error will be emitted by Writable base class via callback
return callback(error);
}
// Ensure callback is only called once (race conditions on older Node)
const cb = callOnce(callback);
const onComplete = ()=>{
if (error) return cb(error);
onEnd(cb);
};
// If last already finished, complete immediately
if (lastFinished) {
onComplete();
} else {
// Wait for last stream to finish
finishCallback = onComplete;
// End the first stream to signal no more data - this propagates through the pipeline
first.end();
}
});
// Track when error is emitted on write stream (from any source)
write.on('error', ()=>{
errorEmittedOnWrite = true;
});
return write;
}