| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252 |
- var fs = require('fs'),
- sys = require('sys'),
- events = require('events'),
- buffer = require('buffer'),
- http = require('http'),
- url = require('url'),
- path = require('path');
- this.version = [0, 5, 7];
- var mime = require('./node-static/mime');
- var util = require('./node-static/util');
- var serverInfo = 'node-static/' + this.version.join('.');
- // In-memory file store
- this.store = {};
- this.indexStore = {};
- this.Server = function (root, options) {
- if (root && (typeof(root) === 'object')) { options = root, root = null }
- this.root = path.normalize(root || '.');
- this.options = options || {};
- this.cache = 3600;
- this.defaultHeaders = {};
- this.options.headers = this.options.headers || {};
- if ('cache' in this.options) {
- if (typeof(this.options.cache) === 'number') {
- this.cache = this.options.cache;
- } else if (! this.options.cache) {
- this.cache = false;
- }
- }
- if (this.cache !== false) {
- this.defaultHeaders['Cache-Control'] = 'max-age=' + this.cache;
- }
- this.defaultHeaders['Server'] = serverInfo;
- for (var k in this.defaultHeaders) {
- this.options.headers[k] = this.options.headers[k] ||
- this.defaultHeaders[k];
- }
- };
- this.Server.prototype.serveDir = function (pathname, req, res, finish) {
- var htmlIndex = path.join(pathname, 'index.html'),
- that = this;
- fs.stat(htmlIndex, function (e, stat) {
- if (!e) {
- that.respond(null, 200, {}, [htmlIndex], stat, req, res, finish);
- } else {
- if (pathname in exports.store) {
- streamFiles(exports.indexStore[pathname].files);
- } else {
- // Stream a directory of files as a single file.
- fs.readFile(path.join(pathname, 'index.json'), function (e, contents) {
- if (e) { return finish(404, {}) }
- var index = JSON.parse(contents);
- exports.indexStore[pathname] = index;
- streamFiles(index.files);
- });
- }
- }
- });
- function streamFiles(files) {
- util.mstat(pathname, files, function (e, stat) {
- that.respond(pathname, 200, {}, files, stat, req, res, finish);
- });
- }
- };
- this.Server.prototype.serveFile = function (pathname, status, headers, req, res) {
- var that = this;
- var promise = new(events.EventEmitter);
- pathname = this.normalize(pathname);
- fs.stat(pathname, function (e, stat) {
- if (e) {
- return promise.emit('error', e);
- }
- that.respond(null, status, headers, [pathname], stat, req, res, function (status, headers) {
- that.finish(status, headers, req, res, promise);
- });
- });
- return promise;
- };
- this.Server.prototype.finish = function (status, headers, req, res, promise, callback) {
- var result = {
- status: status,
- headers: headers,
- message: http.STATUS_CODES[status]
- };
- headers['Server'] = serverInfo;
- if (!status || status >= 400) {
- if (callback) {
- callback(result);
- } else {
- if (promise.listeners('error').length > 0) {
- promise.emit('error', result);
- }
- res.writeHead(status, headers);
- res.end();
- }
- } else {
- // Don't end the request here, if we're streaming;
- // it's taken care of in `prototype.stream`.
- if (status !== 200 || req.method !== 'GET') {
- res.writeHead(status, headers);
- res.end();
- }
- callback && callback(null, result);
- promise.emit('success', result);
- }
- };
- this.Server.prototype.servePath = function (pathname, status, headers, req, res, finish) {
- var that = this,
- promise = new(events.EventEmitter);
- pathname = this.normalize(pathname);
- // Only allow GET and HEAD requests
- if (req.method !== 'GET' && req.method !== 'HEAD') {
- finish(405, { 'Allow': 'GET, HEAD' });
- return promise;
- }
- // Make sure we're not trying to access a
- // file outside of the root.
- if (new(RegExp)('^' + that.root).test(pathname)) {
- fs.stat(pathname, function (e, stat) {
- if (e) {
- finish(404, {});
- } else if (stat.isFile()) { // Stream a single file.
- that.respond(null, status, headers, [pathname], stat, req, res, finish);
- } else if (stat.isDirectory()) { // Stream a directory of files.
- that.serveDir(pathname, req, res, finish);
- } else {
- finish(400, {});
- }
- });
- } else {
- // Forbidden
- finish(403, {});
- }
- return promise;
- };
- this.Server.prototype.normalize = function (pathname) {
- return path.normalize(path.join(this.root, pathname));
- };
- this.Server.prototype.serve = function (req, res, callback) {
- var that = this,
- promise = new(events.EventEmitter);
- var pathname = url.parse(req.url).pathname;
- var finish = function (status, headers) {
- that.finish(status, headers, req, res, promise, callback);
- };
- process.nextTick(function () {
- that.servePath(pathname, 200, {}, req, res, finish).on('success', function (result) {
- promise.emit('success', result);
- }).on('error', function (err) {
- promise.emit('error');
- });
- });
- if (! callback) { return promise }
- };
- this.Server.prototype.respond = function (pathname, status, _headers, files, stat, req, res, finish) {
- var mtime = Date.parse(stat.mtime),
- key = pathname || files[0],
- headers = {};
- // Copy default headers
- for (var k in this.options.headers) { headers[k] = this.options.headers[k] }
- headers['Etag'] = JSON.stringify([stat.ino, stat.size, mtime].join('-'));
- headers['Date'] = new(Date)().toUTCString();
- headers['Last-Modified'] = new(Date)(stat.mtime).toUTCString();
- // Conditional GET
- // If the "If-Modified-Since" or "If-None-Match" headers
- // match the conditions, send a 304 Not Modified.
- if (req.headers['if-none-match'] === headers['Etag'] ||
- Date.parse(req.headers['if-modified-since']) >= mtime) {
- finish(304, headers);
- } else if (req.method === 'HEAD') {
- finish(200, headers);
- } else {
- headers['Content-Length'] = stat.size;
- headers['Content-Type'] = mime.contentTypes[path.extname(files[0]).slice(1)] ||
- 'application/octet-stream';
- for (var k in _headers) { headers[k] = _headers[k] }
- res.writeHead(status, headers);
- // If the file was cached and it's not older
- // than what's on disk, serve the cached version.
- if (this.cache && (key in exports.store) &&
- exports.store[key].stat.mtime >= stat.mtime) {
- res.end(exports.store[key].buffer);
- finish(status, headers);
- } else {
- this.stream(pathname, files, new(buffer.Buffer)(stat.size), res, function (e, buffer) {
- if (e) { return finish(500, {}) }
- exports.store[key] = {
- stat: stat,
- buffer: buffer,
- timestamp: Date.now()
- };
- finish(status, headers);
- });
- }
- }
- };
- this.Server.prototype.stream = function (pathname, files, buffer, res, callback) {
- (function streamFile(files, offset) {
- var file = files.shift();
- if (file) {
- file = file[0] === '/' ? file : path.join(pathname || '.', file);
- // Stream the file to the client
- fs.createReadStream(file, {
- flags: 'r',
- mode: 0666
- }).on('data', function (chunk) {
- chunk.copy(buffer, offset);
- offset += chunk.length;
- }).on('close', function () {
- streamFile(files, offset);
- }).on('error', function (err) {
- callback(err);
- console.error(err);
- }).pipe(res, { end: false });
- } else {
- res.end();
- callback(null, buffer, offset);
- }
- })(files.slice(0), 0);
- };
|