annotate node_modules/node-static/lib/node-static.js @ 101:52e44ee1c791 tip master

enabled all scores in autostart script
author Rob Canning <rc@kiben.net>
date Tue, 21 Apr 2015 16:20:57 +0100
parents 0ae87af84e2f
children
rev   line source
rc-web@69 1 var fs = require('fs')
rc-web@69 2 , events = require('events')
rc-web@69 3 , buffer = require('buffer')
rc-web@69 4 , http = require('http')
rc-web@69 5 , url = require('url')
rc-web@69 6 , path = require('path')
rc-web@69 7 , mime = require('mime')
rc-web@69 8 , util = require('./node-static/util');
rc-web@69 9
rc-web@69 10 // Current version
rob@76 11 var version = [0, 7, 3];
rc-web@69 12
rc-web@69 13 Server = function (root, options) {
rc-web@69 14 if (root && (typeof(root) === 'object')) { options = root; root = null }
rc-web@69 15
rc-web@69 16 this.root = path.resolve(root || '.');
rc-web@69 17 this.options = options || {};
rc-web@69 18 this.cache = 3600;
rc-web@69 19
rc-web@69 20 this.defaultHeaders = {};
rc-web@69 21 this.options.headers = this.options.headers || {};
rc-web@69 22
rc-web@69 23 if ('cache' in this.options) {
rc-web@69 24 if (typeof(this.options.cache) === 'number') {
rc-web@69 25 this.cache = this.options.cache;
rc-web@69 26 } else if (! this.options.cache) {
rc-web@69 27 this.cache = false;
rc-web@69 28 }
rc-web@69 29 }
rc-web@69 30
rc-web@69 31 if ('serverInfo' in this.options) {
rc-web@69 32 this.serverInfo = this.options.serverInfo.toString();
rc-web@69 33 } else {
rc-web@69 34 this.serverInfo = 'node-static/' + version.join('.');
rc-web@69 35 }
rc-web@69 36
rc-web@69 37 this.defaultHeaders['server'] = this.serverInfo;
rc-web@69 38
rc-web@69 39 if (this.cache !== false) {
rc-web@69 40 this.defaultHeaders['cache-control'] = 'max-age=' + this.cache;
rc-web@69 41 }
rc-web@69 42
rc-web@69 43 for (var k in this.defaultHeaders) {
rc-web@69 44 this.options.headers[k] = this.options.headers[k] ||
rc-web@69 45 this.defaultHeaders[k];
rc-web@69 46 }
rc-web@69 47 };
rc-web@69 48
rc-web@69 49 Server.prototype.serveDir = function (pathname, req, res, finish) {
rc-web@69 50 var htmlIndex = path.join(pathname, 'index.html'),
rc-web@69 51 that = this;
rc-web@69 52
rc-web@69 53 fs.stat(htmlIndex, function (e, stat) {
rc-web@69 54 if (!e) {
rc-web@69 55 var status = 200;
rc-web@69 56 var headers = {};
rc-web@69 57 var originalPathname = decodeURI(url.parse(req.url).pathname);
rc-web@69 58 if (originalPathname.length && originalPathname.charAt(originalPathname.length - 1) !== '/') {
rc-web@69 59 return finish(301, { 'Location': originalPathname + '/' });
rc-web@69 60 } else {
rc-web@69 61 that.respond(null, status, headers, [htmlIndex], stat, req, res, finish);
rc-web@69 62 }
rc-web@69 63 } else {
rc-web@69 64 // Stream a directory of files as a single file.
rc-web@69 65 fs.readFile(path.join(pathname, 'index.json'), function (e, contents) {
rc-web@69 66 if (e) { return finish(404, {}) }
rc-web@69 67 var index = JSON.parse(contents);
rc-web@69 68 streamFiles(index.files);
rc-web@69 69 });
rc-web@69 70 }
rc-web@69 71 });
rc-web@69 72 function streamFiles(files) {
rc-web@69 73 util.mstat(pathname, files, function (e, stat) {
rc-web@69 74 if (e) { return finish(404, {}) }
rc-web@69 75 that.respond(pathname, 200, {}, files, stat, req, res, finish);
rc-web@69 76 });
rc-web@69 77 }
rc-web@69 78 };
rc-web@69 79
rc-web@69 80 Server.prototype.serveFile = function (pathname, status, headers, req, res) {
rc-web@69 81 var that = this;
rc-web@69 82 var promise = new(events.EventEmitter);
rc-web@69 83
rc-web@69 84 pathname = this.resolve(pathname);
rc-web@69 85
rc-web@69 86 fs.stat(pathname, function (e, stat) {
rc-web@69 87 if (e) {
rc-web@69 88 return promise.emit('error', e);
rc-web@69 89 }
rc-web@69 90 that.respond(null, status, headers, [pathname], stat, req, res, function (status, headers) {
rc-web@69 91 that.finish(status, headers, req, res, promise);
rc-web@69 92 });
rc-web@69 93 });
rc-web@69 94 return promise;
rc-web@69 95 };
rc-web@69 96
rc-web@69 97 Server.prototype.finish = function (status, headers, req, res, promise, callback) {
rc-web@69 98 var result = {
rc-web@69 99 status: status,
rc-web@69 100 headers: headers,
rc-web@69 101 message: http.STATUS_CODES[status]
rc-web@69 102 };
rc-web@69 103
rc-web@69 104 headers['server'] = this.serverInfo;
rc-web@69 105
rc-web@69 106 if (!status || status >= 400) {
rc-web@69 107 if (callback) {
rc-web@69 108 callback(result);
rc-web@69 109 } else {
rc-web@69 110 if (promise.listeners('error').length > 0) {
rc-web@69 111 promise.emit('error', result);
rc-web@69 112 }
rc-web@69 113 else {
rc-web@69 114 res.writeHead(status, headers);
rc-web@69 115 res.end();
rc-web@69 116 }
rc-web@69 117 }
rc-web@69 118 } else {
rc-web@69 119 // Don't end the request here, if we're streaming;
rc-web@69 120 // it's taken care of in `prototype.stream`.
rc-web@69 121 if (status !== 200 || req.method !== 'GET') {
rc-web@69 122 res.writeHead(status, headers);
rc-web@69 123 res.end();
rc-web@69 124 }
rc-web@69 125 callback && callback(null, result);
rc-web@69 126 promise.emit('success', result);
rc-web@69 127 }
rc-web@69 128 };
rc-web@69 129
rc-web@69 130 Server.prototype.servePath = function (pathname, status, headers, req, res, finish) {
rc-web@69 131 var that = this,
rc-web@69 132 promise = new(events.EventEmitter);
rc-web@69 133
rc-web@69 134 pathname = this.resolve(pathname);
rc-web@69 135
rc-web@69 136 // Make sure we're not trying to access a
rc-web@69 137 // file outside of the root.
rc-web@69 138 if (pathname.indexOf(that.root) === 0) {
rc-web@69 139 fs.stat(pathname, function (e, stat) {
rc-web@69 140 if (e) {
rc-web@69 141 finish(404, {});
rc-web@69 142 } else if (stat.isFile()) { // Stream a single file.
rc-web@69 143 that.respond(null, status, headers, [pathname], stat, req, res, finish);
rc-web@69 144 } else if (stat.isDirectory()) { // Stream a directory of files.
rc-web@69 145 that.serveDir(pathname, req, res, finish);
rc-web@69 146 } else {
rc-web@69 147 finish(400, {});
rc-web@69 148 }
rc-web@69 149 });
rc-web@69 150 } else {
rc-web@69 151 // Forbidden
rc-web@69 152 finish(403, {});
rc-web@69 153 }
rc-web@69 154 return promise;
rc-web@69 155 };
rc-web@69 156
rc-web@69 157 Server.prototype.resolve = function (pathname) {
rc-web@69 158 return path.resolve(path.join(this.root, pathname));
rc-web@69 159 };
rc-web@69 160
rc-web@69 161 Server.prototype.serve = function (req, res, callback) {
rc-web@69 162 var that = this,
rc-web@69 163 promise = new(events.EventEmitter),
rc-web@69 164 pathname;
rc-web@69 165
rc-web@69 166 var finish = function (status, headers) {
rc-web@69 167 that.finish(status, headers, req, res, promise, callback);
rc-web@69 168 };
rc-web@69 169
rc-web@69 170 try {
rc-web@69 171 pathname = decodeURI(url.parse(req.url).pathname);
rc-web@69 172 }
rc-web@69 173 catch(e) {
rc-web@69 174 return process.nextTick(function() {
rc-web@69 175 return finish(400, {});
rc-web@69 176 });
rc-web@69 177 }
rc-web@69 178
rc-web@69 179 process.nextTick(function () {
rc-web@69 180 that.servePath(pathname, 200, {}, req, res, finish).on('success', function (result) {
rc-web@69 181 promise.emit('success', result);
rc-web@69 182 }).on('error', function (err) {
rc-web@69 183 promise.emit('error');
rc-web@69 184 });
rc-web@69 185 });
rc-web@69 186 if (! callback) { return promise }
rc-web@69 187 };
rc-web@69 188
rc-web@69 189 /* Check if we should consider sending a gzip version of the file based on the
rc-web@69 190 * file content type and client's Accept-Encoding header value.
rc-web@69 191 */
rc-web@69 192 Server.prototype.gzipOk = function(req, contentType) {
rc-web@69 193 var enable = this.options.gzip;
rc-web@69 194 if(enable &&
rc-web@69 195 (typeof enable === 'boolean' ||
rc-web@69 196 (contentType && (enable instanceof RegExp) && enable.test(contentType)))) {
rc-web@69 197 var acceptEncoding = req.headers['accept-encoding'];
rc-web@69 198 return acceptEncoding && acceptEncoding.indexOf("gzip") >= 0;
rc-web@69 199 }
rc-web@69 200 return false;
rc-web@69 201 }
rc-web@69 202
rc-web@69 203 /* Send a gzipped version of the file if the options and the client indicate gzip is enabled and
rc-web@69 204 * we find a .gz file mathing the static resource requested.
rc-web@69 205 */
rc-web@69 206 Server.prototype.respondGzip = function(pathname, status, contentType, _headers, files, stat, req, res, finish) {
rc-web@69 207 var that = this;
rc-web@69 208 if(files.length == 1 && this.gzipOk(req, contentType)) {
rc-web@69 209 var gzFile = files[0] + ".gz";
rc-web@69 210 fs.stat(gzFile, function(e, gzStat) {
rc-web@69 211 if(!e && gzStat.isFile()) {
rc-web@69 212 //console.log('Serving', gzFile, 'to gzip-capable client instead of', files[0], 'new size is', gzStat.size, 'uncompressed size', stat.size);
rc-web@69 213 var vary = _headers['Vary'];
rc-web@69 214 _headers['Vary'] = (vary && vary != 'Accept-Encoding'?vary+', ':'')+'Accept-Encoding';
rc-web@69 215 _headers['Content-Encoding'] = 'gzip';
rc-web@69 216 stat.size = gzStat.size;
rc-web@69 217 files = [gzFile];
rc-web@69 218 } else {
rc-web@69 219 //console.log('gzip file not found or error finding it', gzFile, String(e), stat.isFile());
rc-web@69 220 }
rc-web@69 221 that.respondNoGzip(pathname, status, contentType, _headers, files, stat, req, res, finish);
rc-web@69 222 });
rc-web@69 223 } else {
rc-web@69 224 // Client doesn't want gzip or we're sending multiple files
rc-web@69 225 that.respondNoGzip(pathname, status, contentType, _headers, files, stat, req, res, finish);
rc-web@69 226 }
rc-web@69 227 }
rc-web@69 228
rc-web@69 229 Server.prototype.respondNoGzip = function (pathname, status, contentType, _headers, files, stat, req, res, finish) {
rc-web@69 230 var mtime = Date.parse(stat.mtime),
rc-web@69 231 key = pathname || files[0],
rc-web@69 232 headers = {},
rc-web@69 233 clientETag = req.headers['if-none-match'],
rc-web@69 234 clientMTime = Date.parse(req.headers['if-modified-since']);
rc-web@69 235
rc-web@69 236
rc-web@69 237 // Copy default headers
rc-web@69 238 for (var k in this.options.headers) { headers[k] = this.options.headers[k] }
rc-web@69 239 // Copy custom headers
rc-web@69 240 for (var k in _headers) { headers[k] = _headers[k] }
rc-web@69 241
rc-web@69 242 headers['Etag'] = JSON.stringify([stat.ino, stat.size, mtime].join('-'));
rc-web@69 243 headers['Date'] = new(Date)().toUTCString();
rc-web@69 244 headers['Last-Modified'] = new(Date)(stat.mtime).toUTCString();
rc-web@69 245 headers['Content-Type'] = contentType;
rc-web@69 246 headers['Content-Length'] = stat.size;
rc-web@69 247
rc-web@69 248 for (var k in _headers) { headers[k] = _headers[k] }
rc-web@69 249
rc-web@69 250 // Conditional GET
rc-web@69 251 // If the "If-Modified-Since" or "If-None-Match" headers
rc-web@69 252 // match the conditions, send a 304 Not Modified.
rc-web@69 253 if ((clientMTime || clientETag) &&
rc-web@69 254 (!clientETag || clientETag === headers['Etag']) &&
rc-web@69 255 (!clientMTime || clientMTime >= mtime)) {
rc-web@69 256 // 304 response should not contain entity headers
rc-web@69 257 ['Content-Encoding',
rc-web@69 258 'Content-Language',
rc-web@69 259 'Content-Length',
rc-web@69 260 'Content-Location',
rc-web@69 261 'Content-MD5',
rc-web@69 262 'Content-Range',
rc-web@69 263 'Content-Type',
rc-web@69 264 'Expires',
rc-web@69 265 'Last-Modified'].forEach(function(entityHeader) {
rc-web@69 266 delete headers[entityHeader];
rc-web@69 267 });
rc-web@69 268 finish(304, headers);
rc-web@69 269 } else {
rc-web@69 270 res.writeHead(status, headers);
rc-web@69 271
rc-web@69 272 this.stream(pathname, files, new(buffer.Buffer)(stat.size), res, function (e, buffer) {
rc-web@69 273 if (e) { return finish(500, {}) }
rc-web@69 274 finish(status, headers);
rc-web@69 275 });
rc-web@69 276 }
rc-web@69 277 };
rc-web@69 278
rc-web@69 279 Server.prototype.respond = function (pathname, status, _headers, files, stat, req, res, finish) {
rc-web@69 280 var contentType = _headers['Content-Type'] ||
rc-web@69 281 mime.lookup(files[0]) ||
rc-web@69 282 'application/octet-stream';
rc-web@69 283 if(this.options.gzip) {
rc-web@69 284 this.respondGzip(pathname, status, contentType, _headers, files, stat, req, res, finish);
rc-web@69 285 } else {
rc-web@69 286 this.respondNoGzip(pathname, status, contentType, _headers, files, stat, req, res, finish);
rc-web@69 287 }
rc-web@69 288 }
rc-web@69 289
rc-web@69 290 Server.prototype.stream = function (pathname, files, buffer, res, callback) {
rc-web@69 291 (function streamFile(files, offset) {
rc-web@69 292 var file = files.shift();
rc-web@69 293
rc-web@69 294 if (file) {
rc-web@69 295 file = file[0] === '/' ? file : path.join(pathname || '.', file);
rc-web@69 296
rc-web@69 297 // Stream the file to the client
rc-web@69 298 fs.createReadStream(file, {
rc-web@69 299 flags: 'r',
rc-web@69 300 mode: 0666
rc-web@69 301 }).on('data', function (chunk) {
rob@76 302 // Bounds check the incoming chunk and offset, as copying
rob@76 303 // a buffer from an invalid offset will throw an error and crash
rob@76 304 if (chunk.length && offset < buffer.length && offset >= 0) {
rob@76 305 chunk.copy(buffer, offset);
rob@76 306 offset += chunk.length;
rob@76 307 }
rc-web@69 308 }).on('close', function () {
rc-web@69 309 streamFile(files, offset);
rc-web@69 310 }).on('error', function (err) {
rc-web@69 311 callback(err);
rc-web@69 312 console.error(err);
rc-web@69 313 }).pipe(res, { end: false });
rc-web@69 314 } else {
rc-web@69 315 res.end();
rc-web@69 316 callback(null, buffer, offset);
rc-web@69 317 }
rc-web@69 318 })(files.slice(0), 0);
rc-web@69 319 };
rc-web@69 320
rc-web@69 321 // Exports
rc-web@69 322 exports.Server = Server;
rc-web@69 323 exports.version = version;
rc-web@69 324 exports.mime = mime;
rc-web@69 325
rc-web@69 326
rc-web@69 327