Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Initial commit

  • Loading branch information...
commit 2a684541939f4321388abbf333d414ed9958d285 1 parent 7f4e06a
@carlos8f authored
View
1  .gitignore
@@ -0,0 +1 @@
+node_modules
View
7 Makefile
@@ -0,0 +1,7 @@
+REPORTER = spec
+
+test:
+ @NODE_ENV=test ./node_modules/.bin/mocha \
+ --reporter $(REPORTER)
+
+.PHONY: test
View
66 cache.js
@@ -0,0 +1,66 @@
+var fs = require('fs')
+ , Stream = require('stream').Stream
+ , inherits = require('util').inherits
+ , mime = require('mime')
+ , hash = require('node_hash')
+ , gzip = require('./gzip').gzip
+ , sync = require('sync')
+ ;
+
+function copy(orig) {
+ var n = {};
+ if (orig) {
+ Object.keys(orig).forEach(function(k) {
+ n[k] = orig[k];
+ });
+ }
+ return n;
+}
+
+function Cache(file, options) {
+ this.options = copy(options);
+ this.file = file;
+ this.buf = fs.readFileSync(file);
+ this.gzip();
+ this.buildHeaders();
+}
+
+Cache.prototype.buildHeaders = function() {
+ var stats = fs.statSync(this.file);
+ this.headers = {};
+ this.headers['Content-Type'] = mime.lookup(this.file);
+ var charset = mime.charsets.lookup(this.file);
+ if (charset) {
+ this.headers['Content-Type'] += '; charset=' + charset;
+ }
+ this.headers['Last-Modified'] = stats.mtime.toUTCString();
+ this.headers['ETag'] = hash.sha1(this.buf.toString());
+ this.headers['Content-Length'] = stats.size;
+ this.headers['Vary'] = 'Accept-Encoding';
+ if (this.options.maxAge) {
+ this.headers['Cache-Control'] = 'public, max-age: ' + this.options.maxAge;
+ }
+ this.headers['Connection'] = 'close';
+};
+
+Cache.prototype.gzip = function() {
+ var self = this;
+ sync(function() {
+ self.gzipped = gzip.sync(null, self.buf, {});
+ self.gzippedLength = Buffer.byteLength(self.gzipped.toString());
+ });
+};
+
+Cache.prototype.stream = function(req, res) {
+ var bufProp = 'buf', headers = copy(this.headers);
+ if (req.headers['accept-encoding'] && /gzip/i.match(req.headers['accept-encoding'])) {
+ bufProp = 'gzipped';
+ headers['Content-Encoding'] = 'gzip';
+ headers['Content-Length'] = this.gzippedLength;
+ }
+ headers['Date'] = new Date().toUTCString();
+ res.writeHead(200, headers);
+ res.end(this[bufProp]);
+};
+
+module.exports = Cache;
View
149 gzip.js
@@ -0,0 +1,149 @@
+
+/*!
+ * gzip-buffer
+ * Copyright(c) 2011 Russell Bradberry <rbradberry@gmail.com>
+ * MIT Licensed
+ */
+var zlib = require('zlib'),
+ Stream = require('stream').Stream,
+ util = require('util');
+
+/**
+ * Collects a stream into a buffer and when the stream ends it emits the collected buffer
+ * @constructor
+ * @private
+ */
+var StreamCollector = function(){
+ this.writable = true;
+ this._data = new Buffer(0);
+};
+util.inherits(StreamCollector, Stream);
+
+/**
+ * Writes data to the buffer
+ * @param {Object} chunk The chunk to buffer
+ */
+StreamCollector.prototype.write = function(chunk){
+ if (chunk !== undefined){
+ if (!Buffer.isBuffer(chunk)){
+ chunk = new Buffer(chunk);
+ }
+
+ var newBuffer = new Buffer(chunk.length + this._data.length);
+
+ this._data.copy(newBuffer);
+ chunk.copy(newBuffer, this._data.length);
+
+ this._data = newBuffer;
+ }
+};
+
+/**
+ * Ends the stream writing the final chunk
+ * @param {Object} chunk The chunk to buffer
+ */
+StreamCollector.prototype.end = function(chunk){
+ this.write(chunk);
+ this.emit('end', this._data);
+};
+
+/**
+ * Creates the StreamCollector, zips or unzips it and calls back with the data
+ * @param {Object} data The data to zip or unzip
+ * @param {Object} gz The zipping mechanism
+ * @param {Function} callback The callback to call once the stream has finished
+ * @private
+ */
+function process(data, gz, options, callback){
+ var stream = new StreamCollector();
+
+ if (typeof options === 'function'){
+ callback = options;
+ options = null;
+ }
+
+ callback = callback || function(){};
+ gz = gz(options);
+
+ stream.on('end', function(data){
+ callback(null, data);
+ });
+
+ gz.pipe(stream);
+ gz.end(data);
+}
+
+/**
+ * Compresses data using deflate and calls back with the compressed data
+ * @param {Object} data The data to compress
+ * @param {Object} options Options to pass to the zlib class
+ * @param {Function} callback The callback function that returns the data
+ */
+exports.deflate = function(data, options, callback){
+ process(data, zlib.createDeflate, options, callback);
+};
+
+/**
+ * Uncompresses data using inflate and calls back with the compressed data
+ * @param {Object} data The data to uncompress
+ * @param {Object} options Options to pass to the zlib class
+ * @param {Function} callback The callback function that returns the data
+ */
+exports.inflate = function(data, options, callback){
+ process(data, zlib.createInflate, options, callback);
+};
+
+/**
+ * Compresses data using deflateRaw and calls back with the compressed data
+ * @param {Object} data The data to compress
+ * @param {Object} options Options to pass to the zlib class
+ * @param {Function} callback The callback function that returns the data
+ */
+exports.deflateRaw = function(data, options, callback){
+ process(data, zlib.createDeflateRaw, options, callback);
+};
+
+/**
+ * Uncompresses data using inflateRaw and calls back with the compressed data
+ * @param {Object} data The data to uncompress
+ * @param {Object} options Options to pass to the zlib class
+ * @param {Function} callback The callback function that returns the data
+ */
+exports.inflateRaw = function(data, options, callback){
+ process(data, zlib.createInflateRaw, options, callback);
+};
+
+/**
+ * Compresses data using gzip and calls back with the compressed data
+ * @param {Object} data The data to compress
+ * @param {Object} options Options to pass to the zlib class
+ * @param {Function} callback The callback function that returns the data
+ */
+exports.gzip = function(data, options, callback){
+ process(data, zlib.createGzip, options, callback);
+};
+
+/**
+ * Uncompresses data using gunzip and calls back with the compressed data
+ * @param {Object} data The data to uncompress
+ * @param {Object} options Options to pass to the zlib class
+ * @param {Function} callback The callback function that returns the data
+ */
+exports.gunzip = function(data, options, callback){
+ process(data, zlib.createGunzip, options, callback);
+};
+
+/**
+ * Uncompresses data using the header of the data and calls back with the compressed data
+ * @param {Object} data The data to uncompress
+ * @param {Object} options Options to pass to the zlib class
+ * @param {Function} callback The callback function that returns the data
+ */
+exports.unzip = function(data, options, callback){
+ process(data, zlib.createUnzip, options, callback);
+};
+
+/**
+ * Library version.
+ */
+exports.version = '0.0.2';
View
46 index.js
@@ -0,0 +1,46 @@
+var fs = require('fs')
+ , url = require('url')
+ , mime = require('mime')
+ , join = require('path').join
+ , dive = require('diveSync')
+ , path = require('path')
+ , Cache = require('./cache')
+ ;
+
+var stalwart = module.exports = function(root, options) {
+ var cache = {}, parsed = {};
+
+ root = path.resolve(root);
+
+ dive(root, function(err, file) {
+ if (err) throw err;
+
+ var urlPath = file.substring(root.length);
+ cache[urlPath] = new Cache(file, options);
+ });
+
+ return function stalwartHandler(req, res, next) {
+ if (req.method !== 'GET' && req.method !== 'HEAD') {
+ return next();
+ }
+
+ var urlPath = parsed[req.url] ? parsed[req.url] : url.parse(req.url).pathname;
+
+ // Decode and strip nullbytes for security
+ urlPath = decodeURIComponent(urlPath).replace(/\0/g, '');
+
+ parsed[req.url] = urlPath;
+
+ // Don't support paths outside the root
+ if (urlPath.indexOf('..') !== -1) {
+ return next();
+ }
+
+ var cached = cache[urlPath];
+ if (!cached) {
+ return next();
+ }
+
+ cached.stream(req, res);
+ };
+};
View
21 package.json
@@ -0,0 +1,21 @@
+{
+ "author": "Carlos Rodriguez <carlos@s8f.org> (http://s8f.org/)",
+ "name": "stalwart",
+ "description": "Stubborn file streamer for Node.js",
+ "version": "0.0.0",
+ "repository": {
+ "type": "git",
+ "url": "git://github.com/carlos8f/node-stalwart.git"
+ },
+ "dependencies": {
+ "mime": "~1.2.6",
+ "mocha": "~1.3.0",
+ "diveSync": "~0.2.0",
+ "sync": "~v0.2.1"
+ },
+ "devDependencies": {},
+ "optionalDependencies": {},
+ "engines": {
+ "node": "*"
+ }
+}
View
1  test/files/hello.txt
@@ -0,0 +1 @@
+hello world!
View
149 test/gzip.js
@@ -0,0 +1,149 @@
+
+/*!
+ * gzip-buffer
+ * Copyright(c) 2011 Russell Bradberry <rbradberry@gmail.com>
+ * MIT Licensed
+ */
+var zlib = require('zlib'),
+ Stream = require('stream').Stream,
+ util = require('util');
+
+/**
+ * Collects a stream into a buffer and when the stream ends it emits the collected buffer
+ * @constructor
+ * @private
+ */
+var StreamCollector = function(){
+ this.writable = true;
+ this._data = new Buffer(0);
+};
+util.inherits(StreamCollector, Stream);
+
+/**
+ * Writes data to the buffer
+ * @param {Object} chunk The chunk to buffer
+ */
+StreamCollector.prototype.write = function(chunk){
+ if (chunk !== undefined){
+ if (!Buffer.isBuffer(chunk)){
+ chunk = new Buffer(chunk);
+ }
+
+ var newBuffer = new Buffer(chunk.length + this._data.length);
+
+ this._data.copy(newBuffer);
+ chunk.copy(newBuffer, this._data.length);
+
+ this._data = newBuffer;
+ }
+};
+
+/**
+ * Ends the stream writing the final chunk
+ * @param {Object} chunk The chunk to buffer
+ */
+StreamCollector.prototype.end = function(chunk){
+ this.write(chunk);
+ this.emit('end', this._data);
+};
+
+/**
+ * Creates the StreamCollector, zips or unzips it and calls back with the data
+ * @param {Object} data The data to zip or unzip
+ * @param {Object} gz The zipping mechanism
+ * @param {Function} callback The callback to call once the stream has finished
+ * @private
+ */
+function process(data, gz, options, callback){
+ var stream = new StreamCollector();
+
+ if (typeof options === 'function'){
+ callback = options;
+ options = null;
+ }
+
+ callback = callback || function(){};
+ gz = gz(options);
+
+ stream.on('end', function(data){
+ callback(null, data);
+ });
+
+ gz.pipe(stream);
+ gz.end(data);
+}
+
+/**
+ * Compresses data using deflate and calls back with the compressed data
+ * @param {Object} data The data to compress
+ * @param {Object} options Options to pass to the zlib class
+ * @param {Function} callback The callback function that returns the data
+ */
+exports.deflate = function(data, options, callback){
+ process(data, zlib.createDeflate, options, callback);
+};
+
+/**
+ * Uncompresses data using inflate and calls back with the compressed data
+ * @param {Object} data The data to uncompress
+ * @param {Object} options Options to pass to the zlib class
+ * @param {Function} callback The callback function that returns the data
+ */
+exports.inflate = function(data, options, callback){
+ process(data, zlib.createInflate, options, callback);
+};
+
+/**
+ * Compresses data using deflateRaw and calls back with the compressed data
+ * @param {Object} data The data to compress
+ * @param {Object} options Options to pass to the zlib class
+ * @param {Function} callback The callback function that returns the data
+ */
+exports.deflateRaw = function(data, options, callback){
+ process(data, zlib.createDeflateRaw, options, callback);
+};
+
+/**
+ * Uncompresses data using inflateRaw and calls back with the compressed data
+ * @param {Object} data The data to uncompress
+ * @param {Object} options Options to pass to the zlib class
+ * @param {Function} callback The callback function that returns the data
+ */
+exports.inflateRaw = function(data, options, callback){
+ process(data, zlib.createInflateRaw, options, callback);
+};
+
+/**
+ * Compresses data using gzip and calls back with the compressed data
+ * @param {Object} data The data to compress
+ * @param {Object} options Options to pass to the zlib class
+ * @param {Function} callback The callback function that returns the data
+ */
+exports.gzip = function(data, options, callback){
+ process(data, zlib.createGzip, options, callback);
+};
+
+/**
+ * Uncompresses data using gunzip and calls back with the compressed data
+ * @param {Object} data The data to uncompress
+ * @param {Object} options Options to pass to the zlib class
+ * @param {Function} callback The callback function that returns the data
+ */
+exports.gunzip = function(data, options, callback){
+ process(data, zlib.createGunzip, options, callback);
+};
+
+/**
+ * Uncompresses data using the header of the data and calls back with the compressed data
+ * @param {Object} data The data to uncompress
+ * @param {Object} options Options to pass to the zlib class
+ * @param {Function} callback The callback function that returns the data
+ */
+exports.unzip = function(data, options, callback){
+ process(data, zlib.createUnzip, options, callback);
+};
+
+/**
+ * Library version.
+ */
+exports.version = '0.0.2';
View
41 test/simple.js
@@ -0,0 +1,41 @@
+var stalwart = require('../')
+ , http = require('http')
+ , assert = require('assert')
+ , port = 33333
+ ;
+
+describe('simple test', function() {
+ before(function(done) {
+ var stalwartHandler = stalwart('test/files');
+ http.createServer(function(req, res) {
+ stalwartHandler(req, res, function() {
+ res.writeHead(404, {'Content-Type': 'text/plain'});
+ res.write('file not found');
+ res.end();
+ });
+ }).listen(port, done);
+ });
+
+ it('can serve a txt file', function(done) {
+ var req = http.get('http://localhost:' + port + '/hello.txt', function(res) {
+ assert.equal(res.headers['content-type'], 'text/plain');
+ assert.ok(res.headers['last-modified']);
+ assert.ok(res.headers['etag']);
+ assert.equal(res.headers['vary'], 'Accept-Encoding');
+ assert.ok(res.headers['date']);
+ assert.equal(res.statusCode, 200);
+ var data = '';
+ res.setEncoding('utf8');
+ res.on('data', function(chunk) {
+ data += chunk;
+ });
+ res.on('end', function() {
+ assert.equal(data, 'hello world!');
+ done();
+ });
+ }).on('error', function(err) {
+ console.error(err, 'error');
+ });
+ req.end();
+ });
+});
Please sign in to comment.
Something went wrong with that request. Please try again.