arcane-middleware-assets
Version:
File Serving module for arcane
277 lines (248 loc) • 7.78 kB
text/coffeescript
#!package export Assets
#!import fs
#!import path
#!import crypto
#!import MiddlewareHandler from arcane-middleware
#!import on-finished
#!import etag
#!import mime
#!import tools.wait
class Assets extends MiddlewareHandler
middleware: (, ) ->
self = this
components_map = {}
controller_path = "#{@global.root}/controller"
for i in fs.readdirSync(controller_path)
component_path = "#{controller_path}/#{i}/components"
try for x in fs.readdirSync(component_path)
components_map["/#{i.replace(/Controller$/g , '').toLowerCase()}/components/#{x.replace(/.ts$/g, '')}"] = "#{component_path}/#{x}"
.on 'components-delete', (path) ->
delete components_map[path];
.on 'components-added', (path_arr) ->
components_map[path_arr[0].replace(/.ts$/g, '')] = path_arr[1]
(req, res, app) ->
request_file = "#{req.root}/assets#{req.url.split('?')[0]}"
if not (fs.existsSync(request_file) and fs.lstatSync(request_file).isFile()) and components_map[req.url.split('?')[0]]?
request_file = components_map[req.url.split('?')[0]]
if request_file? and fs.existsSync(request_file) and fs.lstatSync(request_file).isFile()
stats = fs.statSync(request_file)
result = wait.for Assets.RequestFileStaticFile, req, res, request_file, stats
throw 'assets'
: fs.ReadStream
: (size, str) ->
valid = true
i = str.indexOf('=')
if -1 == i
return -2
arr = str.slice(i + 1).split(',').map((range) ->
_range = range.split('-')
start = parseInt(_range[0], 10)
end = parseInt(_range[1], 10)
# -nnn
if isNaN(start)
start = size - end
end = size - 1
# nnn-
else if isNaN(end)
end = size - 1
# limit last-byte-pos to current length
if end > size - 1
end = size - 1
# invalid
if isNaN(start) or isNaN(end) or start > end or start < 0
valid = false
{
start: start
end: end
}
)
arr.type = str.slice(0, i)
if valid then arr else -1
: (req, res, path, stat, cb) ->
len = stat.size
options = {}
opts = {}
ranges = req.headers.range
offset = options.start or 0
#debug('pipe "%s"', path);
# set header fields
Assets._setHeader res, path, stat
# set content-type
if res.getHeader('Content-Type')
cb null, true
return
type = mime.lookup(path)
charset = mime.charsets.lookup(type)
res.set 'Content-Type', type + (if charset then '; charset=' + charset else '')
#------------------------------------------------------------------------------------------//
# conditional GET support
if Assets.isConditionalGET(req) and Assets.isCachable(res) and Assets.isFresh(req, res, stat)
#console.log('ERROR: not modified ' + path);
cb null, true
return Assets.notModified(res)
#-----------------------------------------------------------------------------------------//
# adjust len to start/end options
len = Math.max(0, len - offset)
if options.end != undefined
bytes = options.end - offset + 1
if len > bytes
len = bytes
#-----------------------------------------------------------------------------------------//
# Range support
if ranges
ranges = Assets.rangeParser(len, ranges)
# If-Range support
if !Assets.isRangeFresh(req, res)
#debug('range stale');
ranges = -2
# unsatisfiable
if -1 == ranges
#debug('range unsatisfiable');
res.set 'Content-Range', 'bytes */' + stat.size
Assets.serverError 416
cb null, true
return
# valid (syntactically invalid/multiple ranges are treated as a regular response)
if -2 != ranges and ranges.length == 1
#debug('range %j', ranges);
# Content-Range
res.status 206
res.set 'Content-Range', 'bytes ' + ranges[0].start + '-' + ranges[0].end + '/' + len
offset += ranges[0].start
len = ranges[0].end - (ranges[0].start) + 1
#---------------------------------------------------------------------------------------//
# clone options
for prop of options
opts[prop] = options[prop]
#--------------------------------------------------------------------------------------//
# set read options
opts.start = offset
opts.end = Math.max(offset, offset + len - 1)
#---------------------------------------------------------------------------------------//
# content-length
res.set 'Content-Length', len
# HEAD support
if 'HEAD' == req.method
res.send res.statusCode
cb null, true
return
# res.updateHeaders res.statusCode
Assets.ServerStream req, res, path, opts, cb
return
: (res, path, stat) ->
if !res.get('Accept-Ranges')
res.set 'Accept-Ranges', 'bytes'
if !res.get('Date')
res.set 'Date', (new Date).toUTCString()
if !res.get('Cache-Control')
res.set 'Cache-Control', 'public, max-age=31536000'
if !res.get('Last-Modified')
modified = stat.mtime.toUTCString()
res.set 'Last-Modified', modified
if !res.get('ETag')
val = etag(stat)
res.set 'ETag', val
return
: (req) ->
req.headers['if-none-match'] or req.headers['if-modified-since']
: (res) ->
res.statusCode >= 200 and res.statusCode < 300 or 304 == res.statusCode
: (req, res, stat) ->
Assets.fresh req.headers, res._headers, stat
: (req, res, stat) ->
mtime = Date.parse(stat.mtime)
headers = {}
clientETag = req['if-none-match']
clientMTime = Date.parse(req['if-modified-since'])
length = stat.size
(clientMTime or clientETag) and (!clientETag or clientETag == etag(stat)) and (!clientMTime or clientMTime >= mtime)
: (res) ->
Assets.removeContentHeaderFields res
res.sendStatus 304
return
: (res) ->
headers = [
'Content-Encoding'
'Content-Language'
'Content-Length'
'Content-Location'
'Content-MD5'
'Content-Range'
'Content-Type'
'Expires'
'Last-Modified'
]
for i of headers
res.removeHeader headers[i]
return
: (req, res) ->
ifRange = req.headers['if-range']
if !ifRange
return true
if ~ifRange.indexOf('"') then ~ifRange.indexOf(req.headers.etag) else Date.parse(req.headers['last-modified']) <= Date.parse(ifRange)
: (res, status) ->
msg = http.STATUS_CODES[status]
res._headers = undefined
res.send status, msg
return
: (req, res, path, options, cb) ->
# TODO: this is all lame, refactor meeee
finished = false
# pipe
_stream = fs.createReadStream(path, options)
#this.emit('stream', _stream);
_stream.pipe res
# response finished, done with the fd
onFinished res, ->
finished = true
Assets._destroy _stream
cb null, 'assets:finish'
return
# error handling code-smell
_stream.on 'error', (err) ->
# request already finished
if finished
cb null, 'assets:already-finish'
return
# clean up stream
finished = true
Assets._destroy _stream
# error
#self.onStatError(err);
notfound = [
'ENOENT'
'ENAMETOOLONG'
'ENOTDIR'
]
if ~notfound.indexOf(err.code)
cb null, true
return Assets.serverError(404, err)
Assets.serverError res, 500, err
cb null, true
return
# end
_stream.on 'end', ->
#self.emit('end');
#res.send(res.statusCode);
return
do req.__response
return
: (stream) ->
if stream instanceof Assets.ReadStream
return Assets.destroyReadStream(stream)
if !(stream instanceof Stream)
return stream
if typeof stream.destroy == 'function'
stream.destroy()
stream
: (stream) ->
stream.destroy()
if typeof stream.close == 'function'
# node.js core bug work-around
stream.on 'open', Assets.onopenClose
stream
: ->
if typeof == 'number'
# actually close down the fd
return