node-static.js 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252
  1. var fs = require('fs'),
  2. sys = require('sys'),
  3. events = require('events'),
  4. buffer = require('buffer'),
  5. http = require('http'),
  6. url = require('url'),
  7. path = require('path');
  8. this.version = [0, 5, 7];
  9. var mime = require('./node-static/mime');
  10. var util = require('./node-static/util');
  11. var serverInfo = 'node-static/' + this.version.join('.');
  12. // In-memory file store
  13. this.store = {};
  14. this.indexStore = {};
  15. this.Server = function (root, options) {
  16. if (root && (typeof(root) === 'object')) { options = root, root = null }
  17. this.root = path.normalize(root || '.');
  18. this.options = options || {};
  19. this.cache = 3600;
  20. this.defaultHeaders = {};
  21. this.options.headers = this.options.headers || {};
  22. if ('cache' in this.options) {
  23. if (typeof(this.options.cache) === 'number') {
  24. this.cache = this.options.cache;
  25. } else if (! this.options.cache) {
  26. this.cache = false;
  27. }
  28. }
  29. if (this.cache !== false) {
  30. this.defaultHeaders['Cache-Control'] = 'max-age=' + this.cache;
  31. }
  32. this.defaultHeaders['Server'] = serverInfo;
  33. for (var k in this.defaultHeaders) {
  34. this.options.headers[k] = this.options.headers[k] ||
  35. this.defaultHeaders[k];
  36. }
  37. };
  38. this.Server.prototype.serveDir = function (pathname, req, res, finish) {
  39. var htmlIndex = path.join(pathname, 'index.html'),
  40. that = this;
  41. fs.stat(htmlIndex, function (e, stat) {
  42. if (!e) {
  43. that.respond(null, 200, {}, [htmlIndex], stat, req, res, finish);
  44. } else {
  45. if (pathname in exports.store) {
  46. streamFiles(exports.indexStore[pathname].files);
  47. } else {
  48. // Stream a directory of files as a single file.
  49. fs.readFile(path.join(pathname, 'index.json'), function (e, contents) {
  50. if (e) { return finish(404, {}) }
  51. var index = JSON.parse(contents);
  52. exports.indexStore[pathname] = index;
  53. streamFiles(index.files);
  54. });
  55. }
  56. }
  57. });
  58. function streamFiles(files) {
  59. util.mstat(pathname, files, function (e, stat) {
  60. that.respond(pathname, 200, {}, files, stat, req, res, finish);
  61. });
  62. }
  63. };
  64. this.Server.prototype.serveFile = function (pathname, status, headers, req, res) {
  65. var that = this;
  66. var promise = new(events.EventEmitter);
  67. pathname = this.normalize(pathname);
  68. fs.stat(pathname, function (e, stat) {
  69. if (e) {
  70. return promise.emit('error', e);
  71. }
  72. that.respond(null, status, headers, [pathname], stat, req, res, function (status, headers) {
  73. that.finish(status, headers, req, res, promise);
  74. });
  75. });
  76. return promise;
  77. };
  78. this.Server.prototype.finish = function (status, headers, req, res, promise, callback) {
  79. var result = {
  80. status: status,
  81. headers: headers,
  82. message: http.STATUS_CODES[status]
  83. };
  84. headers['Server'] = serverInfo;
  85. if (!status || status >= 400) {
  86. if (callback) {
  87. callback(result);
  88. } else {
  89. if (promise.listeners('error').length > 0) {
  90. promise.emit('error', result);
  91. }
  92. res.writeHead(status, headers);
  93. res.end();
  94. }
  95. } else {
  96. // Don't end the request here, if we're streaming;
  97. // it's taken care of in `prototype.stream`.
  98. if (status !== 200 || req.method !== 'GET') {
  99. res.writeHead(status, headers);
  100. res.end();
  101. }
  102. callback && callback(null, result);
  103. promise.emit('success', result);
  104. }
  105. };
  106. this.Server.prototype.servePath = function (pathname, status, headers, req, res, finish) {
  107. var that = this,
  108. promise = new(events.EventEmitter);
  109. pathname = this.normalize(pathname);
  110. // Only allow GET and HEAD requests
  111. if (req.method !== 'GET' && req.method !== 'HEAD') {
  112. finish(405, { 'Allow': 'GET, HEAD' });
  113. return promise;
  114. }
  115. // Make sure we're not trying to access a
  116. // file outside of the root.
  117. if (new(RegExp)('^' + that.root).test(pathname)) {
  118. fs.stat(pathname, function (e, stat) {
  119. if (e) {
  120. finish(404, {});
  121. } else if (stat.isFile()) { // Stream a single file.
  122. that.respond(null, status, headers, [pathname], stat, req, res, finish);
  123. } else if (stat.isDirectory()) { // Stream a directory of files.
  124. that.serveDir(pathname, req, res, finish);
  125. } else {
  126. finish(400, {});
  127. }
  128. });
  129. } else {
  130. // Forbidden
  131. finish(403, {});
  132. }
  133. return promise;
  134. };
  135. this.Server.prototype.normalize = function (pathname) {
  136. return path.normalize(path.join(this.root, pathname));
  137. };
  138. this.Server.prototype.serve = function (req, res, callback) {
  139. var that = this,
  140. promise = new(events.EventEmitter);
  141. var pathname = url.parse(req.url).pathname;
  142. var finish = function (status, headers) {
  143. that.finish(status, headers, req, res, promise, callback);
  144. };
  145. process.nextTick(function () {
  146. that.servePath(pathname, 200, {}, req, res, finish).on('success', function (result) {
  147. promise.emit('success', result);
  148. }).on('error', function (err) {
  149. promise.emit('error');
  150. });
  151. });
  152. if (! callback) { return promise }
  153. };
  154. this.Server.prototype.respond = function (pathname, status, _headers, files, stat, req, res, finish) {
  155. var mtime = Date.parse(stat.mtime),
  156. key = pathname || files[0],
  157. headers = {};
  158. // Copy default headers
  159. for (var k in this.options.headers) { headers[k] = this.options.headers[k] }
  160. headers['Etag'] = JSON.stringify([stat.ino, stat.size, mtime].join('-'));
  161. headers['Date'] = new(Date)().toUTCString();
  162. headers['Last-Modified'] = new(Date)(stat.mtime).toUTCString();
  163. // Conditional GET
  164. // If the "If-Modified-Since" or "If-None-Match" headers
  165. // match the conditions, send a 304 Not Modified.
  166. if (req.headers['if-none-match'] === headers['Etag'] ||
  167. Date.parse(req.headers['if-modified-since']) >= mtime) {
  168. finish(304, headers);
  169. } else if (req.method === 'HEAD') {
  170. finish(200, headers);
  171. } else {
  172. headers['Content-Length'] = stat.size;
  173. headers['Content-Type'] = mime.contentTypes[path.extname(files[0]).slice(1)] ||
  174. 'application/octet-stream';
  175. for (var k in _headers) { headers[k] = _headers[k] }
  176. res.writeHead(status, headers);
  177. // If the file was cached and it's not older
  178. // than what's on disk, serve the cached version.
  179. if (this.cache && (key in exports.store) &&
  180. exports.store[key].stat.mtime >= stat.mtime) {
  181. res.end(exports.store[key].buffer);
  182. finish(status, headers);
  183. } else {
  184. this.stream(pathname, files, new(buffer.Buffer)(stat.size), res, function (e, buffer) {
  185. if (e) { return finish(500, {}) }
  186. exports.store[key] = {
  187. stat: stat,
  188. buffer: buffer,
  189. timestamp: Date.now()
  190. };
  191. finish(status, headers);
  192. });
  193. }
  194. }
  195. };
  196. this.Server.prototype.stream = function (pathname, files, buffer, res, callback) {
  197. (function streamFile(files, offset) {
  198. var file = files.shift();
  199. if (file) {
  200. file = file[0] === '/' ? file : path.join(pathname || '.', file);
  201. // Stream the file to the client
  202. fs.createReadStream(file, {
  203. flags: 'r',
  204. mode: 0666
  205. }).on('data', function (chunk) {
  206. chunk.copy(buffer, offset);
  207. offset += chunk.length;
  208. }).on('close', function () {
  209. streamFile(files, offset);
  210. }).on('error', function (err) {
  211. callback(err);
  212. console.error(err);
  213. }).pipe(res, { end: false });
  214. } else {
  215. res.end();
  216. callback(null, buffer, offset);
  217. }
  218. })(files.slice(0), 0);
  219. };