1 var fs
= require('fs'),
2 events
= require('events'),
3 buffer
= require('buffer'),
4 http
= require('http'),
6 path
= require('path');
8 this.version
= [0, 6, 0];
10 var mime
= require('./node-static/mime');
11 var util
= require('./node-static/util');
13 // In-memory file store
17 this.Server = function (root
, options
) {
18 if (root
&& (typeof(root
) === 'object')) { options
= root
, root
= null }
20 this.root
= path
.resolve(root
|| '.');
21 this.options
= options
|| {};
24 this.defaultHeaders
= {};
25 this.options
.headers
= this.options
.headers
|| {};
27 if ('cache' in this.options
) {
28 if (typeof(this.options
.cache
) === 'number') {
29 this.cache
= this.options
.cache
;
30 } else if (! this.options
.cache
) {
35 if ('serverInfo' in this.options
) {
36 this.serverInfo
= this.options
.serverInfo
.toString();
38 this.serverInfo
= 'node-static/' + exports
.version
.join('.');
41 this.defaultHeaders
['Server'] = this.serverInfo
;
43 if (this.cache
!== false) {
44 this.defaultHeaders
['Cache-Control'] = 'max-age=' + this.cache
;
47 for (var k
in this.defaultHeaders
) {
48 this.options
.headers
[k
] = this.options
.headers
[k
] ||
49 this.defaultHeaders
[k
];
53 this.Server
.prototype.serveDir = function (pathname
, req
, res
, finish
) {
54 var htmlIndex
= path
.join(pathname
, 'index.html'),
57 fs
.stat(htmlIndex
, function (e
, stat
) {
59 that
.respond(null, 200, {}, [htmlIndex
], stat
, req
, res
, finish
);
61 if (pathname
in exports
.store
) {
62 streamFiles(exports
.indexStore
[pathname
].files
);
64 // Stream a directory of files as a single file.
65 fs
.readFile(path
.join(pathname
, 'index.json'), function (e
, contents
) {
66 if (e
) { return finish(404, {}) }
67 var index
= JSON
.parse(contents
);
68 exports
.indexStore
[pathname
] = index
;
69 streamFiles(index
.files
);
74 function streamFiles(files
) {
75 util
.mstat(pathname
, files
, function (e
, stat
) {
76 that
.respond(pathname
, 200, {}, files
, stat
, req
, res
, finish
);
80 this.Server
.prototype.serveFile = function (pathname
, status
, headers
, req
, res
) {
82 var promise
= new(events
.EventEmitter
);
84 pathname
= this.resolve(pathname
);
86 fs
.stat(pathname
, function (e
, stat
) {
88 return promise
.emit('error', e
);
90 that
.respond(null, status
, headers
, [pathname
], stat
, req
, res
, function (status
, headers
) {
91 that
.finish(status
, headers
, req
, res
, promise
);
96 this.Server
.prototype.finish = function (status
, headers
, req
, res
, promise
, callback
) {
100 message
: http
.STATUS_CODES
[status
]
103 headers
['Server'] = this.serverInfo
;
105 if (!status
|| status
>= 400) {
109 if (promise
.listeners('error').length
> 0) {
110 promise
.emit('error', result
);
112 res
.writeHead(status
, headers
);
116 // Don't end the request here, if we're streaming;
117 // it's taken care of in `prototype.stream`.
118 if (status
!== 200 || req
.method
!== 'GET') {
119 res
.writeHead(status
, headers
);
122 callback
&& callback(null, result
);
123 promise
.emit('success', result
);
127 this.Server
.prototype.servePath = function (pathname
, status
, headers
, req
, res
, finish
) {
129 promise
= new(events
.EventEmitter
);
131 pathname
= this.resolve(pathname
);
133 // Only allow GET and HEAD requests
134 if (req
.method
!== 'GET' && req
.method
!== 'HEAD') {
135 finish(405, { 'Allow': 'GET, HEAD' });
139 // Make sure we're not trying to access a
140 // file outside of the root.
141 if (pathname
.indexOf(that
.root
) === 0) {
142 fs
.stat(pathname
, function (e
, stat
) {
145 } else if (stat
.isFile()) { // Stream a single file.
146 that
.respond(null, status
, headers
, [pathname
], stat
, req
, res
, finish
);
147 } else if (stat
.isDirectory()) { // Stream a directory of files.
148 that
.serveDir(pathname
, req
, res
, finish
);
159 this.Server
.prototype.resolve = function (pathname
) {
160 return path
.resolve(path
.join(this.root
, pathname
));
162 this.Server
.prototype.serve = function (req
, res
, callback
) {
164 promise
= new(events
.EventEmitter
);
166 var pathname
= decodeURI(url
.parse(req
.url
).pathname
);
168 var finish = function (status
, headers
) {
169 that
.finish(status
, headers
, req
, res
, promise
, callback
);
172 process
.nextTick(function () {
173 that
.servePath(pathname
, 200, {}, req
, res
, finish
).on('success', function (result
) {
174 promise
.emit('success', result
);
175 }).on('error', function (err
) {
176 promise
.emit('error');
179 if (! callback
) { return promise
}
182 this.Server
.prototype.respond = function (pathname
, status
, _headers
, files
, stat
, req
, res
, finish
) {
183 var mtime
= Date
.parse(stat
.mtime
),
184 key
= pathname
|| files
[0],
187 // Copy default headers
188 for (var k
in this.options
.headers
) { headers
[k
] = this.options
.headers
[k
] }
190 headers
['ETag'] = JSON
.stringify([stat
.ino
, stat
.size
, mtime
].join('-'));
191 headers
['Date'] = new(Date
)().toUTCString();
192 headers
['Last-Modified'] = new(Date
)(stat
.mtime
).toUTCString();
195 // If the "If-Modified-Since" or "If-None-Match" headers
196 // match the conditions, send a 304 Not Modified.
197 if (req
.headers
['if-none-match'] === headers
['ETag'] ||
198 Date
.parse(req
.headers
['if-modified-since']) >= mtime
) {
199 finish(304, headers
);
201 var fileExtension
= path
.extname(files
[0]).slice(1).toLowerCase();
202 headers
['Content-Length'] = stat
.size
;
203 headers
['Content-Type'] = mime
.contentTypes
[fileExtension
] ||
204 'application/octet-stream';
206 for (var k
in _headers
) { headers
[k
] = _headers
[k
] }
208 res
.writeHead(status
, headers
);
210 if (req
.method
=== 'HEAD') {
211 finish(200, headers
);
215 // If the file was cached and it's not older
216 // than what's on disk, serve the cached version.
217 if (this.cache
&& (key
in exports
.store
) &&
218 exports
.store
[key
].stat
.mtime
>= stat
.mtime
) {
219 res
.end(exports
.store
[key
].buffer
);
220 finish(status
, headers
);
222 this.stream(pathname
, files
, new(buffer
.Buffer
)(stat
.size
), res
, function (e
, buffer
) {
223 if (e
) { return finish(500, {}) }
224 exports
.store
[key
] = {
227 timestamp
: Date
.now()
229 finish(status
, headers
);
235 this.Server
.prototype.stream = function (pathname
, files
, buffer
, res
, callback
) {
236 (function streamFile(files
, offset
) {
237 var file
= files
.shift();
240 file
= file
[0] === '/' ? file
: path
.join(pathname
|| '.', file
);
242 // Stream the file to the client
243 fs
.createReadStream(file
, {
246 }).on('data', function (chunk
) {
247 chunk
.copy(buffer
, offset
);
248 offset
+= chunk
.length
;
249 }).on('close', function () {
250 streamFile(files
, offset
);
251 }).on('error', function (err
) {
254 }).pipe(res
, { end
: false });
257 callback(null, buffer
, offset
);
259 })(files
.slice(0), 0);