Forgetting to call stream.pause
and releasing the tick lets data spill out
between your request callback and the next function.
To try it out:
npm install
rm v3.1.1.zip
node .
unzip -t v3.1.1.zip
Follow the sorry saga in the commits.
node_modules | |
*.zip |
{ | |
"node": true, | |
"browser": true, | |
"esnext": true, | |
"bitwise": true, | |
"camelcase": true, | |
"curly": true, | |
"eqeqeq": true, | |
"immed": true, | |
"indent": 4, | |
"latedef": true, | |
"newcap": true, | |
"noarg": true, | |
"quotmark": "single", | |
"regexp": true, | |
"undef": true, | |
"unused": true, | |
"strict": true, | |
"trailing": true, | |
"smarttabs": true, | |
"globals": { | |
"angular": false | |
} | |
} |
'use strict'; | |
var assert = require('assert'), | |
path = require('path'), | |
http = require('http'), | |
https = require('https'), | |
fs = require('fs'), | |
url = require('url'); | |
function fetch(uri, callback) { | |
'fetch a file'; | |
console.error('requesting', uri); | |
var modules = { | |
'http:': http, | |
'https:': https | |
}, | |
module = modules[url.parse(uri).protocol]; | |
module.get(uri, function (res) { | |
console.error('headers:', res.headers); | |
switch (res.statusCode) { | |
case 200: | |
return callback(null, res); | |
case 301: | |
case 302: | |
res.socket.end(); | |
return fetch(res.headers.location, callback); | |
default: | |
return callback(new Error(res.statusCode)); | |
} | |
}) | |
.on('error', callback); | |
} | |
function writeResults(destFile, res, callback) { | |
'write response text to a file'; | |
assert(!res.buffered); | |
console.error('streaming to', destFile); | |
var stream = fs.createWriteStream(destFile), | |
size = 0; | |
// you can tell I'm getting annoyed | |
function onError(err) { | |
console.error('ERROR', err); | |
callback(err); | |
} | |
res.on('error', onError); | |
res.on('data', function(chunk) { | |
size += chunk.length; | |
}); | |
stream.on('error', onError); | |
stream.on('close', function() { // also fails for finish | |
callback(null, size); | |
}); | |
res.pipe(stream); | |
res.resume(); | |
} | |
function download(details, callback) { | |
'download a file if it is absent or empty, making its parent directory.'; | |
function afterWriting(err, size) { | |
if (err) { | |
return callback(err); | |
} else if (size === details.size || !details.size) { | |
return callback(null); | |
} else { | |
return callback(new Error('unexpected size: ' + size)); | |
} | |
} | |
function afterRequesting(err, res) { | |
if (err) { | |
return callback(err); | |
} else { | |
res.pause(); // we don't want to miss writes | |
return setImmediate(writeResults, details.as, res, afterWriting); | |
} | |
} | |
fs.existsSync(details.as) && fs.truncateSync(details.as, 0); | |
fetch(details.url, afterRequesting); | |
} | |
module.exports = download; | |
if (!module.parent) { | |
download({ | |
url: 'https://github.com/twbs/bootstrap/archive/v3.1.1.zip', | |
as: path.join(__dirname, 'v3.1.1.zip'), | |
size: 2357836 | |
}, console.error); | |
} |
{ | |
"dependencies": {}, | |
"version": "0.4.0" | |
} |