Skip to content

Instantly share code, notes, and snippets.

@JosePedroDias
Last active August 29, 2015 14:01
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save JosePedroDias/ca5c7f8a9049d783a2f4 to your computer and use it in GitHub Desktop.
Save JosePedroDias/ca5c7f8a9049d783a2f4 to your computer and use it in GitHub Desktop.
serve media (static server with ranged request support)
/**
* serves media files in the directory.
* by default won't refresh on file changes, use nodemon if you want that behavior
* 2014, jose.pedro.dias@gmail.com
*/
var http = require('http'),
fs = require('fs'),
util = require('util'),
url = require('url');
var log = function(msg) {
var ts = (new Date()).toISOString().split('T')[1];
console.log([ts, msg].join(': '));
};
var getExt = function(f) {
return f.split('.').pop().toLowerCase();
};
// based on http://stackoverflow.com/questions/20459630/javascript-human-readable-filesize
var humanFileSize = function(size) {
var units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB'];
var i = 0;
while(size >= 1024) {
size /= 1024;
++i;
}
return [size.toFixed(1), units[i]].join(' ');
};
var args = process.argv.slice();
var ip = args.pop();
var config = {
ip: ip,
port: 1337
};
var mimeTypes = {
html: 'text/html',
css: 'text/css',
js: 'text/javascript',
json: 'application/json',
txt: 'text/plain',
mp4: 'video/mp4',
mpg: 'video/mpeg2',
jpg: 'image/jpeg',
jpeg: 'image/jpeg',
png: 'image/png',
gif: 'image/giv',
wav: 'audio/wav',
mp3: 'audio/mpeg',
ogg: 'audio/ogg',
pdf: 'application/pdf',
csv: 'text/csv',
xls: 'application/vnd.ms-excel'
};
var init = function(cb) {
fs.readdir('.', function(err, files1) {
var fileMap = {};
var left = files1.length;
files1.forEach(function(f) {
fs.stat(f, function(f, err, st) {
--left;
if (!err && st.isFile()) {
var ext = getExt(f);
fileMap[f] = {
filename: f,
size: st.size,
mimeType: mimeTypes[ext] || 'text/plain'
};
}
if (left === 0) {
cb(fileMap);
}
}.bind(undefined, f));
});
});
};
var generateIndex = function(fileMap) {
var host = ['http://', config.ip, ':', config.port, '/'].join('');
var keys = Object.keys(fileMap);
keys.sort();
// creating index.html
var listItems = keys.map(function(k) {
var fo = fileMap[k];
return [
'<tr><td>',
'<a href="', host + fo.filename, '">',
fo.filename,
'</td><td>', humanFileSize(fo.size),
'</td><td>', fo.mimeType,
'</td></tr>'
].join('');
});
var markup = [
'<!DOCTYPE html>',
'<html>',
' <head>',
' <meta charset="utf-8">',
' <title>served files</title>',
' <style type="text/css">',
' body { font-family: sans-serif; }',
' table { border-collapse: collapse; border: 1px solid #000; font-size: 0.9em; }',
' th, td { padding: 0.45em; text-align: left; }',
' th { background-color: #000; color: #FFF; }',
' tr:nth-child(odd) { background-color: #DDD; }',
' th:nth-child(3n-1), td:nth-child(3n-1) { text-align: right; }',
' </style>',
' </head>',
' <body>',
' <table>',
' <tr><th>filename</th><th>size</th><th>mime type</th></tr>',
' ' + listItems.join('\n '),
' </table>',
' </body>',
'</html>'
].join('\n');
var markupSize = markup.length;
var fo = {
filename: 'index.html',
mimeType: 'text/html',
size: markupSize,
content: markup
};
fileMap[''] = fo;
fileMap['index.html'] = fo;
};
init(function(fileMap) {
generateIndex(fileMap);
http.createServer(function(req, res) {
var uri = url.parse(req.url).pathname;
uri = decodeURIComponent(uri);
var path = uri.substring(1);
var fo = fileMap[path];
if (fo === undefined) { // not found
res.writeHead(404);
log([uri, 404].join(' '));
return res.end();
}
var total = fo.size;
var mimeType = fo.mimeType;
if (fo.content) { // from memory
res.writeHead(200, {
'Content-Length': total,
'Content-Type': mimeType
});
log([uri, 200, mimeType, 'from memory'].join(' '));
return res.end(fo.content);
}
if (req.headers.range) {
var range = req.headers.range;
var parts = range.replace(/bytes=/, '').split('-');
var partialStart = parts[0];
var partialEnd = parts[1];
var start = parseInt(partialStart, 10);
var end = partialEnd ? parseInt(partialEnd, 10) : total-1;
var chunkSize = (end - start) + 1;
var fStream = fs.createReadStream(path, {
start: start,
end: end
});
var rangeS = ['bytes ', start, '-', end, '/', total].join('');
res.writeHead(206, { // ranged download
'Content-Range': rangeS,
'Accept-Ranges': 'bytes',
'Content-Length': chunkSize,
'Content-Type': mimeType
});
log([uri, 206, mimeType, rangeS].join(' '));
fStream.pipe(res);
}
else {
res.writeHead(200, { // regular download
'Content-Length': total,
'Content-Type': mimeType
});
log([uri, 200, mimeType, 'all'].join(' '));
fs.createReadStream(path).pipe(res);
}
}).listen(config.port);
console.log('Media server running at http://' + config.ip + ':' + config.port + '/ ...\n');
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment