203 lines
6.2 KiB
JavaScript
203 lines
6.2 KiB
JavaScript
|
var fs = require('fs')
|
||
|
, util = require('util')
|
||
|
, crypto = require('crypto')
|
||
|
, stream = require('stream')
|
||
|
, path = require('path')
|
||
|
, mimetypes = require('./mimetypes')
|
||
|
, rfc822 = require('./rfc822')
|
||
|
;
|
||
|
|
||
|
function File (options) {
|
||
|
stream.Stream.call(this)
|
||
|
|
||
|
this.writable = true
|
||
|
this.readable = true
|
||
|
this.buffers = []
|
||
|
|
||
|
var self = this
|
||
|
|
||
|
if (typeof options === 'string') options = {path:options}
|
||
|
if (!options.index) options.index = 'index.html'
|
||
|
self.writable = (typeof options.writable === "undefined") ? true : options.writable
|
||
|
self.readable = (typeof options.readable === "undefined") ? true : options.readable
|
||
|
|
||
|
self.path = options.path
|
||
|
self.index = options.index
|
||
|
|
||
|
self.on('pipe', function (src) {
|
||
|
this.src = src
|
||
|
})
|
||
|
|
||
|
this.buffering = true
|
||
|
|
||
|
this.mimetype = options.mimetype || mimetypes.lookup(this.path.slice(this.path.lastIndexOf('.')+1))
|
||
|
|
||
|
var stopBuffering = function () {
|
||
|
self.buffering = false
|
||
|
while (self.buffers.length) {
|
||
|
self.emit('data', self.buffers.shift())
|
||
|
}
|
||
|
if (self.ended) self.emit('end')
|
||
|
}
|
||
|
|
||
|
fs.stat(options.path, function (err, stats) {
|
||
|
|
||
|
var finish = function (err, stats) {
|
||
|
self.stat = stats
|
||
|
if (err && err.code === 'ENOENT' && !self.dest && !self.src) self.src = self.path
|
||
|
if (err && !self.dest && !self.src) return self.emit('error', err)
|
||
|
if (err && self.dest && !self.dest.writeHead) return self.emit('error', err)
|
||
|
|
||
|
// See if writes are disabled
|
||
|
if (self.src && self.src.method &&
|
||
|
!self.writable && self.dest.writeHead &&
|
||
|
(self.src.method === 'PUT' || self.src.method === 'POST')) {
|
||
|
self.dest.writeHead(405, {'content-type':'text/plain'})
|
||
|
self.dest.end(self.src.method+' Not Allowed')
|
||
|
return
|
||
|
}
|
||
|
|
||
|
if (!err) {
|
||
|
self.etag = crypto.createHash('md5').update(stats.ino+'/'+stats.mtime+'/'+stats.size).digest("hex")
|
||
|
self.lastmodified = rfc822.getRFC822Date(stats.mtime)
|
||
|
}
|
||
|
|
||
|
process.nextTick(function () {
|
||
|
stopBuffering()
|
||
|
})
|
||
|
|
||
|
// 404 and 500
|
||
|
if ( err && self.dest && self.dest.writeHead && // We have an error object and dest is an HTTP response
|
||
|
( // Either we have a source and it's a GET/HEAD or we don't have a src
|
||
|
(self.src && (self.src.method == 'GET' || self.src.method === 'HEAD')) || (!self.src)
|
||
|
)
|
||
|
) {
|
||
|
if (err.code === 'ENOENT') {
|
||
|
self.dest.statusCode = 404
|
||
|
self.dest.end('Not Found')
|
||
|
} else {
|
||
|
self.dest.statusCode = 500
|
||
|
self.dest.end(err.message)
|
||
|
}
|
||
|
return
|
||
|
}
|
||
|
|
||
|
// Source is an HTTP Server Request
|
||
|
if (self.src && (self.src.method === 'GET' || self.src.method === 'HEAD') && self.dest) {
|
||
|
if (self.dest.setHeader) {
|
||
|
self.dest.setHeader('content-type', self.mimetype)
|
||
|
self.dest.setHeader('etag', self.etag)
|
||
|
self.dest.setHeader('last-modified', self.lastmodified)
|
||
|
}
|
||
|
|
||
|
if (self.dest.writeHead) {
|
||
|
if (self.src && self.src.headers) {
|
||
|
if (self.src.headers['if-none-match'] === self.etag ||
|
||
|
// Lazy last-modifed matching but it's faster than parsing Datetime
|
||
|
self.src.headers['if-modified-since'] === self.lastmodified) {
|
||
|
self.dest.statusCode = 304
|
||
|
self.dest.end()
|
||
|
return
|
||
|
}
|
||
|
}
|
||
|
// We're going to return the whole file
|
||
|
self.dest.statusCode = 200
|
||
|
self.dest.setHeader('content-length', stats.size)
|
||
|
} else {
|
||
|
// Destination is not an HTTP response, GET and HEAD method are not allowed
|
||
|
return
|
||
|
}
|
||
|
|
||
|
if (self.src.method !== 'HEAD') {
|
||
|
fs.createReadStream(self.path).pipe(self.dest)
|
||
|
}
|
||
|
return
|
||
|
}
|
||
|
|
||
|
if (self.src && (self.src.method === 'PUT' || self.src.method === 'POST')) {
|
||
|
if (!err) {
|
||
|
// TODO handle overwrite case
|
||
|
return
|
||
|
}
|
||
|
stream.Stream.prototype.pipe.call(self, fs.createWriteStream(self.path))
|
||
|
if (self.dest && self.dest.writeHead) {
|
||
|
self.on('end', function () {
|
||
|
self.dest.statusCode = 201
|
||
|
self.dest.setHeader('content-length', 0)
|
||
|
self.dest.end()
|
||
|
})
|
||
|
}
|
||
|
return
|
||
|
}
|
||
|
|
||
|
// Desination is an HTTP response, we already handled 404 and 500
|
||
|
if (self.dest && self.dest.writeHead) {
|
||
|
self.dest.statusCode = 200
|
||
|
self.dest.setHeader('content-type', self.mimetype)
|
||
|
self.dest.setHeader('etag', self.etag)
|
||
|
self.dest.setHeader('last-modified', self.lastmodified)
|
||
|
self.dest.setHeader('content-length', stats.size)
|
||
|
fs.createReadStream(self.path).pipe(self.dest)
|
||
|
return
|
||
|
}
|
||
|
|
||
|
// Destination is not an HTTP request
|
||
|
|
||
|
if (self.src && !self.dest) {
|
||
|
stream.Stream.prototype.pipe.call(self, fs.createWriteStream(self.path))
|
||
|
} else if (self.dest && !self.src) {
|
||
|
fs.createReadStream(self.path).pipe(self.dest)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (!err && stats.isDirectory()) {
|
||
|
self.path = path.join(self.path, self.index)
|
||
|
self.mimetype = mimetypes.lookup(self.path.slice(self.path.lastIndexOf('.')+1))
|
||
|
fs.stat(self.path, finish)
|
||
|
return
|
||
|
} else {
|
||
|
finish(err, stats)
|
||
|
}
|
||
|
|
||
|
if (!self.src && !self.dest) {
|
||
|
if (self.buffers.length > 0) {
|
||
|
stream.Stream.prototype.pipe.call(self, fs.createWriteStream(self.path))
|
||
|
} else if (self.listeners('data').length > 0) {
|
||
|
fs.createReadStream(self.path).pipe(self.dest)
|
||
|
} else {
|
||
|
fs.createReadStream(self.path).pipe(self)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
})
|
||
|
|
||
|
}
|
||
|
util.inherits(File, stream.Stream)
|
||
|
File.prototype.pipe = function (dest, options) {
|
||
|
this.dest = dest
|
||
|
this.destOptions = options
|
||
|
dest.emit('pipe', this)
|
||
|
// stream.Stream.prototype.pipe.call(this, dest, options)
|
||
|
}
|
||
|
File.prototype.write = function (chunk, encoding) {
|
||
|
if (encoding) chunk = chunk.toString(encoding)
|
||
|
if (this.buffering) {
|
||
|
this.buffers.push(chunk)
|
||
|
} else {
|
||
|
this.emit('data', chunk)
|
||
|
}
|
||
|
}
|
||
|
File.prototype.end = function (chunk) {
|
||
|
if (chunk) this.write(chunk)
|
||
|
if (this.buffering) {
|
||
|
this.ended = true
|
||
|
} else {
|
||
|
this.emit('end')
|
||
|
}
|
||
|
}
|
||
|
|
||
|
module.exports = function (options) {
|
||
|
return new File(options)
|
||
|
}
|
||
|
module.exports.File = File
|