mirror of
https://github.com/bodyrep/DemoApp.git
synced 2025-12-06 06:01:48 +00:00
290 lines
8.7 KiB
JavaScript
290 lines
8.7 KiB
JavaScript
/*!
|
|
* Tom Gallacher
|
|
*
|
|
* MIT Licensed
|
|
*/
|
|
|
|
/**
|
|
* Module dependencies.
|
|
*/
|
|
|
|
// Commented out as I think that connect is avalible from within express...
|
|
// try {
|
|
// var staticMiddleware = require('connect').static;
|
|
|
|
// } catch (e) {
|
|
// staticMiddleware = require('express').static;
|
|
// }
|
|
|
|
var fs = require('fs'),
|
|
parse = require('url').parse,
|
|
path = require('path'),
|
|
zlib = require('zlib'),
|
|
MemoryStore = require('./memory'),
|
|
StoreStream = require('./storeStream'),
|
|
FileAsset = require('./fileAsset'),
|
|
send = require('send'),
|
|
mime = send.mime
|
|
;
|
|
|
|
/**
|
|
* Strip `Content-*` headers from `res`.
|
|
*
|
|
* @param {ServerResponse} res
|
|
* @api public
|
|
*/
|
|
|
|
var removeContentHeaders = function(res){
|
|
Object.keys(res._headers).forEach(function(field){
|
|
if (0 === field.indexOf('content')) {
|
|
res.removeHeader(field);
|
|
}
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Supported content-encoding methods.
|
|
*/
|
|
|
|
var methods = {
|
|
gzip: zlib.createGzip,
|
|
deflate: zlib.createDeflate
|
|
};
|
|
|
|
/**
|
|
* Default filter function.
|
|
*/
|
|
|
|
exports.filter = function(req, res){
|
|
var type = res.getHeader('Content-Type') || '';
|
|
return type.match(/json|text|javascript/);
|
|
};
|
|
|
|
/**
|
|
* Parse the `req` url with memoization.
|
|
*
|
|
* @param {ServerRequest} req
|
|
* @return {Object}
|
|
* @api private
|
|
*/
|
|
|
|
var parseUrl = function(req){
|
|
var parsed = req._parsedUrl;
|
|
if (parsed && parsed.href == req.url) {
|
|
return parsed;
|
|
} else {
|
|
return req._parsedUrl = parse(req.url);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* By default gzip's static's that match the given regular expression /text|javascript|json/
|
|
* and then serves them with Connects static provider, denoted by the given `dirPath`.
|
|
*
|
|
* Options:
|
|
*
|
|
* - `maxAge` how long gzippo should cache gziped assets, defaulting to 1 day
|
|
* - `clientMaxAge` client cache-control max-age directive, defaulting to 0; 604800000 is one week.
|
|
* - `contentTypeMatch` - A regular expression tested against the Content-Type header to determine whether the response
|
|
* should be gzipped or not. The default value is `/text|javascript|json/`.
|
|
* - `prefix` - A url prefix. If you want all your static content in a root path such as /resource/. Any url paths not matching will be ignored
|
|
*
|
|
* Examples:
|
|
*
|
|
* connect.createServer(
|
|
* connect.staticGzip(__dirname + '/public/');
|
|
* );
|
|
*
|
|
* connect.createServer(
|
|
* connect.staticGzip(__dirname + '/public/', {maxAge: 86400000});
|
|
* );
|
|
*
|
|
* @param {String} path
|
|
* @param {Object} options
|
|
* @return {Function}
|
|
* @api public
|
|
*/
|
|
|
|
exports = module.exports = function staticGzip(dirPath, options){
|
|
options = options || {};
|
|
|
|
var maxAge = options.maxAge || 86400000,
|
|
contentTypeMatch = options.contentTypeMatch || /text|javascript|json/,
|
|
clientMaxAge = options.clientMaxAge || 604800000,
|
|
prefix = options.prefix || '',
|
|
names = Object.keys(methods),
|
|
compressionOptions = options.compression || {},
|
|
store = options.store || new MemoryStore();
|
|
|
|
if (!dirPath) throw new Error('You need to provide the directory to your static content.');
|
|
if (!contentTypeMatch.test) throw new Error('contentTypeMatch: must be a regular expression.');
|
|
|
|
dirPath = path.normalize(dirPath);
|
|
|
|
return function(req, res, next) {
|
|
var acceptEncoding = req.headers['accept-encoding'] || '',
|
|
url,
|
|
filename,
|
|
contentType,
|
|
charset,
|
|
method;
|
|
|
|
function pass(name) {
|
|
send(req, url.substring(prefix.length))
|
|
.maxage(clientMaxAge || 0)
|
|
.root(dirPath)
|
|
.pipe(res)
|
|
;
|
|
}
|
|
|
|
function setHeaders(stat, asset) {
|
|
res.setHeader('Content-Type', contentType);
|
|
res.setHeader('Content-Encoding', method);
|
|
res.setHeader('Vary', 'Accept-Encoding');
|
|
// if cache version is avalible then add this.
|
|
if (asset) {
|
|
// res.setHeader('Content-Length', asset.length);
|
|
res.setHeader('ETag', '"' + asset.length + '-' + Number(asset.mtime) + '"');
|
|
res.setHeader('Last-Modified', asset.mtime.toUTCString());
|
|
}
|
|
res.setHeader('Date', new Date().toUTCString());
|
|
res.setHeader('Expires', new Date(Date.now() + clientMaxAge).toUTCString());
|
|
res.setHeader('Cache-Control', 'public, max-age=' + (clientMaxAge / 1000));
|
|
}
|
|
|
|
// function gzipAndSend(filename, gzipName, mtime) {
|
|
// gzippo(filename, charset, function(gzippedData) {
|
|
// gzippoCache[gzipName] = {
|
|
// 'ctime': Date.now(),
|
|
// 'mtime': mtime,
|
|
// 'content': gzippedData
|
|
// };
|
|
// sendGzipped(gzippoCache[gzipName]);
|
|
// });
|
|
// }
|
|
|
|
function forbidden(res) {
|
|
var body = 'Forbidden';
|
|
res.setHeader('Content-Type', 'text/plain');
|
|
res.setHeader('Content-Length', body.length);
|
|
res.statusCode = 403;
|
|
res.end(body);
|
|
}
|
|
|
|
if (req.method !== 'GET' && req.method !== 'HEAD') {
|
|
return next();
|
|
}
|
|
|
|
url = decodeURI(parseUrl(req).pathname);
|
|
|
|
// Allow a url path prefix
|
|
if (url.substring(0, prefix.length) !== prefix) {
|
|
return next();
|
|
}
|
|
|
|
filename = path.normalize(path.join(dirPath, url.substring(prefix.length)));
|
|
// malicious path
|
|
if (0 != filename.indexOf(dirPath)){
|
|
return forbidden(res);
|
|
}
|
|
|
|
// directory index file support
|
|
if (filename.substr(-1) === '/') filename += 'index.html';
|
|
|
|
|
|
contentType = mime.lookup(filename);
|
|
charset = mime.charsets.lookup(contentType, 'UTF-8');
|
|
contentType = contentType + (charset ? '; charset=' + charset : '');
|
|
|
|
// default to gzip
|
|
if ('*' == acceptEncoding.trim()) method = 'gzip';
|
|
|
|
// compression method
|
|
if (!method) {
|
|
for (var i = 0, len = names.length; i < len; ++i) {
|
|
if (~acceptEncoding.indexOf(names[i])) {
|
|
method = names[i];
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!method) return pass(filename);
|
|
|
|
fs.stat(filename, function(err, stat) {
|
|
|
|
if (err) {
|
|
return next();
|
|
}
|
|
|
|
if (stat.isDirectory()) {
|
|
return next();
|
|
}
|
|
|
|
if (!contentTypeMatch.test(contentType)) {
|
|
return pass(filename);
|
|
}
|
|
|
|
// superceeded by if (!method) return;
|
|
// if (!~acceptEncoding.indexOf('gzip')) {
|
|
// return pass(filename);
|
|
// }
|
|
|
|
var base = path.basename(filename),
|
|
dir = path.dirname(filename),
|
|
gzipName = path.join(dir, base + '.gz');
|
|
|
|
var sendGzipped = function(filename) {
|
|
var stream = fs.createReadStream(filename);
|
|
|
|
req.on('close', stream.destroy.bind(stream));
|
|
|
|
var storeStream = new StoreStream(store, filename, {
|
|
mtime: stat.mtime,
|
|
maxAge: options.maxAge
|
|
});
|
|
|
|
var compressionStream = methods[method](options.compression);
|
|
|
|
stream.pipe(compressionStream).pipe(storeStream).pipe(res);
|
|
|
|
stream.on('error', function(err){
|
|
if (res.headerSent) {
|
|
console.error(err.stack);
|
|
req.destroy();
|
|
} else {
|
|
next(err);
|
|
}
|
|
});
|
|
};
|
|
|
|
store.get(decodeURI(filename), function(err, asset) {
|
|
setHeaders(stat, asset);
|
|
if (err) {
|
|
// handle error
|
|
|
|
} else if (!asset) {
|
|
sendGzipped(decodeURI(filename));
|
|
} else if ((asset.mtime < stat.mtime) || asset.isExpired) {
|
|
sendGzipped(decodeURI(filename));
|
|
}
|
|
else if (req.headers['if-modified-since'] && asset &&
|
|
// Optimisation: new Date().getTime is 90% faster that Date.parse()
|
|
+stat.mtime <= new Date(req.headers['if-modified-since']).getTime()) {
|
|
removeContentHeaders(res);
|
|
res.statusCode = 304;
|
|
return res.end();
|
|
}
|
|
else {
|
|
// StoreReadStream to pipe to res.
|
|
// console.log("hit: " + filename + " length: " + asset.length);
|
|
for (var i = 0; i < asset.content.length; i++) {
|
|
res.write(asset.content[i], 'binary');
|
|
}
|
|
res.end();
|
|
}
|
|
});
|
|
});
|
|
};
|
|
};
|