UNPKG

arcane-middleware-assets

Version:

File Serving module for arcane

277 lines (248 loc) 7.78 kB
#!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: (@config, @global) -> 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}" @config.on 'components-delete', (path) -> delete components_map[path]; @config.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' @ReadStream: fs.ReadStream @rangeParser: (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 @RequestFileStaticFile: (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 @_setHeader: (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 @isConditionalGET: (req) -> req.headers['if-none-match'] or req.headers['if-modified-since'] @isCachable: (res) -> res.statusCode >= 200 and res.statusCode < 300 or 304 == res.statusCode @isFresh: (req, res, stat) -> Assets.fresh req.headers, res._headers, stat @fresh: (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) @notModified: (res) -> Assets.removeContentHeaderFields res res.sendStatus 304 return @removeContentHeaderFields: (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 @isRangeFresh: (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) @serverError: (res, status) -> msg = http.STATUS_CODES[status] res._headers = undefined res.send status, msg return @ServerStream: (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 @_destroy: (stream) -> if stream instanceof Assets.ReadStream return Assets.destroyReadStream(stream) if !(stream instanceof Stream) return stream if typeof stream.destroy == 'function' stream.destroy() stream @destroyReadStream: (stream) -> stream.destroy() if typeof stream.close == 'function' # node.js core bug work-around stream.on 'open', Assets.onopenClose stream @onopenClose: -> if typeof @fd == 'number' # actually close down the fd @close() return