Permalink
Browse files

Initial commit

  • Loading branch information...
Carlos Rodriguez
Carlos Rodriguez committed Jul 19, 2012
1 parent 7f4e06a commit 2a684541939f4321388abbf333d414ed9958d285
Showing with 481 additions and 0 deletions.
  1. +1 −0 .gitignore
  2. +7 −0 Makefile
  3. +66 −0 cache.js
  4. +149 −0 gzip.js
  5. +46 −0 index.js
  6. +21 −0 package.json
  7. +1 −0 test/files/hello.txt
  8. +149 −0 test/gzip.js
  9. +41 −0 test/simple.js
View
@@ -0,0 +1 @@
+node_modules
View
@@ -0,0 +1,7 @@
+REPORTER = spec
+
+test:
+ @NODE_ENV=test ./node_modules/.bin/mocha \
+ --reporter $(REPORTER)
+
+.PHONY: test
View
@@ -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
@@ -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
@@ -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
@@ -0,0 +1 @@
+hello world!
Oops, something went wrong.

0 comments on commit 2a68454

Please sign in to comment.