st 3.0.1 → 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.
- package/.claude/settings.local.json +12 -0
- package/README.md +3 -3
- package/bin/server.js +5 -4
- package/package.json +8 -8
- package/st.js +203 -206
- package/test/basic.js +2 -11
- package/test/cli/common.js +2 -2
- package/test/common.js +1 -1
- package/test/gzip-after-no-gzip.js +4 -1
- package/test/multi-mount.js +3 -3
- package/test/no-cache.js +5 -5
- package/test/preset-cache-control.js +1 -1
package/README.md
CHANGED
|
@@ -129,7 +129,7 @@ const mount = st({
|
|
|
129
129
|
},
|
|
130
130
|
|
|
131
131
|
content: {
|
|
132
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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/bin/server.js
CHANGED
|
@@ -82,6 +82,7 @@ for (let i = 2; i < process.argv.length; i++) {
|
|
|
82
82
|
case '--help':
|
|
83
83
|
help()
|
|
84
84
|
process.exit(0)
|
|
85
|
+
break
|
|
85
86
|
|
|
86
87
|
case '-nc':
|
|
87
88
|
case '--no-cache':
|
|
@@ -150,9 +151,9 @@ if (isNaN(port)) {
|
|
|
150
151
|
|
|
151
152
|
const opt = {
|
|
152
153
|
path: dir,
|
|
153
|
-
url
|
|
154
|
-
index
|
|
155
|
-
dot
|
|
154
|
+
url,
|
|
155
|
+
index,
|
|
156
|
+
dot,
|
|
156
157
|
cache: {
|
|
157
158
|
fd: {},
|
|
158
159
|
stat: {},
|
|
@@ -160,7 +161,7 @@ const opt = {
|
|
|
160
161
|
readdir: {},
|
|
161
162
|
content: {}
|
|
162
163
|
},
|
|
163
|
-
cors
|
|
164
|
+
cors
|
|
164
165
|
}
|
|
165
166
|
|
|
166
167
|
if (cache === false) {
|
package/package.json
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "st",
|
|
3
|
-
"version": "3.0.
|
|
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
|
-
"
|
|
9
|
-
"bl": "^
|
|
10
|
-
"fd": "
|
|
11
|
-
"mime": "^
|
|
12
|
-
"negotiator": "
|
|
8
|
+
"lru-cache": "^11.1.0",
|
|
9
|
+
"bl": "^6.1.0",
|
|
10
|
+
"fd": "^0.0.3",
|
|
11
|
+
"mime": "^3.0.0",
|
|
12
|
+
"negotiator": "^1.0.0"
|
|
13
13
|
},
|
|
14
14
|
"optionalDependencies": {
|
|
15
15
|
"graceful-fs": "^4.2.3"
|
|
@@ -17,8 +17,8 @@
|
|
|
17
17
|
"devDependencies": {
|
|
18
18
|
"request": "^2.88.2",
|
|
19
19
|
"rimraf": "^3.0.2",
|
|
20
|
-
"standard": "^
|
|
21
|
-
"tap": "^15.
|
|
20
|
+
"standard": "^17.1.2",
|
|
21
|
+
"tap": "^15.2.3"
|
|
22
22
|
},
|
|
23
23
|
"scripts": {
|
|
24
24
|
"lint": "standard",
|
package/st.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
const mime = require('mime')
|
|
2
2
|
const path = require('path')
|
|
3
|
-
const url = require('url')
|
|
4
3
|
let fs
|
|
5
4
|
try {
|
|
6
5
|
fs = require('graceful-fs')
|
|
@@ -10,7 +9,7 @@ try {
|
|
|
10
9
|
const zlib = require('zlib')
|
|
11
10
|
const Neg = require('negotiator')
|
|
12
11
|
const http = require('http')
|
|
13
|
-
const
|
|
12
|
+
const { LRUCache } = require('lru-cache')
|
|
14
13
|
const FD = require('fd')
|
|
15
14
|
const bl = require('bl')
|
|
16
15
|
const { STATUS_CODES } = http
|
|
@@ -18,26 +17,31 @@ const { STATUS_CODES } = http
|
|
|
18
17
|
const defaultCacheOptions = {
|
|
19
18
|
fd: {
|
|
20
19
|
max: 1000,
|
|
21
|
-
maxAge: 1000 * 60 * 60
|
|
20
|
+
maxAge: 1000 * 60 * 60,
|
|
21
|
+
ignoreFetchAbort: true
|
|
22
22
|
},
|
|
23
23
|
stat: {
|
|
24
24
|
max: 5000,
|
|
25
|
-
maxAge: 1000 * 60
|
|
25
|
+
maxAge: 1000 * 60,
|
|
26
|
+
ignoreFetchAbort: true
|
|
26
27
|
},
|
|
27
28
|
content: {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
maxAge: 1000 * 60 * 10
|
|
29
|
+
maxSize: 1024 * 1024 * 64,
|
|
30
|
+
sizeCalculation: (n) => n.length,
|
|
31
|
+
maxAge: 1000 * 60 * 10,
|
|
32
|
+
ignoreFetchAbort: true
|
|
31
33
|
},
|
|
32
34
|
index: {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
maxAge: 1000 * 60 * 10
|
|
35
|
+
maxSize: 1024 * 8,
|
|
36
|
+
sizeCalculation: (n) => n.length,
|
|
37
|
+
maxAge: 1000 * 60 * 10,
|
|
38
|
+
ignoreFetchAbort: true
|
|
36
39
|
},
|
|
37
40
|
readdir: {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
maxAge: 1000 * 60 * 10
|
|
41
|
+
maxSize: 1000,
|
|
42
|
+
sizeCalculation: (n) => Object.keys(n).length,
|
|
43
|
+
maxAge: 1000 * 60 * 10,
|
|
44
|
+
ignoreFetchAbort: true
|
|
41
45
|
}
|
|
42
46
|
}
|
|
43
47
|
|
|
@@ -45,8 +49,8 @@ const defaultCacheOptions = {
|
|
|
45
49
|
// everything is really big. kind of a kludge, but easiest way
|
|
46
50
|
// to get it done
|
|
47
51
|
const none = {
|
|
48
|
-
|
|
49
|
-
|
|
52
|
+
maxSize: 1,
|
|
53
|
+
sizeCalculation: () => Number.MAX_SAFE_INTEGER
|
|
50
54
|
}
|
|
51
55
|
|
|
52
56
|
const noCaching = {
|
|
@@ -125,11 +129,11 @@ class Mount {
|
|
|
125
129
|
// cache basically everything
|
|
126
130
|
const c = this.getCacheOptions(opt)
|
|
127
131
|
this.cache = {
|
|
128
|
-
fd:
|
|
129
|
-
stat:
|
|
130
|
-
index:
|
|
131
|
-
readdir:
|
|
132
|
-
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)
|
|
133
137
|
}
|
|
134
138
|
|
|
135
139
|
this._cacheControl =
|
|
@@ -159,7 +163,7 @@ class Mount {
|
|
|
159
163
|
const d = defaultCacheOptions
|
|
160
164
|
|
|
161
165
|
// should really only ever set max and maxAge here.
|
|
162
|
-
//
|
|
166
|
+
// fetchMethod and fd disposal is important to control.
|
|
163
167
|
const c = {
|
|
164
168
|
fd: set('fd'),
|
|
165
169
|
stat: set('stat'),
|
|
@@ -168,34 +172,26 @@ class Mount {
|
|
|
168
172
|
content: set('content')
|
|
169
173
|
}
|
|
170
174
|
|
|
171
|
-
c.fd.dispose = this.fdman.close
|
|
172
|
-
c.fd.
|
|
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)))
|
|
173
182
|
|
|
174
|
-
c.stat.load = this._loadStat.bind(this)
|
|
175
|
-
c.index.load = this._loadIndex.bind(this)
|
|
176
|
-
c.readdir.load = this._loadReaddir.bind(this)
|
|
177
|
-
c.content.load = this._loadContent.bind(this)
|
|
178
183
|
return c
|
|
179
184
|
}
|
|
180
185
|
|
|
181
186
|
// get the path component from a URI
|
|
182
187
|
getUriPath (u) {
|
|
183
|
-
let p =
|
|
184
|
-
|
|
185
|
-
// Encoded dots are dots
|
|
186
|
-
p = p.replace(/%2e/ig, '.')
|
|
187
|
-
|
|
188
|
-
// encoded slashes are /
|
|
189
|
-
p = p.replace(/%2f|%5c/ig, '/')
|
|
188
|
+
let p = new URL(u, 'http://base').pathname
|
|
190
189
|
|
|
191
|
-
//
|
|
192
|
-
p = p.replace(
|
|
190
|
+
// Convert any backslashes to forward slashes (for consistency)
|
|
191
|
+
p = p.replace(/\\/g, '/')
|
|
193
192
|
|
|
194
|
-
//
|
|
195
|
-
p = p.replace(/^\//, '/')
|
|
193
|
+
// Remove the redundant leading slash replacement - URL().pathname always starts with /
|
|
196
194
|
if ((/[/\\]\.\.([/\\]|$)/).test(p)) {
|
|
197
|
-
// traversal urls not ever even slightly allowed. clearly shenanigans
|
|
198
|
-
// send a 403 on that noise, do not pass go, do not collect $200
|
|
199
195
|
return 403
|
|
200
196
|
}
|
|
201
197
|
|
|
@@ -204,15 +200,18 @@ class Mount {
|
|
|
204
200
|
return false
|
|
205
201
|
}
|
|
206
202
|
|
|
203
|
+
// URL constructor already decoded, but we might need additional decoding
|
|
204
|
+
// for edge cases. Only do it if it would actually change something.
|
|
207
205
|
try {
|
|
208
|
-
|
|
206
|
+
const decoded = decodeURIComponent(u)
|
|
207
|
+
if (decoded !== u) {
|
|
208
|
+
u = decoded
|
|
209
|
+
}
|
|
209
210
|
} catch (e) {
|
|
210
211
|
// if decodeURIComponent failed, we weren't given a valid URL to begin with.
|
|
211
212
|
return false
|
|
212
213
|
}
|
|
213
214
|
|
|
214
|
-
// /a/b/c mounted on /path/to/z/d/x
|
|
215
|
-
// /a/b/c/d --> /path/to/z/d/x/d
|
|
216
215
|
u = u.substr(this.url.length)
|
|
217
216
|
if (u.charAt(0) !== '/') {
|
|
218
217
|
u = '/' + u
|
|
@@ -223,10 +222,12 @@ class Mount {
|
|
|
223
222
|
|
|
224
223
|
// get a path from a url
|
|
225
224
|
getPath (u) {
|
|
226
|
-
//
|
|
227
|
-
//
|
|
228
|
-
|
|
229
|
-
|
|
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)
|
|
230
231
|
}
|
|
231
232
|
|
|
232
233
|
// get a url from a path
|
|
@@ -274,80 +275,82 @@ class Mount {
|
|
|
274
275
|
const p = this.getPath(req.sturl)
|
|
275
276
|
|
|
276
277
|
// now we have a path. check for the fd.
|
|
277
|
-
this.cache.fd.
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
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.
|
|
283
348
|
if (this.opt.passthrough === true && er.code === 'ENOENT' && next) {
|
|
284
349
|
return next()
|
|
285
350
|
}
|
|
286
351
|
return this.error(er, res)
|
|
287
352
|
}
|
|
288
|
-
|
|
289
|
-
// we may be about to use this, so don't let it be closed by cache purge
|
|
290
|
-
this.fdman.checkout(p, fd)
|
|
291
|
-
// a safe end() function that can be called multiple times but
|
|
292
|
-
// only perform a single checkin
|
|
293
|
-
const end = this.fdman.checkinfn(p, fd)
|
|
294
|
-
|
|
295
|
-
this.cache.stat.get(fd + ':' + p, (er, stat) => {
|
|
296
|
-
if (er) {
|
|
297
|
-
if (next && this.opt.passthrough === true && this._index === false) {
|
|
298
|
-
return next()
|
|
299
|
-
}
|
|
300
|
-
end()
|
|
301
|
-
return this.error(er, res)
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
const isDirectory = stat.isDirectory()
|
|
305
|
-
|
|
306
|
-
if (isDirectory) {
|
|
307
|
-
end() // we won't need this fd for a directory in any case
|
|
308
|
-
if (next && this.opt.passthrough === true && this._index === false) {
|
|
309
|
-
// this is done before if-modified-since and if-non-match checks so
|
|
310
|
-
// cached modified and etag values won't return 304's if we've since
|
|
311
|
-
// switched to !index. See Issue #51.
|
|
312
|
-
return next()
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
let ims = req.headers['if-modified-since']
|
|
317
|
-
if (ims) {
|
|
318
|
-
ims = new Date(ims).getTime()
|
|
319
|
-
}
|
|
320
|
-
if (ims && ims >= stat.mtime.getTime()) {
|
|
321
|
-
res.statusCode = 304
|
|
322
|
-
res.end()
|
|
323
|
-
return end()
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
const etag = getEtag(stat)
|
|
327
|
-
if (req.headers['if-none-match'] === etag) {
|
|
328
|
-
res.statusCode = 304
|
|
329
|
-
res.end()
|
|
330
|
-
return end()
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
// only set headers once we're sure we'll be serving this request
|
|
334
|
-
if (!res.getHeader('cache-control') && this._cacheControl) {
|
|
335
|
-
res.setHeader('cache-control', this._cacheControl)
|
|
336
|
-
}
|
|
337
|
-
res.setHeader('last-modified', stat.mtime.toUTCString())
|
|
338
|
-
res.setHeader('etag', etag)
|
|
339
|
-
|
|
340
|
-
if (this.opt.cors) {
|
|
341
|
-
res.setHeader('Access-Control-Allow-Origin', '*')
|
|
342
|
-
res.setHeader('Access-Control-Allow-Headers',
|
|
343
|
-
'Origin, X-Requested-With, Content-Type, Accept, Range')
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
return isDirectory
|
|
347
|
-
? this.index(p, req, res)
|
|
348
|
-
: this.file(p, fd, stat, etag, req, res, end)
|
|
349
|
-
})
|
|
350
|
-
})
|
|
353
|
+
)
|
|
351
354
|
|
|
352
355
|
return true
|
|
353
356
|
}
|
|
@@ -392,16 +395,15 @@ class Mount {
|
|
|
392
395
|
return
|
|
393
396
|
}
|
|
394
397
|
|
|
395
|
-
this.cache.index.
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
})
|
|
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
|
+
)
|
|
405
407
|
}
|
|
406
408
|
|
|
407
409
|
file (p, fd, stat, etag, req, res, end) {
|
|
@@ -425,39 +427,35 @@ class Mount {
|
|
|
425
427
|
const key = stat.size + ':' + etag
|
|
426
428
|
const gz = this.opt.gzip !== false && getGz(p, req)
|
|
427
429
|
|
|
428
|
-
this.cache.content.get(key
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
res.setHeader('content-length', content.length)
|
|
442
|
-
res.end(content)
|
|
443
|
-
}
|
|
444
|
-
})
|
|
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
|
+
}
|
|
445
443
|
}
|
|
446
444
|
|
|
447
445
|
streamFile (p, fd, stat, etag, req, res, end) {
|
|
448
|
-
const streamOpt = { fd
|
|
446
|
+
const streamOpt = { fd, start: 0, end: stat.size }
|
|
449
447
|
let stream = fs.createReadStream(p, streamOpt)
|
|
450
448
|
stream.destroy = () => {}
|
|
451
449
|
|
|
452
450
|
// gzip only if not explicitly turned off or client doesn't accept it
|
|
453
451
|
const gzOpt = this.opt.gzip !== false
|
|
454
452
|
const gz = gzOpt && getGz(p, req)
|
|
455
|
-
const cachable = this.cache.content.
|
|
453
|
+
const cachable = this.cache.content.maxSize > stat.size
|
|
456
454
|
let gzstr
|
|
457
455
|
|
|
458
456
|
// need a gzipped version for the cache, so do it regardless of what the client wants
|
|
459
457
|
if (gz || (gzOpt && cachable)) {
|
|
460
|
-
gzstr = zlib.
|
|
458
|
+
gzstr = zlib.createGzip()
|
|
461
459
|
}
|
|
462
460
|
|
|
463
461
|
// too late to effectively handle any errors.
|
|
@@ -537,57 +535,56 @@ class Mount {
|
|
|
537
535
|
'<h1>Index of ' + t + '</h1>' +
|
|
538
536
|
'<hr><pre><a href="../">../</a>\n'
|
|
539
537
|
|
|
540
|
-
this.cache.readdir.
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
let nameLen = 0
|
|
546
|
-
let sizeLen = 0
|
|
538
|
+
this.cache.readdir.fetch(p).then(
|
|
539
|
+
(data) => {
|
|
540
|
+
let nameLen = 0
|
|
541
|
+
let sizeLen = 0
|
|
547
542
|
|
|
548
|
-
|
|
549
|
-
|
|
543
|
+
Object.keys(data).map((f) => {
|
|
544
|
+
const d = data[f]
|
|
550
545
|
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
546
|
+
let name = f
|
|
547
|
+
.replace(/"/g, '"')
|
|
548
|
+
.replace(/</g, '<')
|
|
549
|
+
.replace(/>/g, '>')
|
|
550
|
+
.replace(/'/g, ''')
|
|
556
551
|
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
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
|
+
})
|
|
587
582
|
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
583
|
+
str += '</pre><hr></body></html>'
|
|
584
|
+
cb(null, Buffer.from(str))
|
|
585
|
+
},
|
|
586
|
+
(er) => cb(er)
|
|
587
|
+
)
|
|
591
588
|
}
|
|
592
589
|
|
|
593
590
|
_loadReaddir (p, cb) {
|
|
@@ -608,16 +605,16 @@ class Mount {
|
|
|
608
605
|
data = {}
|
|
609
606
|
files.forEach((file) => {
|
|
610
607
|
const pf = path.join(p, file)
|
|
611
|
-
this.cache.stat.
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
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
|
+
)
|
|
621
618
|
})
|
|
622
619
|
})
|
|
623
620
|
|
|
@@ -646,11 +643,11 @@ class Mount {
|
|
|
646
643
|
}
|
|
647
644
|
}
|
|
648
645
|
|
|
649
|
-
_loadContent () {
|
|
646
|
+
_loadContent (_, cb) {
|
|
650
647
|
// this function should never be called.
|
|
651
648
|
// we check if the thing is in the cache, and if not, stream it in
|
|
652
|
-
// manually. this.cache.content.
|
|
653
|
-
|
|
649
|
+
// manually. this.cache.content.fetch() should not ever happen.
|
|
650
|
+
return cb(new Error('This should never happen'))
|
|
654
651
|
}
|
|
655
652
|
}
|
|
656
653
|
|
package/test/basic.js
CHANGED
|
@@ -125,17 +125,8 @@ test('shenanigans', (t) => {
|
|
|
125
125
|
if (er) {
|
|
126
126
|
throw er
|
|
127
127
|
}
|
|
128
|
-
|
|
129
|
-
t.
|
|
130
|
-
})
|
|
131
|
-
})
|
|
132
|
-
|
|
133
|
-
test('shenanigans2', (t) => {
|
|
134
|
-
req('/test//foo/%2e%2E', (er, res) => {
|
|
135
|
-
if (er) {
|
|
136
|
-
throw er
|
|
137
|
-
}
|
|
138
|
-
t.equal(res.statusCode, 403)
|
|
128
|
+
// resolves to simply ./etc/passwd
|
|
129
|
+
t.equal(res.statusCode, 404)
|
|
139
130
|
t.end()
|
|
140
131
|
})
|
|
141
132
|
})
|
package/test/cli/common.js
CHANGED
package/test/common.js
CHANGED
|
@@ -12,7 +12,10 @@ test('does not gzip first response', (t) => {
|
|
|
12
12
|
t.notOk(res.headers['content-encoding'])
|
|
13
13
|
t.notOk(res.headers['x-from-cache'])
|
|
14
14
|
t.equal(body.toString(), stExpect)
|
|
15
|
-
|
|
15
|
+
// response is delivered before cache is set, so we'll give it a tiny bit
|
|
16
|
+
// of grace to close out on the server side before starting the next
|
|
17
|
+
// test
|
|
18
|
+
setTimeout(t.end, 100)
|
|
16
19
|
})
|
|
17
20
|
})
|
|
18
21
|
|
package/test/multi-mount.js
CHANGED
|
@@ -47,13 +47,13 @@ function req (url, headers, cb) {
|
|
|
47
47
|
request({
|
|
48
48
|
encoding: null,
|
|
49
49
|
url: `http://localhost:${port}${url}`,
|
|
50
|
-
headers
|
|
50
|
+
headers,
|
|
51
51
|
agentOptions: { maxSockets: 50 }
|
|
52
52
|
}, next)
|
|
53
53
|
request({
|
|
54
54
|
encoding: null,
|
|
55
55
|
url: `http://localhost:${(port + 1)}${url}`,
|
|
56
|
-
headers
|
|
56
|
+
headers,
|
|
57
57
|
agentOptions: { maxSockets: 50 }
|
|
58
58
|
}, next)
|
|
59
59
|
|
|
@@ -76,7 +76,7 @@ function req (url, headers, cb) {
|
|
|
76
76
|
assert.strictEqual(`${body}`, `${prev.body}`)
|
|
77
77
|
return cb(er, res, body)
|
|
78
78
|
}
|
|
79
|
-
prev = { res
|
|
79
|
+
prev = { res, body }
|
|
80
80
|
}
|
|
81
81
|
}
|
|
82
82
|
|
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.
|
|
14
|
-
t.same(mount._this.cache.stat.
|
|
15
|
-
t.same(mount._this.cache.index.
|
|
16
|
-
t.same(mount._this.cache.readdir.
|
|
17
|
-
t.same(mount._this.cache.content.
|
|
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
|
})
|