st 1.2.2 → 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,540 +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
- }
107
-
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
- }
117
-
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
- }
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
+ }
131
113
 
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
- }
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
+ }
139
131
 
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'),
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)
155
140
  }
156
141
 
157
- c.fd.dispose = this.fdman.close.bind(this.fdman)
158
- c.fd.load = this.fdman.open.bind(this.fdman)
159
-
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
- }
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
+ }
166
149
 
167
- // get the path component from a URI
168
- Mount.prototype.getUriPath = function (u) {
169
- var p = url.parse(u).pathname
150
+ if (o === false) {
151
+ o = noCaching
152
+ } else if (!o) {
153
+ o = {}
154
+ }
170
155
 
171
- // Encoded dots are dots
172
- p = p.replace(/%2e/ig, '.')
156
+ const d = defaultCacheOptions
173
157
 
174
- // encoded slashes are /
175
- p = p.replace(/%2f|%5c/ig, '/')
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
+ }
176
167
 
177
- // back slashes are slashes
178
- p = p.replace(/[\/\\]/g, '/')
168
+ c.fd.dispose = this.fdman.close.bind(this.fdman)
169
+ c.fd.load = this.fdman.open.bind(this.fdman)
179
170
 
180
- // Make sure it starts with a slash
181
- p = p.replace(/^\//, '/')
182
- if ((/[\/\\]\.\.([\/\\]|$)/).test(p)) {
183
- // traversal urls not ever even slightly allowed. clearly shenanigans
184
- // send a 403 on that noise, do not pass go, do not collect $200
185
- return 403
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
186
176
  }
187
177
 
188
- u = path.normalize(p).replace(/\\/g, '/')
189
- if (u.indexOf(this.url) !== 0) return false
178
+ // get the path component from a URI
179
+ getUriPath (u) {
180
+ let p = url.parse(u).pathname // eslint-disable-line
190
181
 
191
- try {
192
- u = decodeURIComponent(u)
193
- }
194
- catch (e) {
195
- // if decodeURIComponent failed, we weren't given a valid URL to begin with.
196
- return false
197
- }
182
+ // Encoded dots are dots
183
+ p = p.replace(/%2e/ig, '.')
198
184
 
199
- // /a/b/c mounted on /path/to/z/d/x
200
- // /a/b/c/d --> /path/to/z/d/x/d
201
- u = u.substr(this.url.length)
202
- if (u.charAt(0) !== '/') u = '/' + u
185
+ // encoded slashes are /
186
+ p = p.replace(/%2f|%5c/ig, '/')
203
187
 
204
- return u
205
- }
188
+ // back slashes are slashes
189
+ p = p.replace(/[/\\]/g, '/')
206
190
 
207
- // get a path from a url
208
- Mount.prototype.getPath = function (u) {
209
- return path.join(this.path, u)
210
- }
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
+ }
211
198
 
212
- // get a url from a path
213
- Mount.prototype.getUrl = function (p) {
214
- p = path.resolve(p)
215
- if (p.indexOf(this.path) !== 0) return false
216
- p = path.join('/', p.substr(this.path.length))
217
- var u = path.join(this.url, p).replace(/\\/g, '/')
218
- return u
219
- }
199
+ u = path.normalize(p).replace(/\\/g, '/')
200
+ if (u.indexOf(this.url) !== 0) {
201
+ return false
202
+ }
220
203
 
221
- Mount.prototype.serve = function (req, res, next) {
222
- if (req.method !== 'HEAD' && req.method !== 'GET') {
223
- if (typeof next === 'function') next()
224
- return false
225
- }
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
+ }
226
210
 
227
- // querystrings are of no concern to us
228
- if (!req.sturl)
229
- req.sturl = this.getUriPath(req.url)
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
+ }
230
217
 
231
- // don't allow dot-urls by default, unless explicitly allowed.
232
- // If we got a 403, then it's explicitly forbidden.
233
- if (req.sturl === 403 || (!this.opt.dot && (/(^|\/)\./).test(req.sturl))) {
234
- res.statusCode = 403
235
- res.end('Forbidden')
236
- return true
218
+ return u
237
219
  }
238
220
 
239
- // Falsey here means we got some kind of invalid path.
240
- // Probably urlencoding we couldn't understand, or some
241
- // other "not compatible with st, but maybe ok" thing.
242
- if (typeof req.sturl !== 'string' || req.sturl == '') {
243
- if (typeof next === 'function') next()
244
- return false
221
+ // get a path from a url
222
+ getPath (u) {
223
+ return path.join(this.path, u)
245
224
  }
246
225
 
247
- var p = this.getPath(req.sturl)
248
-
249
- // now we have a path. check for the fd.
250
- this.cache.fd.get(p, function (er, fd) {
251
- // inability to open is some kind of error, probably 404
252
- // if we're in passthrough, AND got a next function, we can
253
- // fall through to that. otherwise, we already returned true,
254
- // send an error.
255
- if (er) {
256
- if (this.opt.passthrough === true && er.code === 'ENOENT' && next)
257
- return next()
258
- return this.error(er, res)
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
259
231
  }
232
+ p = path.join('/', p.substr(this.path.length))
233
+ const u = path.join(this.url, p).replace(/\\/g, '/')
234
+ return u
235
+ }
260
236
 
261
- // we may be about to use this, so don't let it be closed by cache purge
262
- this.fdman.checkout(p, fd)
263
- // a safe end() function that can be called multiple times but
264
- // only perform a single checkin
265
- var end = this.fdman.checkinfn(p, fd)
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
+ }
244
+
245
+ // querystrings are of no concern to us
246
+ if (!req.sturl) {
247
+ req.sturl = this.getUriPath(req.url)
248
+ }
249
+
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
+ }
257
+
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
266
+ }
266
267
 
267
- this.cache.stat.get(fd+':'+p, function (er, stat) {
268
+ const p = this.getPath(req.sturl)
269
+
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.
268
276
  if (er) {
269
- if (next && this.opt.passthrough === true && this._index === false) {
277
+ if (this.opt.passthrough === true && er.code === 'ENOENT' && next) {
270
278
  return next()
271
279
  }
272
- end()
273
280
  return this.error(er, res)
274
281
  }
275
282
 
276
- var isDirectory = stat.isDirectory()
277
-
278
- if (isDirectory) {
279
- end() // we won't need this fd for a directory in any case
280
- if (next && this.opt.passthrough === true && this._index === false) {
281
- // this is done before if-modified-since and if-non-match checks so
282
- // cached modified and etag values won't return 304's if we've since
283
- // switched to !index. See Issue #51.
284
- 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)
285
296
  }
286
- }
287
297
 
288
- var ims = req.headers['if-modified-since']
289
- if (ims) ims = new Date(ims).getTime()
290
- if (ims && ims >= stat.mtime.getTime()) {
291
- res.statusCode = 304
292
- res.end()
293
- return end()
294
- }
298
+ const isDirectory = stat.isDirectory()
295
299
 
296
- var etag = getEtag(stat)
297
- if (req.headers['if-none-match'] === etag) {
298
- res.statusCode = 304
299
- res.end()
300
- return end()
301
- }
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
+ }
302
309
 
303
- // only set headers once we're sure we'll be serving this request
304
- if (!res.getHeader('cache-control') && this._cacheControl)
305
- res.setHeader('cache-control', this._cacheControl)
306
- res.setHeader('last-modified', stat.mtime.toUTCString())
307
- 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
+ }
308
319
 
309
- if (this.opt.cors) {
310
- res.setHeader('Access-Control-Allow-Origin', '*')
311
- res.setHeader('Access-Control-Allow-Headers',
312
- 'Origin, X-Requested-With, Content-Type, Accept, Range')
313
- }
320
+ const etag = getEtag(stat)
321
+ if (req.headers['if-none-match'] === etag) {
322
+ res.statusCode = 304
323
+ res.end()
324
+ return end()
325
+ }
314
326
 
315
- return isDirectory
316
- ? this.index(p, req, res)
317
- : this.file(p, fd, stat, etag, req, res, end)
318
- }.bind(this))
319
- }.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)
320
333
 
321
- return true
322
- }
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
+ }
323
339
 
324
- Mount.prototype.error = function (er, res) {
325
- res.statusCode = typeof er === 'number' ? er
326
- : er.code === 'ENOENT' || er.code === 'EISDIR' ? 404
327
- : er.code === 'EPERM' || er.code === 'EACCES' ? 403
328
- : 500
340
+ return isDirectory
341
+ ? this.index(p, req, res)
342
+ : this.file(p, fd, stat, etag, req, res, end)
343
+ })
344
+ })
329
345
 
330
- if (typeof res.error === 'function') {
331
- // pattern of express and ErrorPage
332
- return res.error(res.statusCode, er)
346
+ return true
333
347
  }
334
348
 
335
- res.setHeader('content-type', 'text/plain')
336
- res.end(http.STATUS_CODES[res.statusCode] + '\n')
337
- }
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
338
354
 
339
- Mount.prototype.index = function (p, req, res) {
340
- if (this._index === true) {
341
- return this.autoindex(p, req, res)
342
- }
343
- if (typeof this._index === 'string') {
344
- if (!/\/$/.test(req.sturl)) req.sturl += '/'
345
- req.sturl += this._index
346
- return this.serve(req, res)
347
- }
348
- return this.error(404, res)
349
- }
355
+ if (typeof res.error === 'function') {
356
+ // pattern of express and ErrorPage
357
+ return res.error(res.statusCode, er)
358
+ }
350
359
 
351
- Mount.prototype.autoindex = function (p, req, res) {
352
- if (!/\/$/.exec(req.sturl)) {
353
- res.statusCode = 301
354
- res.setHeader('location', req.sturl + '/')
355
- res.end('Moved: ' + req.sturl + '/')
356
- return
360
+ res.setHeader('content-type', 'text/plain')
361
+ res.end(http.STATUS_CODES[res.statusCode] + '\n')
357
362
  }
358
363
 
359
- this.cache.index.get(p, function (er, html) {
360
- if (er) return this.error(er, res)
361
-
362
- res.statusCode = 200
363
- res.setHeader('content-type', 'text/html')
364
- res.setHeader('content-length', html.length)
365
- res.end(html)
366
- }.bind(this))
367
- }
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
+ }
368
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
+ }
369
385
 
370
- Mount.prototype.file = function (p, fd, stat, etag, req, res, end) {
371
- var key = stat.size + ':' + etag
386
+ this.cache.index.get(p, (er, html) => {
387
+ if (er) {
388
+ return this.error(er, res)
389
+ }
372
390
 
373
- var mt = mime.lookup(path.extname(p))
374
- if (mt !== 'application/octet-stream') {
375
- 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
+ })
376
396
  }
377
397
 
378
- // only use the content cache if it will actually fit there.
379
- if (this.cache.content.has(key)) {
380
- end()
381
- this.cachedFile(p, stat, etag, req, res)
382
- } else {
383
- this.streamFile(p, fd, stat, etag, req, res, end)
384
- }
385
- }
398
+ file (p, fd, stat, etag, req, res, end) {
399
+ const key = stat.size + ':' + etag
386
400
 
387
- Mount.prototype.cachedFile = function (p, stat, etag, req, res) {
388
- var key = stat.size + ':' + etag
389
- 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
+ }
390
405
 
391
- this.cache.content.get(key, function (er, content) {
392
- if (er) return this.error(er, res)
393
- res.statusCode = 200
394
- if (this.opt.cachedHeader)
395
- res.setHeader('x-from-cache', 'true')
396
- if (gz && content.gz) {
397
- res.setHeader('content-encoding', 'gzip')
398
- res.setHeader('content-length', content.gz.length)
399
- 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)
400
410
  } else {
401
- res.setHeader('content-length', content.length)
402
- res.end(content)
411
+ this.streamFile(p, fd, stat, etag, req, res, end)
403
412
  }
404
- }.bind(this))
405
- }
413
+ }
406
414
 
407
- Mount.prototype.streamFile = function (p, fd, stat, etag, req, res, end) {
408
- var streamOpt = { fd: fd, start: 0, end: stat.size }
409
- var stream = fs.createReadStream(p, streamOpt)
410
- 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)
411
418
 
412
- // gzip only if not explicitly turned off or client doesn't accept it
413
- var gzOpt = this.opt.gzip !== false
414
- var gz = gzOpt && getGz(p, req)
415
- var cachable = this.cache.content._cache.max > stat.size
416
- 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
+ }
417
437
 
418
- // need a gzipped version for the cache, so do it regardless of what the client wants
419
- 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 = () => {}
420
442
 
421
- // too late to effectively handle any errors.
422
- // just kill the connection if that happens.
423
- stream.on('error', function(e) {
424
- console.error('Error serving %s fd=%d\n%s', p, fd, e.stack || e.message)
425
- res.socket.destroy()
426
- end()
427
- })
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
428
448
 
429
- 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
+ }
430
453
 
431
- 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
+ })
432
461
 
433
- if (gz) {
434
- // we don't know how long it'll be, since it will be compressed.
435
- res.setHeader('content-encoding', 'gzip')
436
- stream.pipe(gzstr).pipe(res)
437
- } else {
438
- if (!res.filter) res.setHeader('content-length', stat.size)
439
- stream.pipe(res)
440
- if (gzstr)
441
- stream.pipe(gzstr) // for cache
442
- }
462
+ if (res.filter) {
463
+ stream = stream.pipe(res.filter)
464
+ }
443
465
 
444
- stream.on('end', function () {
445
- process.nextTick(end)
446
- })
466
+ res.statusCode = 200
447
467
 
448
- if (cachable) {
449
- // 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
+ }
450
481
 
451
- var calls = 0
482
+ stream.on('end', () => process.nextTick(end))
452
483
 
453
- // called by bl() for both the raw stream and gzipped stream if we're
454
- // caching gzipped data
455
- var collectEnd = function () {
456
- if (++calls == (gzOpt ? 2 : 1)) {
457
- var content = bufs.slice()
458
- content.gz = gzbufs && gzbufs.slice()
459
- 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
+ }
460
497
  }
461
- }.bind(this)
462
498
 
463
- var key = stat.size + ':' + etag
464
- var bufs = bl(collectEnd)
465
- var gzbufs
499
+ const key = stat.size + ':' + etag
500
+ const bufs = bl(collectEnd)
501
+ let gzbufs
466
502
 
467
- stream.pipe(bufs)
503
+ stream.pipe(bufs)
468
504
 
469
- if (gzstr) {
470
- gzbufs = bl(collectEnd)
471
- gzstr.pipe(gzbufs)
505
+ if (gzstr) {
506
+ gzbufs = bl(collectEnd)
507
+ gzstr.pipe(gzbufs)
508
+ }
472
509
  }
473
510
  }
474
- }
475
511
 
512
+ // cache-fillers
476
513
 
477
- // cache-fillers
478
-
479
- Mount.prototype._loadIndex = function (p, cb) {
480
- // truncate off the first bits
481
- var url = p.substr(this.path.length).replace(/\\/g, '/')
482
- 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
483
518
  .replace(/"/g, '"')
484
519
  .replace(/</g, '&lt;')
485
520
  .replace(/>/g, '&gt;')
486
521
  .replace(/'/g, '&#39;')
487
522
 
488
- var str =
489
- '<!doctype html>' +
490
- '<html>' +
491
- '<head><title>Index of ' + t + '</title></head>' +
492
- '<body>' +
493
- '<h1>Index of ' + t + '</h1>' +
494
- '<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'
495
530
 
496
- this.cache.readdir.get(p, function (er, data) {
497
- if (er) return cb(er)
531
+ this.cache.readdir.get(p, (er, data) => {
532
+ if (er) {
533
+ return cb(er)
534
+ }
498
535
 
499
- var nameLen = 0
500
- var sizeLen = 0
536
+ let nameLen = 0
537
+ let sizeLen = 0
501
538
 
502
- Object.keys(data).map(function (f) {
503
- var d = data[f]
539
+ Object.keys(data).map((f) => {
540
+ const d = data[f]
504
541
 
505
- var name = f
542
+ let name = f
506
543
  .replace(/"/g, '&quot;')
507
544
  .replace(/</g, '&lt;')
508
545
  .replace(/>/g, '&gt;')
509
546
  .replace(/'/g, '&#39;')
510
547
 
511
- if (d.size === '-') name += '/'
512
- var showName = name.replace(/^(.{40}).{3,}$/, '$1..>')
513
- var linkName = encodeURIComponent(name)
514
- .replace(/%2e/ig, '.') // Encoded dots are dots
515
- .replace(/%2f|%5c/ig, '/') // encoded slashes are /
516
- .replace(/[\/\\]/g, '/') // back slashes are slashes
517
-
518
- nameLen = Math.max(nameLen, showName.length)
519
- sizeLen = Math.max(sizeLen, ('' + d.size).length)
520
- return [ '<a href="' + linkName + '">' + showName + '</a>',
521
- d.mtime, d.size, showName ]
522
- }).sort(function (a, b) {
523
- return a[2] === '-' && b[2] !== '-' ? -1 // dirs first
524
- : a[2] !== '-' && b[2] === '-' ? 1
525
- : a[0].toLowerCase() < b[0].toLowerCase() ? -1 // then alpha
526
- : a[0].toLowerCase() > b[0].toLowerCase() ? 1
527
- : 0
528
- }).forEach(function (line) {
529
- var namePad = new Array(8 + nameLen - line[3].length).join(' ')
530
- var sizePad = new Array(8 + sizeLen - ('' + line[2]).length).join(' ')
531
- str += line[0] + namePad +
532
- line[1].toISOString() +
533
- 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))
534
577
  })
578
+ }
535
579
 
536
- str += '</pre><hr></body></html>'
537
- cb(null, new Buffer(str))
538
- })
539
- }
540
-
541
- Mount.prototype._loadReaddir = function (p, cb) {
542
- var len
543
- var data
544
- fs.readdir(p, function (er, files) {
545
- if (er) return cb(er)
546
- files = files.filter(function (f) {
547
- if (!this.opt.dot) return !/^\./.test(f)
548
- else return f !== '.' && f !== '..'
549
- }.bind(this))
550
- len = files.length
551
- data = {}
552
- files.forEach(function (file) {
553
- var pf = path.join(p, file)
554
- this.cache.stat.get(pf, function (er, stat) {
555
- if (er) return cb(er)
556
- if (stat.isDirectory()) stat.size = '-'
557
- data[file] = stat
558
- next()
559
- }.bind(this))
560
- }.bind(this))
561
- }.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
+ })
562
610
 
563
- function next () {
564
- if (--len === 0) cb(null, data)
611
+ const next = () => {
612
+ if (--len === 0) {
613
+ cb(null, data)
614
+ }
615
+ }
565
616
  }
566
- }
567
617
 
568
- Mount.prototype._loadStat = function (key, cb) {
569
- // key is either fd:path or just a path
570
- var fdp = key.match(/^(\d+):(.*)/)
571
- if (fdp) {
572
- var fd = +fdp[1]
573
- var p = fdp[2]
574
- fs.fstat(fd, function (er, stat) {
575
- if (er) return cb(er)
576
- this.cache.stat.set(p, stat)
577
- cb(null, stat)
578
- }.bind(this))
579
- } else {
580
- 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
+ }
581
634
  }
582
- }
583
635
 
584
- Mount.prototype._loadContent = function () {
585
- // this function should never be called.
586
- // we check if the thing is in the cache, and if not, stream it in
587
- // manually. this.cache.content.get() should not ever happen.
588
- 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
+ }
589
642
  }
590
643
 
591
644
  function getEtag (s) {
592
645
  return '"' + s.dev + '-' + s.ino + '-' + s.mtime.getTime() + '"'
593
646
  }
594
647
 
595
- function getGz (p,req) {
596
- var gz = false
648
+ function getGz (p, req) {
649
+ let gz = false
597
650
  if (!/\.t?gz$/.exec(p)) {
598
- var neg = req.negotiator || new Neg(req)
651
+ const neg = req.negotiator || new Neg(req)
599
652
  gz = neg.preferredEncoding(['gzip', 'identity']) === 'gzip'
600
653
  }
601
654
  return gz
602
655
  }
656
+
657
+ module.exports = st
658
+ module.exports.Mount = Mount