st 3.0.2 → 3.0.3

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.
@@ -0,0 +1,12 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "WebFetch(domain:github.com)",
5
+ "WebFetch(domain:patch-diff.githubusercontent.com)",
6
+ "Bash(curl:*)",
7
+ "Bash(node:*)",
8
+ "Bash(npm test:*)"
9
+ ],
10
+ "deny": []
11
+ }
12
+ }
package/README.md CHANGED
@@ -129,7 +129,7 @@ const mount = st({
129
129
  },
130
130
 
131
131
  content: {
132
- max: 1024*1024*64, // how much memory to use on caching contents
132
+ maxSize: 1024*1024*64, // how much memory to use on caching contents
133
133
  maxAge: 1000 * 60 * 10, // how long to cache contents for
134
134
  // if `false` does not set cache control headers
135
135
  cacheControl: 'public, max-age=600' // to set an explicit cache-control
@@ -137,12 +137,12 @@ const mount = st({
137
137
  },
138
138
 
139
139
  index: { // irrelevant if not using index:true
140
- max: 1024 * 8, // how many bytes of autoindex html to cache
140
+ maxSize: 1024 * 8, // how many bytes of autoindex html to cache
141
141
  maxAge: 1000 * 60 * 10, // how long to store it for
142
142
  },
143
143
 
144
144
  readdir: { // irrelevant if not using index:true
145
- max: 1000, // how many dir entries to cache
145
+ maxSize: 1000, // how many dir entries to cache
146
146
  maxAge: 1000 * 60 * 10, // how long to cache them for
147
147
  }
148
148
  },
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "st",
3
- "version": "3.0.2",
3
+ "version": "3.0.3",
4
4
  "description": "A module for serving static files. Does etags, caching, etc.",
5
5
  "main": "st.js",
6
6
  "bin": "bin/server.js",
7
7
  "dependencies": {
8
- "async-cache": "^1.1.0",
8
+ "lru-cache": "^11.1.0",
9
9
  "bl": "^6.1.0",
10
10
  "fd": "^0.0.3",
11
11
  "mime": "^3.0.0",
package/st.js CHANGED
@@ -9,7 +9,7 @@ try {
9
9
  const zlib = require('zlib')
10
10
  const Neg = require('negotiator')
11
11
  const http = require('http')
12
- const AC = require('async-cache')
12
+ const { LRUCache } = require('lru-cache')
13
13
  const FD = require('fd')
14
14
  const bl = require('bl')
15
15
  const { STATUS_CODES } = http
@@ -17,26 +17,31 @@ const { STATUS_CODES } = http
17
17
  const defaultCacheOptions = {
18
18
  fd: {
19
19
  max: 1000,
20
- maxAge: 1000 * 60 * 60
20
+ maxAge: 1000 * 60 * 60,
21
+ ignoreFetchAbort: true
21
22
  },
22
23
  stat: {
23
24
  max: 5000,
24
- maxAge: 1000 * 60
25
+ maxAge: 1000 * 60,
26
+ ignoreFetchAbort: true
25
27
  },
26
28
  content: {
27
- max: 1024 * 1024 * 64,
28
- length: (n) => n.length,
29
- maxAge: 1000 * 60 * 10
29
+ maxSize: 1024 * 1024 * 64,
30
+ sizeCalculation: (n) => n.length,
31
+ maxAge: 1000 * 60 * 10,
32
+ ignoreFetchAbort: true
30
33
  },
31
34
  index: {
32
- max: 1024 * 8,
33
- length: (n) => n.length,
34
- maxAge: 1000 * 60 * 10
35
+ maxSize: 1024 * 8,
36
+ sizeCalculation: (n) => n.length,
37
+ maxAge: 1000 * 60 * 10,
38
+ ignoreFetchAbort: true
35
39
  },
36
40
  readdir: {
37
- max: 1000,
38
- length: (n) => n.length,
39
- maxAge: 1000 * 60 * 10
41
+ maxSize: 1000,
42
+ sizeCalculation: (n) => Object.keys(n).length,
43
+ maxAge: 1000 * 60 * 10,
44
+ ignoreFetchAbort: true
40
45
  }
41
46
  }
42
47
 
@@ -44,8 +49,8 @@ const defaultCacheOptions = {
44
49
  // everything is really big. kind of a kludge, but easiest way
45
50
  // to get it done
46
51
  const none = {
47
- max: 1,
48
- length: () => Infinity
52
+ maxSize: 1,
53
+ sizeCalculation: () => Number.MAX_SAFE_INTEGER
49
54
  }
50
55
 
51
56
  const noCaching = {
@@ -124,11 +129,11 @@ class Mount {
124
129
  // cache basically everything
125
130
  const c = this.getCacheOptions(opt)
126
131
  this.cache = {
127
- fd: AC(c.fd),
128
- stat: AC(c.stat),
129
- index: AC(c.index),
130
- readdir: AC(c.readdir),
131
- content: AC(c.content)
132
+ fd: new LRUCache(c.fd),
133
+ stat: new LRUCache(c.stat),
134
+ index: new LRUCache(c.index),
135
+ readdir: new LRUCache(c.readdir),
136
+ content: new LRUCache(c.content)
132
137
  }
133
138
 
134
139
  this._cacheControl =
@@ -158,7 +163,7 @@ class Mount {
158
163
  const d = defaultCacheOptions
159
164
 
160
165
  // should really only ever set max and maxAge here.
161
- // load and fd disposal is important to control.
166
+ // fetchMethod and fd disposal is important to control.
162
167
  const c = {
163
168
  fd: set('fd'),
164
169
  stat: set('stat'),
@@ -167,13 +172,14 @@ class Mount {
167
172
  content: set('content')
168
173
  }
169
174
 
170
- c.fd.dispose = this.fdman.close.bind(this.fdman)
171
- c.fd.load = this.fdman.open.bind(this.fdman)
175
+ c.fd.dispose = (key) => this.fdman.close(key)
176
+ c.fd.fetchMethod = (key) => new Promise((resolve, reject) => this.fdman.open(key, (err, fd) => err ? reject(err) : resolve(fd)))
177
+
178
+ c.stat.fetchMethod = (key) => new Promise((resolve, reject) => this._loadStat(key, (err, fd) => err ? reject(err) : resolve(fd)))
179
+ c.index.fetchMethod = (key) => new Promise((resolve, reject) => this._loadIndex(key, (err, fd) => err ? reject(err) : resolve(fd)))
180
+ c.readdir.fetchMethod = (key) => new Promise((resolve, reject) => this._loadReaddir(key, (err, fd) => err ? reject(err) : resolve(fd)))
181
+ c.content.fetchMethod = (key) => new Promise((resolve, reject) => this._loadContent(key, (err, fd) => err ? reject(err) : resolve(fd)))
172
182
 
173
- c.stat.load = this._loadStat.bind(this)
174
- c.index.load = this._loadIndex.bind(this)
175
- c.readdir.load = this._loadReaddir.bind(this)
176
- c.content.load = this._loadContent.bind(this)
177
183
  return c
178
184
  }
179
185
 
@@ -202,7 +208,7 @@ class Mount {
202
208
  u = decoded
203
209
  }
204
210
  } catch (e) {
205
- // if decodeURIComponent failed, we weren't given a valid URL to begin with.
211
+ // if decodeURIComponent failed, we weren't given a valid URL to begin with.
206
212
  return false
207
213
  }
208
214
 
@@ -216,10 +222,12 @@ class Mount {
216
222
 
217
223
  // get a path from a url
218
224
  getPath (u) {
219
- // trailing slash removal to fix Node.js v23 bug
220
- // https://github.com/nodejs/node/pull/55527
221
- // can be removed when this is resolved and released
222
- return path.join(this.path, u.replace(/\/+$/, ''))
225
+ // Normalize paths by removing trailing slashes
226
+ // This ensures consistent paths for directory content rendering
227
+ while (u.length > 0 && u[u.length - 1] === '/') {
228
+ u = u.slice(0, -1)
229
+ }
230
+ return path.join(this.path, u)
223
231
  }
224
232
 
225
233
  // get a url from a path
@@ -267,80 +275,82 @@ class Mount {
267
275
  const p = this.getPath(req.sturl)
268
276
 
269
277
  // now we have a path. check for the fd.
270
- this.cache.fd.get(p, (er, fd) => {
271
- // inability to open is some kind of error, probably 404
272
- // if we're in passthrough, AND got a next function, we can
273
- // fall through to that. otherwise, we already returned true,
274
- // send an error.
275
- if (er) {
278
+ this.cache.fd.fetch(p).then(
279
+ (fd) => {
280
+ // we may be about to use this, so don't let it be closed by cache purge
281
+ this.fdman.checkout(p, fd)
282
+ // a safe end() function that can be called multiple times but
283
+ // only perform a single checkin
284
+ const end = this.fdman.checkinfn(p, fd)
285
+
286
+ this.cache.stat.fetch(fd + ':' + p).then(
287
+ (stat) => {
288
+ const isDirectory = stat.isDirectory()
289
+
290
+ if (isDirectory) {
291
+ end() // we won't need this fd for a directory in any case
292
+ if (next && this.opt.passthrough === true && this._index === false) {
293
+ // this is done before if-modified-since and if-non-match checks so
294
+ // cached modified and etag values won't return 304's if we've since
295
+ // switched to !index. See Issue #51.
296
+ return next()
297
+ }
298
+ }
299
+
300
+ let ims = req.headers['if-modified-since']
301
+ if (ims) {
302
+ ims = new Date(ims).getTime()
303
+ }
304
+ if (ims && ims >= stat.mtime.getTime()) {
305
+ res.statusCode = 304
306
+ res.end()
307
+ return end()
308
+ }
309
+
310
+ const etag = getEtag(stat)
311
+ if (req.headers['if-none-match'] === etag) {
312
+ res.statusCode = 304
313
+ res.end()
314
+ return end()
315
+ }
316
+
317
+ // only set headers once we're sure we'll be serving this request
318
+ if (!res.getHeader('cache-control') && this._cacheControl) {
319
+ res.setHeader('cache-control', this._cacheControl)
320
+ }
321
+ res.setHeader('last-modified', stat.mtime.toUTCString())
322
+ res.setHeader('etag', etag)
323
+
324
+ if (this.opt.cors) {
325
+ res.setHeader('Access-Control-Allow-Origin', '*')
326
+ res.setHeader('Access-Control-Allow-Headers',
327
+ 'Origin, X-Requested-With, Content-Type, Accept, Range')
328
+ }
329
+
330
+ return isDirectory
331
+ ? this.index(p, req, res)
332
+ : this.file(p, fd, stat, etag, req, res, end)
333
+ },
334
+ (er) => {
335
+ if (next && this.opt.passthrough === true && this._index === false) {
336
+ return next()
337
+ }
338
+ end()
339
+ return this.error(er, res)
340
+ }
341
+ )
342
+ },
343
+ (er) => {
344
+ // inability to open is some kind of error, probably 404
345
+ // if we're in passthrough, AND got a next function, we can
346
+ // fall through to that. otherwise, we already returned true,
347
+ // send an error.
276
348
  if (this.opt.passthrough === true && er.code === 'ENOENT' && next) {
277
349
  return next()
278
350
  }
279
351
  return this.error(er, res)
280
352
  }
281
-
282
- // we may be about to use this, so don't let it be closed by cache purge
283
- this.fdman.checkout(p, fd)
284
- // a safe end() function that can be called multiple times but
285
- // only perform a single checkin
286
- const end = this.fdman.checkinfn(p, fd)
287
-
288
- this.cache.stat.get(fd + ':' + p, (er, stat) => {
289
- if (er) {
290
- if (next && this.opt.passthrough === true && this._index === false) {
291
- return next()
292
- }
293
- end()
294
- return this.error(er, res)
295
- }
296
-
297
- const isDirectory = stat.isDirectory()
298
-
299
- if (isDirectory) {
300
- end() // we won't need this fd for a directory in any case
301
- if (next && this.opt.passthrough === true && this._index === false) {
302
- // this is done before if-modified-since and if-non-match checks so
303
- // cached modified and etag values won't return 304's if we've since
304
- // switched to !index. See Issue #51.
305
- return next()
306
- }
307
- }
308
-
309
- let ims = req.headers['if-modified-since']
310
- if (ims) {
311
- ims = new Date(ims).getTime()
312
- }
313
- if (ims && ims >= stat.mtime.getTime()) {
314
- res.statusCode = 304
315
- res.end()
316
- return end()
317
- }
318
-
319
- const etag = getEtag(stat)
320
- if (req.headers['if-none-match'] === etag) {
321
- res.statusCode = 304
322
- res.end()
323
- return end()
324
- }
325
-
326
- // only set headers once we're sure we'll be serving this request
327
- if (!res.getHeader('cache-control') && this._cacheControl) {
328
- res.setHeader('cache-control', this._cacheControl)
329
- }
330
- res.setHeader('last-modified', stat.mtime.toUTCString())
331
- res.setHeader('etag', etag)
332
-
333
- if (this.opt.cors) {
334
- res.setHeader('Access-Control-Allow-Origin', '*')
335
- res.setHeader('Access-Control-Allow-Headers',
336
- 'Origin, X-Requested-With, Content-Type, Accept, Range')
337
- }
338
-
339
- return isDirectory
340
- ? this.index(p, req, res)
341
- : this.file(p, fd, stat, etag, req, res, end)
342
- })
343
- })
353
+ )
344
354
 
345
355
  return true
346
356
  }
@@ -385,16 +395,15 @@ class Mount {
385
395
  return
386
396
  }
387
397
 
388
- this.cache.index.get(p, (er, html) => {
389
- if (er) {
390
- return this.error(er, res)
391
- }
392
-
393
- res.statusCode = 200
394
- res.setHeader('content-type', 'text/html')
395
- res.setHeader('content-length', html.length)
396
- res.end(html)
397
- })
398
+ this.cache.index.fetch(p).then(
399
+ (html) => {
400
+ res.statusCode = 200
401
+ res.setHeader('content-type', 'text/html')
402
+ res.setHeader('content-length', html.length)
403
+ res.end(html)
404
+ },
405
+ (er) => this.error(er, res)
406
+ )
398
407
  }
399
408
 
400
409
  file (p, fd, stat, etag, req, res, end) {
@@ -418,23 +427,19 @@ class Mount {
418
427
  const key = stat.size + ':' + etag
419
428
  const gz = this.opt.gzip !== false && getGz(p, req)
420
429
 
421
- this.cache.content.get(key, (er, content) => {
422
- if (er) {
423
- return this.error(er, res)
424
- }
425
- res.statusCode = 200
426
- if (this.opt.cachedHeader) {
427
- res.setHeader('x-from-cache', 'true')
428
- }
429
- if (gz && content.gz) {
430
- res.setHeader('content-encoding', 'gzip')
431
- res.setHeader('content-length', content.gz.length)
432
- res.end(content.gz)
433
- } else {
434
- res.setHeader('content-length', content.length)
435
- res.end(content)
436
- }
437
- })
430
+ const content = this.cache.content.get(key)
431
+ res.statusCode = 200
432
+ if (this.opt.cachedHeader) {
433
+ res.setHeader('x-from-cache', 'true')
434
+ }
435
+ if (gz && content.gz) {
436
+ res.setHeader('content-encoding', 'gzip')
437
+ res.setHeader('content-length', content.gz.length)
438
+ res.end(content.gz)
439
+ } else {
440
+ res.setHeader('content-length', content.length)
441
+ res.end(content)
442
+ }
438
443
  }
439
444
 
440
445
  streamFile (p, fd, stat, etag, req, res, end) {
@@ -445,7 +450,7 @@ class Mount {
445
450
  // gzip only if not explicitly turned off or client doesn't accept it
446
451
  const gzOpt = this.opt.gzip !== false
447
452
  const gz = gzOpt && getGz(p, req)
448
- const cachable = this.cache.content._cache.max > stat.size
453
+ const cachable = this.cache.content.maxSize > stat.size
449
454
  let gzstr
450
455
 
451
456
  // need a gzipped version for the cache, so do it regardless of what the client wants
@@ -530,57 +535,56 @@ class Mount {
530
535
  '<h1>Index of ' + t + '</h1>' +
531
536
  '<hr><pre><a href="../">../</a>\n'
532
537
 
533
- this.cache.readdir.get(p, (er, data) => {
534
- if (er) {
535
- return cb(er)
536
- }
538
+ this.cache.readdir.fetch(p).then(
539
+ (data) => {
540
+ let nameLen = 0
541
+ let sizeLen = 0
537
542
 
538
- let nameLen = 0
539
- let sizeLen = 0
543
+ Object.keys(data).map((f) => {
544
+ const d = data[f]
540
545
 
541
- Object.keys(data).map((f) => {
542
- const d = data[f]
546
+ let name = f
547
+ .replace(/"/g, '&quot;')
548
+ .replace(/</g, '&lt;')
549
+ .replace(/>/g, '&gt;')
550
+ .replace(/'/g, '&#39;')
543
551
 
544
- let name = f
545
- .replace(/"/g, '&quot;')
546
- .replace(/</g, '&lt;')
547
- .replace(/>/g, '&gt;')
548
- .replace(/'/g, '&#39;')
549
-
550
- if (d.size === '-') {
551
- name += '/'
552
- }
553
- const showName = name.replace(/^(.{40}).{3,}$/, '$1..>')
554
- const linkName = encodeURIComponent(name)
555
- .replace(/%2e/ig, '.') // Encoded dots are dots
556
- .replace(/%2f|%5c/ig, '/') // encoded slashes are /
557
- .replace(/[/\\]/g, '/') // back slashes are slashes
558
-
559
- nameLen = Math.max(nameLen, showName.length)
560
- sizeLen = Math.max(sizeLen, ('' + d.size).length)
561
- return ['<a href="' + linkName + '">' + showName + '</a>',
562
- d.mtime, d.size, showName]
563
- }).sort((a, b) => {
564
- return a[2] === '-' && b[2] !== '-' // dirs first
565
- ? -1
566
- : a[2] !== '-' && b[2] === '-'
567
- ? 1
568
- : a[0].toLowerCase() < b[0].toLowerCase() // then alpha
569
- ? -1
570
- : a[0].toLowerCase() > b[0].toLowerCase()
571
- ? 1
572
- : 0
573
- }).forEach((line) => {
574
- const namePad = new Array(8 + nameLen - line[3].length).join(' ')
575
- const sizePad = new Array(8 + sizeLen - ('' + line[2]).length).join(' ')
576
- str += line[0] + namePad +
577
- line[1].toISOString() +
578
- sizePad + line[2] + '\n'
579
- })
552
+ if (d.size === '-') {
553
+ name += '/'
554
+ }
555
+ const showName = name.replace(/^(.{40}).{3,}$/, '$1..>')
556
+ const linkName = encodeURIComponent(name)
557
+ .replace(/%2e/ig, '.') // Encoded dots are dots
558
+ .replace(/%2f|%5c/ig, '/') // encoded slashes are /
559
+ .replace(/[/\\]/g, '/') // back slashes are slashes
560
+
561
+ nameLen = Math.max(nameLen, showName.length)
562
+ sizeLen = Math.max(sizeLen, ('' + d.size).length)
563
+ return ['<a href="' + linkName + '">' + showName + '</a>',
564
+ d.mtime, d.size, showName]
565
+ }).sort((a, b) => {
566
+ return a[2] === '-' && b[2] !== '-' // dirs first
567
+ ? -1
568
+ : a[2] !== '-' && b[2] === '-'
569
+ ? 1
570
+ : a[0].toLowerCase() < b[0].toLowerCase() // then alpha
571
+ ? -1
572
+ : a[0].toLowerCase() > b[0].toLowerCase()
573
+ ? 1
574
+ : 0
575
+ }).forEach((line) => {
576
+ const namePad = new Array(8 + nameLen - line[3].length).join(' ')
577
+ const sizePad = new Array(8 + sizeLen - ('' + line[2]).length).join(' ')
578
+ str += line[0] + namePad +
579
+ line[1].toISOString() +
580
+ sizePad + line[2] + '\n'
581
+ })
580
582
 
581
- str += '</pre><hr></body></html>'
582
- cb(null, Buffer.from(str))
583
- })
583
+ str += '</pre><hr></body></html>'
584
+ cb(null, Buffer.from(str))
585
+ },
586
+ (er) => cb(er)
587
+ )
584
588
  }
585
589
 
586
590
  _loadReaddir (p, cb) {
@@ -601,16 +605,16 @@ class Mount {
601
605
  data = {}
602
606
  files.forEach((file) => {
603
607
  const pf = path.join(p, file)
604
- this.cache.stat.get(pf, (er, stat) => {
605
- if (er) {
606
- return cb(er)
607
- }
608
- if (stat.isDirectory()) {
609
- stat.size = '-'
610
- }
611
- data[file] = stat
612
- next()
613
- })
608
+ this.cache.stat.fetch(pf).then(
609
+ (stat) => {
610
+ if (stat.isDirectory()) {
611
+ stat.size = '-'
612
+ }
613
+ data[file] = stat
614
+ next()
615
+ },
616
+ (er) => cb(er)
617
+ )
614
618
  })
615
619
  })
616
620
 
@@ -639,11 +643,11 @@ class Mount {
639
643
  }
640
644
  }
641
645
 
642
- _loadContent () {
646
+ _loadContent (_, cb) {
643
647
  // this function should never be called.
644
648
  // we check if the thing is in the cache, and if not, stream it in
645
- // manually. this.cache.content.get() should not ever happen.
646
- throw new Error('This should not ever happen')
649
+ // manually. this.cache.content.fetch() should not ever happen.
650
+ return cb(new Error('This should never happen'))
647
651
  }
648
652
  }
649
653
 
package/test/no-cache.js CHANGED
@@ -10,10 +10,10 @@ const { test } = require('tap')
10
10
  // additional tests to ensure that it's actually not caching.
11
11
 
12
12
  test('all caches should be empty', (t) => {
13
- t.same(mount._this.cache.fd._cache.dump(), [])
14
- t.same(mount._this.cache.stat._cache.dump(), [])
15
- t.same(mount._this.cache.index._cache.dump(), [])
16
- t.same(mount._this.cache.readdir._cache.dump(), [])
17
- t.same(mount._this.cache.content._cache.dump(), [])
13
+ t.same(mount._this.cache.fd.dump(), [])
14
+ t.same(mount._this.cache.stat.dump(), [])
15
+ t.same(mount._this.cache.index.dump(), [])
16
+ t.same(mount._this.cache.readdir.dump(), [])
17
+ t.same(mount._this.cache.content.dump(), [])
18
18
  t.end()
19
19
  })