st 1.1.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/st.js CHANGED
@@ -1,29 +1,23 @@
1
- module.exports = st
2
-
3
- st.Mount = Mount
4
-
5
- var mime = require('mime')
6
- var path = require('path')
7
- var fs
1
+ const mime = require('mime')
2
+ const path = require('path')
3
+ const url = require('url')
4
+ let fs
8
5
  try {
9
6
  fs = require('graceful-fs')
10
7
  } catch (e) {
11
8
  fs = require('fs')
12
9
  }
13
- var url = require('url')
14
- var zlib = require('zlib')
15
- var Neg = require('negotiator')
16
- var http = require('http')
17
- var AC = require('async-cache')
18
- var util = require('util')
19
- var FD = require('fd')
20
- var bl = require('bl')
21
-
22
- // default caching options
23
- var defaultCacheOptions = {
10
+ const zlib = require('zlib')
11
+ const Neg = require('negotiator')
12
+ const http = require('http')
13
+ const AC = require('async-cache')
14
+ const FD = require('fd')
15
+ const bl = require('bl')
16
+
17
+ const defaultCacheOptions = {
24
18
  fd: {
25
19
  max: 1000,
26
- maxAge: 1000 * 60 * 60,
20
+ maxAge: 1000 * 60 * 60
27
21
  },
28
22
  stat: {
29
23
  max: 5000,
@@ -31,29 +25,39 @@ var defaultCacheOptions = {
31
25
  },
32
26
  content: {
33
27
  max: 1024 * 1024 * 64,
34
- length: function (n) {
35
- return n.length
36
- },
28
+ length: (n) => n.length,
37
29
  maxAge: 1000 * 60 * 10
38
30
  },
39
31
  index: {
40
32
  max: 1024 * 8,
41
- length: function (n) {
42
- return n.length
43
- },
33
+ length: (n) => n.length,
44
34
  maxAge: 1000 * 60 * 10
45
35
  },
46
36
  readdir: {
47
37
  max: 1000,
48
- length: function (n) {
49
- return n.length
50
- },
38
+ length: (n) => n.length,
51
39
  maxAge: 1000 * 60 * 10
52
40
  }
53
41
  }
54
42
 
43
+ // lru-cache doesn't like when max=0, so we just pretend
44
+ // everything is really big. kind of a kludge, but easiest way
45
+ // to get it done
46
+ const none = {
47
+ max: 1,
48
+ length: () => Infinity
49
+ }
50
+
51
+ const noCaching = {
52
+ fd: none,
53
+ stat: none,
54
+ index: none,
55
+ readdir: none,
56
+ content: none
57
+ }
58
+
55
59
  function st (opt) {
56
- var p, u
60
+ let p, u
57
61
  if (typeof opt === 'string') {
58
62
  p = opt
59
63
  opt = arguments[1]
@@ -63,537 +67,592 @@ function st (opt) {
63
67
  }
64
68
  }
65
69
 
66
- if (!opt) opt = {}
67
- else opt = util._extend({}, opt)
70
+ if (!opt) {
71
+ opt = {}
72
+ } else {
73
+ opt = Object.assign({}, opt)
74
+ }
68
75
 
69
- if (!p) p = opt.path
70
- if (typeof p !== 'string') throw new Error('no path specified')
76
+ if (!p) {
77
+ p = opt.path
78
+ }
79
+ if (typeof p !== 'string') {
80
+ throw new Error('no path specified')
81
+ }
71
82
  p = path.resolve(p)
72
- if (!u) u = opt.url
73
- if (!u) u = ''
74
- if (u.charAt(0) !== '/') u = '/' + u
83
+ if (!u) {
84
+ u = opt.url
85
+ }
86
+ if (!u) {
87
+ u = ''
88
+ }
89
+ if (u.charAt(0) !== '/') {
90
+ u = '/' + u
91
+ }
75
92
 
76
93
  opt.url = u
77
94
  opt.path = p
78
95
 
79
- var m = new Mount(opt)
80
- var fn = m.serve.bind(m)
96
+ const m = new Mount(opt)
97
+ const fn = m.serve.bind(m)
81
98
  fn._this = m
82
99
  return fn
83
100
  }
84
101
 
85
- function Mount (opt) {
86
- if (!opt) throw new Error('no options provided')
87
- if (typeof opt !== 'object') throw new Error('invalid options')
88
- if (!(this instanceof Mount)) return new Mount(opt)
89
-
90
- this.opt = opt
91
- this.url = opt.url
92
- this.path = opt.path
93
- this._index = opt.index === false ? false
94
- : typeof opt.index === 'string' ? opt.index
95
- : true
96
- this.fdman = FD()
97
-
98
- // cache basically everything
99
- var c = this.getCacheOptions(opt)
100
- this.cache = {
101
- fd: AC(c.fd),
102
- stat: AC(c.stat),
103
- index: AC(c.index),
104
- readdir: AC(c.readdir),
105
- content: AC(c.content)
106
- }
102
+ class Mount {
103
+ constructor (opt) {
104
+ if (!opt) {
105
+ throw new Error('no options provided')
106
+ }
107
+ if (typeof opt !== 'object') {
108
+ throw new Error('invalid options')
109
+ }
110
+ if (!(this instanceof Mount)) {
111
+ return new Mount(opt)
112
+ }
107
113
 
108
- this._cacheControl =
109
- c.content.maxAge === false
110
- ? undefined
111
- : typeof c.content.cacheControl == 'string'
112
- ? c.content.cacheControl
113
- : opt.cache === false
114
- ? 'no-cache'
115
- : 'public, max-age=' + (c.content.maxAge / 1000)
116
- }
114
+ this.opt = opt
115
+ this.url = opt.url
116
+ this.path = opt.path
117
+ this._index = opt.index === false ? false
118
+ : typeof opt.index === 'string' ? opt.index
119
+ : true
120
+ this.fdman = FD()
121
+
122
+ // cache basically everything
123
+ const c = this.getCacheOptions(opt)
124
+ this.cache = {
125
+ fd: AC(c.fd),
126
+ stat: AC(c.stat),
127
+ index: AC(c.index),
128
+ readdir: AC(c.readdir),
129
+ content: AC(c.content)
130
+ }
117
131
 
118
- // lru-cache doesn't like when max=0, so we just pretend
119
- // everything is really big. kind of a kludge, but easiest way
120
- // to get it done
121
- var none = { max: 1, length: function() {
122
- return Infinity
123
- }}
124
- var noCaching = {
125
- fd: none,
126
- stat: none,
127
- index: none,
128
- readdir: none,
129
- content: none
130
- }
132
+ this._cacheControl =
133
+ c.content.maxAge === false
134
+ ? undefined
135
+ : typeof c.content.cacheControl === 'string'
136
+ ? c.content.cacheControl
137
+ : opt.cache === false
138
+ ? 'no-cache'
139
+ : 'public, max-age=' + (c.content.maxAge / 1000)
140
+ }
131
141
 
132
- Mount.prototype.getCacheOptions = function (opt) {
133
- var o = opt.cache
134
- , set = function (key) {
135
- return o[key] === false
136
- ? util._extend({}, none)
137
- : util._extend(util._extend({}, d[key]), o[key])
138
- }
142
+ getCacheOptions (opt) {
143
+ let o = opt.cache
144
+ const set = (key) => {
145
+ return o[key] === false
146
+ ? Object.assign({}, none)
147
+ : Object.assign(Object.assign({}, d[key]), o[key])
148
+ }
139
149
 
140
- if (o === false)
141
- o = noCaching
142
- else if (!o)
143
- o = {}
144
-
145
- var d = defaultCacheOptions
146
-
147
- // should really only ever set max and maxAge here.
148
- // load and fd disposal is important to control.
149
- var c = {
150
- fd: set('fd'),
151
- stat: set('stat'),
152
- index: set('index'),
153
- readdir: set('readdir'),
154
- content: set('content'),
155
- }
150
+ if (o === false) {
151
+ o = noCaching
152
+ } else if (!o) {
153
+ o = {}
154
+ }
156
155
 
157
- c.fd.dispose = this.fdman.close.bind(this.fdman)
158
- c.fd.load = this.fdman.open.bind(this.fdman)
156
+ const d = defaultCacheOptions
159
157
 
160
- c.stat.load = this._loadStat.bind(this)
161
- c.index.load = this._loadIndex.bind(this)
162
- c.readdir.load = this._loadReaddir.bind(this)
163
- c.content.load = this._loadContent.bind(this)
164
- return c
165
- }
158
+ // should really only ever set max and maxAge here.
159
+ // load and fd disposal is important to control.
160
+ const c = {
161
+ fd: set('fd'),
162
+ stat: set('stat'),
163
+ index: set('index'),
164
+ readdir: set('readdir'),
165
+ content: set('content')
166
+ }
166
167
 
167
- // get a path from a url
168
- Mount.prototype.getPath = function (u) {
169
- var p = url.parse(u).pathname
168
+ c.fd.dispose = this.fdman.close.bind(this.fdman)
169
+ c.fd.load = this.fdman.open.bind(this.fdman)
170
170
 
171
- // Encoded dots are dots
172
- p = p.replace(/%2e/ig, '.')
171
+ c.stat.load = this._loadStat.bind(this)
172
+ c.index.load = this._loadIndex.bind(this)
173
+ c.readdir.load = this._loadReaddir.bind(this)
174
+ c.content.load = this._loadContent.bind(this)
175
+ return c
176
+ }
173
177
 
174
- // encoded slashes are /
175
- p = p.replace(/%2f|%5c/ig, '/')
178
+ // get the path component from a URI
179
+ getUriPath (u) {
180
+ let p = url.parse(u).pathname // eslint-disable-line
176
181
 
177
- // back slashes are slashes
178
- p = p.replace(/[\/\\]/g, '/')
182
+ // Encoded dots are dots
183
+ p = p.replace(/%2e/ig, '.')
179
184
 
180
- // Make sure it starts with a slash
181
- p = p.replace(/^\//, '/')
185
+ // encoded slashes are /
186
+ p = p.replace(/%2f|%5c/ig, '/')
182
187
 
183
- if (p.match(/[\/\\]\.\.[\/\\]/)) {
184
- // traversal urls not ever even slightly allowed. clearly shenanigans
185
- // send a 403 on that noise, do not pass go, do not collect $200
186
- return 403
187
- }
188
+ // back slashes are slashes
189
+ p = p.replace(/[/\\]/g, '/')
188
190
 
189
- u = path.normalize(p).replace(/\\/g, '/')
190
- if (u.indexOf(this.url) !== 0) return false
191
+ // Make sure it starts with a slash
192
+ p = p.replace(/^\//, '/')
193
+ if ((/[/\\]\.\.([/\\]|$)/).test(p)) {
194
+ // traversal urls not ever even slightly allowed. clearly shenanigans
195
+ // send a 403 on that noise, do not pass go, do not collect $200
196
+ return 403
197
+ }
191
198
 
192
- try {
193
- u = decodeURIComponent(u)
194
- }
195
- catch (e) {
196
- // if decodeURIComponent failed, we weren't given a valid URL to begin with.
197
- return false
198
- }
199
+ u = path.normalize(p).replace(/\\/g, '/')
200
+ if (u.indexOf(this.url) !== 0) {
201
+ return false
202
+ }
199
203
 
200
- // /a/b/c mounted on /path/to/z/d/x
201
- // /a/b/c/d --> /path/to/z/d/x/d
202
- u = u.substr(this.url.length)
203
- if (u.charAt(0) !== '/') u = '/' + u
204
+ try {
205
+ u = decodeURIComponent(u)
206
+ } catch (e) {
207
+ // if decodeURIComponent failed, we weren't given a valid URL to begin with.
208
+ return false
209
+ }
204
210
 
205
- p = path.join(this.path, u)
206
- return p
207
- }
211
+ // /a/b/c mounted on /path/to/z/d/x
212
+ // /a/b/c/d --> /path/to/z/d/x/d
213
+ u = u.substr(this.url.length)
214
+ if (u.charAt(0) !== '/') {
215
+ u = '/' + u
216
+ }
208
217
 
209
- // get a url from a path
210
- Mount.prototype.getUrl = function (p) {
211
- p = path.resolve(p)
212
- if (p.indexOf(this.path) !== 0) return false
213
- p = path.join('/', p.substr(this.path.length))
214
- var u = path.join(this.url, p).replace(/\\/g, '/')
215
- return u
216
- }
218
+ return u
219
+ }
217
220
 
218
- Mount.prototype.serve = function (req, res, next) {
219
- if (req.method !== 'HEAD' && req.method !== 'GET') {
220
- if (typeof next === 'function') next()
221
- return false
221
+ // get a path from a url
222
+ getPath (u) {
223
+ return path.join(this.path, u)
222
224
  }
223
225
 
224
- // querystrings are of no concern to us
225
- if (!req.sturl)
226
- req.sturl = url.parse(req.url).pathname
226
+ // get a url from a path
227
+ getUrl (p) {
228
+ p = path.resolve(p)
229
+ if (p.indexOf(this.path) !== 0) {
230
+ return false
231
+ }
232
+ p = path.join('/', p.substr(this.path.length))
233
+ const u = path.join(this.url, p).replace(/\\/g, '/')
234
+ return u
235
+ }
227
236
 
228
- var p = this.getPath(req.sturl)
237
+ serve (req, res, next) {
238
+ if (req.method !== 'HEAD' && req.method !== 'GET') {
239
+ if (typeof next === 'function') {
240
+ next()
241
+ }
242
+ return false
243
+ }
229
244
 
230
- // Falsey here means we got some kind of invalid path.
231
- // Probably urlencoding we couldn't understand, or some
232
- // other "not compatible with st, but maybe ok" thing.
233
- if (!p) {
234
- if (typeof next === 'function') next()
235
- return false
236
- }
245
+ // querystrings are of no concern to us
246
+ if (!req.sturl) {
247
+ req.sturl = this.getUriPath(req.url)
248
+ }
237
249
 
238
- // don't allow dot-urls by default, unless explicitly allowed.
239
- // If we got a 403, then it's explicitly forbidden.
240
- if (p === 403 || !this.opt.dot && req.sturl.match(/(^|\/)\./)) {
241
- res.statusCode = 403
242
- res.end('Forbidden')
243
- return true
244
- }
250
+ // don't allow dot-urls by default, unless explicitly allowed.
251
+ // If we got a 403, then it's explicitly forbidden.
252
+ if (req.sturl === 403 || (!this.opt.dot && (/(^|\/)\./).test(req.sturl))) {
253
+ res.statusCode = 403
254
+ res.end('Forbidden')
255
+ return true
256
+ }
245
257
 
246
- // now we have a path. check for the fd.
247
- this.cache.fd.get(p, function (er, fd) {
248
- // inability to open is some kind of error, probably 404
249
- // if we're in passthrough, AND got a next function, we can
250
- // fall through to that. otherwise, we already returned true,
251
- // send an error.
252
- if (er) {
253
- if (this.opt.passthrough === true && er.code === 'ENOENT' && next)
254
- return next()
255
- return this.error(er, res)
258
+ // Falsey here means we got some kind of invalid path.
259
+ // Probably urlencoding we couldn't understand, or some
260
+ // other "not compatible with st, but maybe ok" thing.
261
+ if (typeof req.sturl !== 'string' || req.sturl === '') {
262
+ if (typeof next === 'function') {
263
+ next()
264
+ }
265
+ return false
256
266
  }
257
267
 
258
- // we may be about to use this, so don't let it be closed by cache purge
259
- this.fdman.checkout(p, fd)
260
- // a safe end() function that can be called multiple times but
261
- // only perform a single checkin
262
- var end = this.fdman.checkinfn(p, fd)
268
+ const p = this.getPath(req.sturl)
263
269
 
264
- this.cache.stat.get(fd+':'+p, function (er, stat) {
270
+ // now we have a path. check for the fd.
271
+ this.cache.fd.get(p, (er, fd) => {
272
+ // inability to open is some kind of error, probably 404
273
+ // if we're in passthrough, AND got a next function, we can
274
+ // fall through to that. otherwise, we already returned true,
275
+ // send an error.
265
276
  if (er) {
266
- if (next && this.opt.passthrough === true && this._index === false) {
277
+ if (this.opt.passthrough === true && er.code === 'ENOENT' && next) {
267
278
  return next()
268
279
  }
269
- end()
270
280
  return this.error(er, res)
271
281
  }
272
282
 
273
- var isDirectory = stat.isDirectory()
274
-
275
- if (isDirectory) {
276
- end() // we won't need this fd for a directory in any case
277
- if (next && this.opt.passthrough === true && this._index === false) {
278
- // this is done before if-modified-since and if-non-match checks so
279
- // cached modified and etag values won't return 304's if we've since
280
- // switched to !index. See Issue #51.
281
- return next()
283
+ // we may be about to use this, so don't let it be closed by cache purge
284
+ this.fdman.checkout(p, fd)
285
+ // a safe end() function that can be called multiple times but
286
+ // only perform a single checkin
287
+ const end = this.fdman.checkinfn(p, fd)
288
+
289
+ this.cache.stat.get(fd + ':' + p, (er, stat) => {
290
+ if (er) {
291
+ if (next && this.opt.passthrough === true && this._index === false) {
292
+ return next()
293
+ }
294
+ end()
295
+ return this.error(er, res)
282
296
  }
283
- }
284
297
 
285
- var ims = req.headers['if-modified-since']
286
- if (ims) ims = new Date(ims).getTime()
287
- if (ims && ims >= stat.mtime.getTime()) {
288
- res.statusCode = 304
289
- res.end()
290
- return end()
291
- }
298
+ const isDirectory = stat.isDirectory()
292
299
 
293
- var etag = getEtag(stat)
294
- if (req.headers['if-none-match'] === etag) {
295
- res.statusCode = 304
296
- res.end()
297
- return end()
298
- }
300
+ if (isDirectory) {
301
+ end() // we won't need this fd for a directory in any case
302
+ if (next && this.opt.passthrough === true && this._index === false) {
303
+ // this is done before if-modified-since and if-non-match checks so
304
+ // cached modified and etag values won't return 304's if we've since
305
+ // switched to !index. See Issue #51.
306
+ return next()
307
+ }
308
+ }
299
309
 
300
- // only set headers once we're sure we'll be serving this request
301
- if (!res.getHeader('cache-control') && this._cacheControl)
302
- res.setHeader('cache-control', this._cacheControl)
303
- res.setHeader('last-modified', stat.mtime.toUTCString())
304
- res.setHeader('etag', etag)
310
+ let ims = req.headers['if-modified-since']
311
+ if (ims) {
312
+ ims = new Date(ims).getTime()
313
+ }
314
+ if (ims && ims >= stat.mtime.getTime()) {
315
+ res.statusCode = 304
316
+ res.end()
317
+ return end()
318
+ }
305
319
 
306
- if (this.opt.cors) {
307
- res.setHeader('Access-Control-Allow-Origin', '*')
308
- res.setHeader('Access-Control-Allow-Headers',
309
- 'Origin, X-Requested-With, Content-Type, Accept, Range')
310
- }
320
+ const etag = getEtag(stat)
321
+ if (req.headers['if-none-match'] === etag) {
322
+ res.statusCode = 304
323
+ res.end()
324
+ return end()
325
+ }
311
326
 
312
- return isDirectory
313
- ? this.index(p, req, res)
314
- : this.file(p, fd, stat, etag, req, res, end)
315
- }.bind(this))
316
- }.bind(this))
327
+ // only set headers once we're sure we'll be serving this request
328
+ if (!res.getHeader('cache-control') && this._cacheControl) {
329
+ res.setHeader('cache-control', this._cacheControl)
330
+ }
331
+ res.setHeader('last-modified', stat.mtime.toUTCString())
332
+ res.setHeader('etag', etag)
317
333
 
318
- return true
319
- }
334
+ if (this.opt.cors) {
335
+ res.setHeader('Access-Control-Allow-Origin', '*')
336
+ res.setHeader('Access-Control-Allow-Headers',
337
+ 'Origin, X-Requested-With, Content-Type, Accept, Range')
338
+ }
320
339
 
321
- Mount.prototype.error = function (er, res) {
322
- res.statusCode = typeof er === 'number' ? er
323
- : er.code === 'ENOENT' || er.code === 'EISDIR' ? 404
324
- : er.code === 'EPERM' || er.code === 'EACCES' ? 403
325
- : 500
340
+ return isDirectory
341
+ ? this.index(p, req, res)
342
+ : this.file(p, fd, stat, etag, req, res, end)
343
+ })
344
+ })
326
345
 
327
- if (typeof res.error === 'function') {
328
- // pattern of express and ErrorPage
329
- return res.error(res.statusCode, er)
346
+ return true
330
347
  }
331
348
 
332
- res.setHeader('content-type', 'text/plain')
333
- res.end(http.STATUS_CODES[res.statusCode] + '\n')
334
- }
349
+ error (er, res) {
350
+ res.statusCode = typeof er === 'number' ? er
351
+ : er.code === 'ENOENT' || er.code === 'EISDIR' ? 404
352
+ : er.code === 'EPERM' || er.code === 'EACCES' ? 403
353
+ : 500
335
354
 
336
- Mount.prototype.index = function (p, req, res) {
337
- if (this._index === true) {
338
- return this.autoindex(p, req, res)
339
- }
340
- if (typeof this._index === 'string') {
341
- if (!/\/$/.test(req.sturl)) req.sturl += '/'
342
- req.sturl += this._index
343
- return this.serve(req, res)
344
- }
345
- return this.error(404, res)
346
- }
355
+ if (typeof res.error === 'function') {
356
+ // pattern of express and ErrorPage
357
+ return res.error(res.statusCode, er)
358
+ }
347
359
 
348
- Mount.prototype.autoindex = function (p, req, res) {
349
- if (!/\/$/.exec(req.sturl)) {
350
- res.statusCode = 301
351
- res.setHeader('location', req.sturl + '/')
352
- res.end('Moved: ' + req.sturl + '/')
353
- return
360
+ res.setHeader('content-type', 'text/plain')
361
+ res.end(http.STATUS_CODES[res.statusCode] + '\n')
354
362
  }
355
363
 
356
- this.cache.index.get(p, function (er, html) {
357
- if (er) return this.error(er, res)
358
-
359
- res.statusCode = 200
360
- res.setHeader('content-type', 'text/html')
361
- res.setHeader('content-length', html.length)
362
- res.end(html)
363
- }.bind(this))
364
- }
364
+ index (p, req, res) {
365
+ if (this._index === true) {
366
+ return this.autoindex(p, req, res)
367
+ }
368
+ if (typeof this._index === 'string') {
369
+ if (!/\/$/.test(req.sturl)) {
370
+ req.sturl += '/'
371
+ }
372
+ req.sturl += this._index
373
+ return this.serve(req, res)
374
+ }
375
+ return this.error(404, res)
376
+ }
365
377
 
378
+ autoindex (p, req, res) {
379
+ if (!/\/$/.exec(req.sturl)) {
380
+ res.statusCode = 301
381
+ res.setHeader('location', req.sturl + '/')
382
+ res.end('Moved: ' + req.sturl + '/')
383
+ return
384
+ }
366
385
 
367
- Mount.prototype.file = function (p, fd, stat, etag, req, res, end) {
368
- var key = stat.size + ':' + etag
386
+ this.cache.index.get(p, (er, html) => {
387
+ if (er) {
388
+ return this.error(er, res)
389
+ }
369
390
 
370
- var mt = mime.lookup(path.extname(p))
371
- if (mt !== 'application/octet-stream') {
372
- res.setHeader('content-type', mt)
391
+ res.statusCode = 200
392
+ res.setHeader('content-type', 'text/html')
393
+ res.setHeader('content-length', html.length)
394
+ res.end(html)
395
+ })
373
396
  }
374
397
 
375
- // only use the content cache if it will actually fit there.
376
- if (this.cache.content.has(key)) {
377
- end()
378
- this.cachedFile(p, stat, etag, req, res)
379
- } else {
380
- this.streamFile(p, fd, stat, etag, req, res, end)
381
- }
382
- }
398
+ file (p, fd, stat, etag, req, res, end) {
399
+ const key = stat.size + ':' + etag
383
400
 
384
- Mount.prototype.cachedFile = function (p, stat, etag, req, res) {
385
- var key = stat.size + ':' + etag
386
- var gz = this.opt.gzip !== false && getGz(p, req)
401
+ const mt = mime.getType(path.extname(p))
402
+ if (mt !== 'application/octet-stream') {
403
+ res.setHeader('content-type', mt)
404
+ }
387
405
 
388
- this.cache.content.get(key, function (er, content) {
389
- if (er) return this.error(er, res)
390
- res.statusCode = 200
391
- if (this.opt.cachedHeader)
392
- res.setHeader('x-from-cache', 'true')
393
- if (gz && content.gz) {
394
- res.setHeader('content-encoding', 'gzip')
395
- res.setHeader('content-length', content.gz.length)
396
- res.end(content.gz)
406
+ // only use the content cache if it will actually fit there.
407
+ if (this.cache.content.has(key)) {
408
+ end()
409
+ this.cachedFile(p, stat, etag, req, res)
397
410
  } else {
398
- res.setHeader('content-length', content.length)
399
- res.end(content)
411
+ this.streamFile(p, fd, stat, etag, req, res, end)
400
412
  }
401
- }.bind(this))
402
- }
413
+ }
403
414
 
404
- Mount.prototype.streamFile = function (p, fd, stat, etag, req, res, end) {
405
- var streamOpt = { fd: fd, start: 0, end: stat.size }
406
- var stream = fs.createReadStream(p, streamOpt)
407
- stream.destroy = function () {}
415
+ cachedFile (p, stat, etag, req, res) {
416
+ const key = stat.size + ':' + etag
417
+ const gz = this.opt.gzip !== false && getGz(p, req)
408
418
 
409
- // gzip only if not explicitly turned off or client doesn't accept it
410
- var gzOpt = this.opt.gzip !== false
411
- var gz = gzOpt && getGz(p, req)
412
- var cachable = this.cache.content._cache.max > stat.size
413
- var gzstr
419
+ this.cache.content.get(key, (er, content) => {
420
+ if (er) {
421
+ return this.error(er, res)
422
+ }
423
+ res.statusCode = 200
424
+ if (this.opt.cachedHeader) {
425
+ res.setHeader('x-from-cache', 'true')
426
+ }
427
+ if (gz && content.gz) {
428
+ res.setHeader('content-encoding', 'gzip')
429
+ res.setHeader('content-length', content.gz.length)
430
+ res.end(content.gz)
431
+ } else {
432
+ res.setHeader('content-length', content.length)
433
+ res.end(content)
434
+ }
435
+ })
436
+ }
414
437
 
415
- // need a gzipped version for the cache, so do it regardless of what the client wants
416
- if (gz || (gzOpt && cachable)) gzstr = zlib.Gzip()
438
+ streamFile (p, fd, stat, etag, req, res, end) {
439
+ const streamOpt = { fd: fd, start: 0, end: stat.size }
440
+ let stream = fs.createReadStream(p, streamOpt)
441
+ stream.destroy = () => {}
417
442
 
418
- // too late to effectively handle any errors.
419
- // just kill the connection if that happens.
420
- stream.on('error', function(e) {
421
- console.error('Error serving %s fd=%d\n%s', p, fd, e.stack || e.message)
422
- res.socket.destroy()
423
- end()
424
- })
443
+ // gzip only if not explicitly turned off or client doesn't accept it
444
+ const gzOpt = this.opt.gzip !== false
445
+ const gz = gzOpt && getGz(p, req)
446
+ const cachable = this.cache.content._cache.max > stat.size
447
+ let gzstr
425
448
 
426
- if (res.filter) stream = stream.pipe(res.filter)
449
+ // need a gzipped version for the cache, so do it regardless of what the client wants
450
+ if (gz || (gzOpt && cachable)) {
451
+ gzstr = zlib.Gzip()
452
+ }
427
453
 
428
- res.statusCode = 200
454
+ // too late to effectively handle any errors.
455
+ // just kill the connection if that happens.
456
+ stream.on('error', (e) => {
457
+ console.error('Error serving %s fd=%d\n%s', p, fd, e.stack || e.message)
458
+ res.socket.destroy()
459
+ end()
460
+ })
429
461
 
430
- if (gz) {
431
- // we don't know how long it'll be, since it will be compressed.
432
- res.setHeader('content-encoding', 'gzip')
433
- stream.pipe(gzstr).pipe(res)
434
- } else {
435
- if (!res.filter) res.setHeader('content-length', stat.size)
436
- stream.pipe(res)
437
- if (gzstr)
438
- stream.pipe(gzstr) // for cache
439
- }
462
+ if (res.filter) {
463
+ stream = stream.pipe(res.filter)
464
+ }
440
465
 
441
- stream.on('end', function () {
442
- process.nextTick(end)
443
- })
466
+ res.statusCode = 200
444
467
 
445
- if (cachable) {
446
- // collect it, and put it in the cache
468
+ if (gz) {
469
+ // we don't know how long it'll be, since it will be compressed.
470
+ res.setHeader('content-encoding', 'gzip')
471
+ stream.pipe(gzstr).pipe(res)
472
+ } else {
473
+ if (!res.filter) {
474
+ res.setHeader('content-length', stat.size)
475
+ }
476
+ stream.pipe(res)
477
+ if (gzstr) {
478
+ stream.pipe(gzstr)
479
+ } // for cache
480
+ }
447
481
 
448
- var calls = 0
482
+ stream.on('end', () => process.nextTick(end))
449
483
 
450
- // called by bl() for both the raw stream and gzipped stream if we're
451
- // caching gzipped data
452
- var collectEnd = function () {
453
- if (++calls == (gzOpt ? 2 : 1)) {
454
- var content = bufs.slice()
455
- content.gz = gzbufs && gzbufs.slice()
456
- this.cache.content.set(key, content)
484
+ if (cachable) {
485
+ // collect it, and put it in the cache
486
+
487
+ let calls = 0
488
+
489
+ // called by bl() for both the raw stream and gzipped stream if we're
490
+ // caching gzipped data
491
+ const collectEnd = () => {
492
+ if (++calls === (gzOpt ? 2 : 1)) {
493
+ const content = bufs.slice()
494
+ content.gz = gzbufs && gzbufs.slice()
495
+ this.cache.content.set(key, content)
496
+ }
457
497
  }
458
- }.bind(this)
459
498
 
460
- var key = stat.size + ':' + etag
461
- var bufs = bl(collectEnd)
462
- var gzbufs
499
+ const key = stat.size + ':' + etag
500
+ const bufs = bl(collectEnd)
501
+ let gzbufs
463
502
 
464
- stream.pipe(bufs)
503
+ stream.pipe(bufs)
465
504
 
466
- if (gzstr) {
467
- gzbufs = bl(collectEnd)
468
- gzstr.pipe(gzbufs)
505
+ if (gzstr) {
506
+ gzbufs = bl(collectEnd)
507
+ gzstr.pipe(gzbufs)
508
+ }
469
509
  }
470
510
  }
471
- }
472
511
 
512
+ // cache-fillers
473
513
 
474
- // cache-fillers
475
-
476
- Mount.prototype._loadIndex = function (p, cb) {
477
- // truncate off the first bits
478
- var url = p.substr(this.path.length).replace(/\\/g, '/')
479
- var t = url
514
+ _loadIndex (p, cb) {
515
+ // truncate off the first bits
516
+ const url = p.substr(this.path.length).replace(/\\/g, '/')
517
+ const t = url
480
518
  .replace(/"/g, '"')
481
519
  .replace(/</g, '&lt;')
482
520
  .replace(/>/g, '&gt;')
483
521
  .replace(/'/g, '&#39;')
484
522
 
485
- var str =
486
- '<!doctype html>' +
487
- '<html>' +
488
- '<head><title>Index of ' + t + '</title></head>' +
489
- '<body>' +
490
- '<h1>Index of ' + t + '</h1>' +
491
- '<hr><pre><a href="../">../</a>\n'
523
+ let str =
524
+ '<!doctype html>' +
525
+ '<html>' +
526
+ '<head><title>Index of ' + t + '</title></head>' +
527
+ '<body>' +
528
+ '<h1>Index of ' + t + '</h1>' +
529
+ '<hr><pre><a href="../">../</a>\n'
492
530
 
493
- this.cache.readdir.get(p, function (er, data) {
494
- if (er) return cb(er)
531
+ this.cache.readdir.get(p, (er, data) => {
532
+ if (er) {
533
+ return cb(er)
534
+ }
495
535
 
496
- var nameLen = 0
497
- var sizeLen = 0
536
+ let nameLen = 0
537
+ let sizeLen = 0
498
538
 
499
- Object.keys(data).map(function (f) {
500
- var d = data[f]
539
+ Object.keys(data).map((f) => {
540
+ const d = data[f]
501
541
 
502
- var name = f
542
+ let name = f
503
543
  .replace(/"/g, '&quot;')
504
544
  .replace(/</g, '&lt;')
505
545
  .replace(/>/g, '&gt;')
506
546
  .replace(/'/g, '&#39;')
507
547
 
508
- if (d.size === '-') name += '/'
509
- var showName = name.replace(/^(.{40}).{3,}$/, '$1..>')
510
- var linkName = encodeURIComponent(name)
511
- .replace(/%2e/ig, '.') // Encoded dots are dots
512
- .replace(/%2f|%5c/ig, '/') // encoded slashes are /
513
- .replace(/[\/\\]/g, '/') // back slashes are slashes
514
-
515
- nameLen = Math.max(nameLen, showName.length)
516
- sizeLen = Math.max(sizeLen, ('' + d.size).length)
517
- return [ '<a href="' + linkName + '">' + showName + '</a>',
518
- d.mtime, d.size, showName ]
519
- }).sort(function (a, b) {
520
- return a[2] === '-' && b[2] !== '-' ? -1 // dirs first
521
- : a[2] !== '-' && b[2] === '-' ? 1
522
- : a[0].toLowerCase() < b[0].toLowerCase() ? -1 // then alpha
523
- : a[0].toLowerCase() > b[0].toLowerCase() ? 1
524
- : 0
525
- }).forEach(function (line) {
526
- var namePad = new Array(8 + nameLen - line[3].length).join(' ')
527
- var sizePad = new Array(8 + sizeLen - ('' + line[2]).length).join(' ')
528
- str += line[0] + namePad +
529
- line[1].toISOString() +
530
- sizePad + line[2] + '\n'
548
+ if (d.size === '-') {
549
+ name += '/'
550
+ }
551
+ const showName = name.replace(/^(.{40}).{3,}$/, '$1..>')
552
+ const linkName = encodeURIComponent(name)
553
+ .replace(/%2e/ig, '.') // Encoded dots are dots
554
+ .replace(/%2f|%5c/ig, '/') // encoded slashes are /
555
+ .replace(/[/\\]/g, '/') // back slashes are slashes
556
+
557
+ nameLen = Math.max(nameLen, showName.length)
558
+ sizeLen = Math.max(sizeLen, ('' + d.size).length)
559
+ return ['<a href="' + linkName + '">' + showName + '</a>',
560
+ d.mtime, d.size, showName]
561
+ }).sort((a, b) => {
562
+ return a[2] === '-' && b[2] !== '-' ? -1 // dirs first
563
+ : a[2] !== '-' && b[2] === '-' ? 1
564
+ : a[0].toLowerCase() < b[0].toLowerCase() ? -1 // then alpha
565
+ : a[0].toLowerCase() > b[0].toLowerCase() ? 1
566
+ : 0
567
+ }).forEach((line) => {
568
+ const namePad = new Array(8 + nameLen - line[3].length).join(' ')
569
+ const sizePad = new Array(8 + sizeLen - ('' + line[2]).length).join(' ')
570
+ str += line[0] + namePad +
571
+ line[1].toISOString() +
572
+ sizePad + line[2] + '\n'
573
+ })
574
+
575
+ str += '</pre><hr></body></html>'
576
+ cb(null, Buffer.from(str))
531
577
  })
578
+ }
532
579
 
533
- str += '</pre><hr></body></html>'
534
- cb(null, new Buffer(str))
535
- })
536
- }
537
-
538
- Mount.prototype._loadReaddir = function (p, cb) {
539
- var len
540
- var data
541
- fs.readdir(p, function (er, files) {
542
- if (er) return cb(er)
543
- files = files.filter(function (f) {
544
- if (!this.opt.dot) return !/^\./.test(f)
545
- else return f !== '.' && f !== '..'
546
- }.bind(this))
547
- len = files.length
548
- data = {}
549
- files.forEach(function (file) {
550
- var pf = path.join(p, file)
551
- this.cache.stat.get(pf, function (er, stat) {
552
- if (er) return cb(er)
553
- if (stat.isDirectory()) stat.size = '-'
554
- data[file] = stat
555
- next()
556
- }.bind(this))
557
- }.bind(this))
558
- }.bind(this))
580
+ _loadReaddir (p, cb) {
581
+ let len
582
+ let data
583
+ fs.readdir(p, (er, files) => {
584
+ if (er) {
585
+ return cb(er)
586
+ }
587
+ files = files.filter((f) => {
588
+ if (!this.opt.dot) {
589
+ return !/^\./.test(f)
590
+ } else {
591
+ return f !== '.' && f !== '..'
592
+ }
593
+ })
594
+ len = files.length
595
+ data = {}
596
+ files.forEach((file) => {
597
+ const pf = path.join(p, file)
598
+ this.cache.stat.get(pf, (er, stat) => {
599
+ if (er) {
600
+ return cb(er)
601
+ }
602
+ if (stat.isDirectory()) {
603
+ stat.size = '-'
604
+ }
605
+ data[file] = stat
606
+ next()
607
+ })
608
+ })
609
+ })
559
610
 
560
- function next () {
561
- if (--len === 0) cb(null, data)
611
+ const next = () => {
612
+ if (--len === 0) {
613
+ cb(null, data)
614
+ }
615
+ }
562
616
  }
563
- }
564
617
 
565
- Mount.prototype._loadStat = function (key, cb) {
566
- // key is either fd:path or just a path
567
- var fdp = key.match(/^(\d+):(.*)/)
568
- if (fdp) {
569
- var fd = +fdp[1]
570
- var p = fdp[2]
571
- fs.fstat(fd, function (er, stat) {
572
- if (er) return cb(er)
573
- this.cache.stat.set(p, stat)
574
- cb(null, stat)
575
- }.bind(this))
576
- } else {
577
- fs.stat(key, cb)
618
+ _loadStat (key, cb) {
619
+ // key is either fd:path or just a path
620
+ const fdp = key.match(/^(\d+):(.*)/)
621
+ if (fdp) {
622
+ const fd = +fdp[1]
623
+ const p = fdp[2]
624
+ fs.fstat(fd, (er, stat) => {
625
+ if (er) {
626
+ return cb(er)
627
+ }
628
+ this.cache.stat.set(p, stat)
629
+ cb(null, stat)
630
+ })
631
+ } else {
632
+ fs.stat(key, cb)
633
+ }
578
634
  }
579
- }
580
635
 
581
- Mount.prototype._loadContent = function () {
582
- // this function should never be called.
583
- // we check if the thing is in the cache, and if not, stream it in
584
- // manually. this.cache.content.get() should not ever happen.
585
- throw new Error('This should not ever happen')
636
+ _loadContent () {
637
+ // this function should never be called.
638
+ // we check if the thing is in the cache, and if not, stream it in
639
+ // manually. this.cache.content.get() should not ever happen.
640
+ throw new Error('This should not ever happen')
641
+ }
586
642
  }
587
643
 
588
644
  function getEtag (s) {
589
645
  return '"' + s.dev + '-' + s.ino + '-' + s.mtime.getTime() + '"'
590
646
  }
591
647
 
592
- function getGz (p,req) {
593
- var gz = false
648
+ function getGz (p, req) {
649
+ let gz = false
594
650
  if (!/\.t?gz$/.exec(p)) {
595
- var neg = req.negotiator || new Neg(req)
651
+ const neg = req.negotiator || new Neg(req)
596
652
  gz = neg.preferredEncoding(['gzip', 'identity']) === 'gzip'
597
653
  }
598
654
  return gz
599
655
  }
656
+
657
+ module.exports = st
658
+ module.exports.Mount = Mount