@wooksjs/http-static 0.4.10 → 0.4.11
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/dist/index.cjs +162 -195
- package/dist/index.mjs +162 -195
- package/package.json +2 -2
package/dist/index.cjs
CHANGED
|
@@ -4,38 +4,6 @@ var eventHttp = require('@wooksjs/event-http');
|
|
|
4
4
|
var fs = require('fs');
|
|
5
5
|
var path = require('path');
|
|
6
6
|
|
|
7
|
-
/******************************************************************************
|
|
8
|
-
Copyright (c) Microsoft Corporation.
|
|
9
|
-
|
|
10
|
-
Permission to use, copy, modify, and/or distribute this software for any
|
|
11
|
-
purpose with or without fee is hereby granted.
|
|
12
|
-
|
|
13
|
-
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
|
14
|
-
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
|
15
|
-
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
|
16
|
-
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
|
17
|
-
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
|
18
|
-
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
|
19
|
-
PERFORMANCE OF THIS SOFTWARE.
|
|
20
|
-
***************************************************************************** */
|
|
21
|
-
/* global Reflect, Promise, SuppressedError, Symbol */
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
function __awaiter(thisArg, _arguments, P, generator) {
|
|
25
|
-
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
26
|
-
return new (P || (P = Promise))(function (resolve, reject) {
|
|
27
|
-
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
28
|
-
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
29
|
-
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
30
|
-
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
31
|
-
});
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
|
|
35
|
-
var e = new Error(message);
|
|
36
|
-
return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
|
|
37
|
-
};
|
|
38
|
-
|
|
39
7
|
const extensions = {
|
|
40
8
|
'123': 'application/vnd.lotus-1-2-3',
|
|
41
9
|
ez: 'application/andrew-inset',
|
|
@@ -1230,130 +1198,131 @@ function normalizePath(filePath, baseDir) {
|
|
|
1230
1198
|
}
|
|
1231
1199
|
|
|
1232
1200
|
const { stat, readdir } = fs.promises;
|
|
1233
|
-
function serveFile(filePath, options = {}) {
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1201
|
+
async function serveFile(filePath, options = {}) {
|
|
1202
|
+
if (!options.allowDotDot && (/\/\.\.\//.test(filePath) || /^\.\.\//.test(filePath))) {
|
|
1203
|
+
throw new Error('Parent Traversal ("/../") is not allowed.');
|
|
1204
|
+
}
|
|
1205
|
+
const { restoreCtx } = eventHttp.useHttpContext();
|
|
1206
|
+
const { status } = eventHttp.useResponse();
|
|
1207
|
+
const { setHeader, removeHeader } = eventHttp.useSetHeaders();
|
|
1208
|
+
const headers = eventHttp.useHeaders();
|
|
1209
|
+
const { method, url } = eventHttp.useRequest();
|
|
1210
|
+
const { setCacheControl, setExpires, setPragmaNoCache } = eventHttp.useSetCacheControl();
|
|
1211
|
+
const normalizedPath = normalizePath(filePath, options.baseDir);
|
|
1212
|
+
let fileStats;
|
|
1213
|
+
try {
|
|
1214
|
+
fileStats = await stat(normalizedPath);
|
|
1215
|
+
}
|
|
1216
|
+
catch (e) {
|
|
1217
|
+
if (options.defaultExt) {
|
|
1218
|
+
const ext = path.extname(filePath);
|
|
1219
|
+
if (!ext) {
|
|
1220
|
+
restoreCtx();
|
|
1221
|
+
return serveFile(filePath + '.' + options.defaultExt);
|
|
1222
|
+
}
|
|
1237
1223
|
}
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1224
|
+
throw new eventHttp.HttpError(404);
|
|
1225
|
+
}
|
|
1226
|
+
status(200);
|
|
1227
|
+
// if-none-match & if-modified-since processing start
|
|
1228
|
+
// rfc7232
|
|
1229
|
+
const etag = `"${[
|
|
1230
|
+
fileStats.ino,
|
|
1231
|
+
fileStats.size,
|
|
1232
|
+
fileStats.mtime.toISOString(),
|
|
1233
|
+
].join('-')}"`;
|
|
1234
|
+
const lastModified = new Date(fileStats.mtime);
|
|
1235
|
+
if (isNotModified(etag, lastModified, headers['if-none-match'] || '', headers['if-modified-since'] || '')) {
|
|
1236
|
+
status(304);
|
|
1237
|
+
return '';
|
|
1238
|
+
}
|
|
1239
|
+
// if-none-match & if-modified-since processing end
|
|
1240
|
+
setHeader('etag', etag);
|
|
1241
|
+
setHeader('last-modified', lastModified.toUTCString());
|
|
1242
|
+
if (options.cacheControl !== undefined) {
|
|
1243
|
+
setCacheControl(options.cacheControl);
|
|
1244
|
+
}
|
|
1245
|
+
if (options.expires) {
|
|
1246
|
+
setExpires(options.expires);
|
|
1247
|
+
}
|
|
1248
|
+
if (options.pragmaNoCache) {
|
|
1249
|
+
setPragmaNoCache(options.pragmaNoCache);
|
|
1250
|
+
}
|
|
1251
|
+
if (fileStats.isDirectory()) {
|
|
1252
|
+
if (options.listDirectory) {
|
|
1253
|
+
restoreCtx();
|
|
1254
|
+
return listDirectory(normalizedPath);
|
|
1248
1255
|
}
|
|
1249
|
-
|
|
1250
|
-
if (
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
+
else if (options.index) {
|
|
1257
|
+
if (filePath[filePath.length - 1] !== '/' &&
|
|
1258
|
+
url &&
|
|
1259
|
+
url[url.length - 1] !== '/') {
|
|
1260
|
+
return new eventHttp.BaseHttpResponse()
|
|
1261
|
+
.setStatus(302)
|
|
1262
|
+
.setHeader('location', url + '/');
|
|
1256
1263
|
}
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
const etag = `"${[
|
|
1263
|
-
fileStats.ino,
|
|
1264
|
-
fileStats.size,
|
|
1265
|
-
fileStats.mtime.toISOString(),
|
|
1266
|
-
].join('-')}"`;
|
|
1267
|
-
const lastModified = new Date(fileStats.mtime);
|
|
1268
|
-
if (isNotModified(etag, lastModified, headers['if-none-match'] || '', headers['if-modified-since'] || '')) {
|
|
1269
|
-
status(304);
|
|
1270
|
-
return '';
|
|
1264
|
+
restoreCtx();
|
|
1265
|
+
return serveFile(path.join(filePath, options.index), {
|
|
1266
|
+
...options,
|
|
1267
|
+
index: '',
|
|
1268
|
+
});
|
|
1271
1269
|
}
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1270
|
+
removeHeader('etag');
|
|
1271
|
+
removeHeader('last-modified');
|
|
1272
|
+
throw new eventHttp.HttpError(404);
|
|
1273
|
+
}
|
|
1274
|
+
// range header processing start
|
|
1275
|
+
let range = headers.range;
|
|
1276
|
+
let start, end;
|
|
1277
|
+
let size = fileStats.size;
|
|
1278
|
+
if (range) {
|
|
1279
|
+
const rangeParts = range
|
|
1280
|
+
.trim()
|
|
1281
|
+
.replace(/bytes=/, '')
|
|
1282
|
+
.split('-');
|
|
1283
|
+
const [s, e] = rangeParts;
|
|
1284
|
+
start = parseInt(s);
|
|
1285
|
+
end = e ? parseInt(e) : size - 1;
|
|
1286
|
+
end = Math.min(size - 1, end);
|
|
1287
|
+
if (start > end || isNaN(start) || isNaN(end)) {
|
|
1288
|
+
throw new eventHttp.HttpError(416);
|
|
1277
1289
|
}
|
|
1278
|
-
|
|
1279
|
-
|
|
1290
|
+
size = end - start + 1;
|
|
1291
|
+
// if-range processing start
|
|
1292
|
+
// rfc7233#section-3.2\
|
|
1293
|
+
const ifRange = headers['if-range'] || '';
|
|
1294
|
+
const ifRangeTag = ifRange[0] === '"' ? ifRange : '';
|
|
1295
|
+
const ifRangeDate = ifRangeTag ? '' : ifRange;
|
|
1296
|
+
if (ifRange &&
|
|
1297
|
+
!isNotModified(etag, lastModified, ifRangeTag, ifRangeDate)) {
|
|
1298
|
+
// If the validator does not match, the server MUST ignore
|
|
1299
|
+
// the Range header field.
|
|
1300
|
+
status(200);
|
|
1301
|
+
size = fileStats.size;
|
|
1302
|
+
range = '';
|
|
1280
1303
|
}
|
|
1281
|
-
|
|
1282
|
-
|
|
1304
|
+
else {
|
|
1305
|
+
// If the validator given in the If-Range header field matches the
|
|
1306
|
+
// current validator for the selected representation of the target
|
|
1307
|
+
// resource, then the server SHOULD process the Range header field as
|
|
1308
|
+
// requested.
|
|
1309
|
+
setHeader('content-range', `bytes ${start}-${end}/${fileStats.size}`);
|
|
1310
|
+
status(206);
|
|
1283
1311
|
}
|
|
1284
|
-
if
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
return new eventHttp.BaseHttpResponse()
|
|
1294
|
-
.setStatus(302)
|
|
1295
|
-
.setHeader('location', url + '/');
|
|
1296
|
-
}
|
|
1297
|
-
restoreCtx();
|
|
1298
|
-
return serveFile(path.join(filePath, options.index), Object.assign(Object.assign({}, options), { index: '' }));
|
|
1299
|
-
}
|
|
1300
|
-
removeHeader('etag');
|
|
1301
|
-
removeHeader('last-modified');
|
|
1302
|
-
throw new eventHttp.HttpError(404);
|
|
1303
|
-
}
|
|
1304
|
-
// range header processing start
|
|
1305
|
-
let range = headers.range;
|
|
1306
|
-
let start, end;
|
|
1307
|
-
let size = fileStats.size;
|
|
1308
|
-
if (range) {
|
|
1309
|
-
const rangeParts = range
|
|
1310
|
-
.trim()
|
|
1311
|
-
.replace(/bytes=/, '')
|
|
1312
|
-
.split('-');
|
|
1313
|
-
const [s, e] = rangeParts;
|
|
1314
|
-
start = parseInt(s);
|
|
1315
|
-
end = e ? parseInt(e) : size - 1;
|
|
1316
|
-
end = Math.min(size - 1, end);
|
|
1317
|
-
if (start > end || isNaN(start) || isNaN(end)) {
|
|
1318
|
-
throw new eventHttp.HttpError(416);
|
|
1319
|
-
}
|
|
1320
|
-
size = end - start + 1;
|
|
1321
|
-
// if-range processing start
|
|
1322
|
-
// rfc7233#section-3.2\
|
|
1323
|
-
const ifRange = headers['if-range'] || '';
|
|
1324
|
-
const ifRangeTag = ifRange[0] === '"' ? ifRange : '';
|
|
1325
|
-
const ifRangeDate = ifRangeTag ? '' : ifRange;
|
|
1326
|
-
if (ifRange &&
|
|
1327
|
-
!isNotModified(etag, lastModified, ifRangeTag, ifRangeDate)) {
|
|
1328
|
-
// If the validator does not match, the server MUST ignore
|
|
1329
|
-
// the Range header field.
|
|
1330
|
-
status(200);
|
|
1331
|
-
size = fileStats.size;
|
|
1332
|
-
range = '';
|
|
1333
|
-
}
|
|
1334
|
-
else {
|
|
1335
|
-
// If the validator given in the If-Range header field matches the
|
|
1336
|
-
// current validator for the selected representation of the target
|
|
1337
|
-
// resource, then the server SHOULD process the Range header field as
|
|
1338
|
-
// requested.
|
|
1339
|
-
setHeader('content-range', `bytes ${start}-${end}/${fileStats.size}`);
|
|
1340
|
-
status(206);
|
|
1341
|
-
}
|
|
1342
|
-
// if-range processing end
|
|
1343
|
-
}
|
|
1344
|
-
// range header processing end
|
|
1345
|
-
setHeader('accept-ranges', 'bytes');
|
|
1346
|
-
setHeader('content-type', getMimeType(normalizedPath) || 'application/octet-stream');
|
|
1347
|
-
setHeader('content-length', size);
|
|
1348
|
-
if (options.headers) {
|
|
1349
|
-
for (const header of Object.keys(options.headers)) {
|
|
1350
|
-
setHeader(header, options.headers[header]);
|
|
1351
|
-
}
|
|
1312
|
+
// if-range processing end
|
|
1313
|
+
}
|
|
1314
|
+
// range header processing end
|
|
1315
|
+
setHeader('accept-ranges', 'bytes');
|
|
1316
|
+
setHeader('content-type', getMimeType(normalizedPath) || 'application/octet-stream');
|
|
1317
|
+
setHeader('content-length', size);
|
|
1318
|
+
if (options.headers) {
|
|
1319
|
+
for (const header of Object.keys(options.headers)) {
|
|
1320
|
+
setHeader(header, options.headers[header]);
|
|
1352
1321
|
}
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1322
|
+
}
|
|
1323
|
+
return method === 'HEAD'
|
|
1324
|
+
? ''
|
|
1325
|
+
: fs.createReadStream(normalizedPath, !!range ? { start, end } : undefined);
|
|
1357
1326
|
}
|
|
1358
1327
|
// function toWeak(etag: string): string {
|
|
1359
1328
|
// return `W/${etag}`
|
|
@@ -1384,53 +1353,51 @@ function isNotModified(etag, lastModified, clientEtag, clientLM) {
|
|
|
1384
1353
|
}
|
|
1385
1354
|
return false;
|
|
1386
1355
|
}
|
|
1387
|
-
function listDirectory(dirPath) {
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
(d.size
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
'</ul></body></html>');
|
|
1433
|
-
});
|
|
1356
|
+
async function listDirectory(dirPath) {
|
|
1357
|
+
const { setContentType } = eventHttp.useSetHeaders();
|
|
1358
|
+
const { url } = eventHttp.useRequest();
|
|
1359
|
+
const list = await readdir(dirPath);
|
|
1360
|
+
const promises = [];
|
|
1361
|
+
let detailedList = [];
|
|
1362
|
+
for (const item of list) {
|
|
1363
|
+
promises.push({ name: item, promise: stat(path.join(dirPath, item)) });
|
|
1364
|
+
}
|
|
1365
|
+
for (const item of promises) {
|
|
1366
|
+
const data = await item.promise;
|
|
1367
|
+
detailedList.push({
|
|
1368
|
+
name: item.name,
|
|
1369
|
+
size: data.size,
|
|
1370
|
+
mtime: data.mtime,
|
|
1371
|
+
dir: data.isDirectory(),
|
|
1372
|
+
});
|
|
1373
|
+
}
|
|
1374
|
+
detailedList = detailedList.sort((a, b) => a.dir === b.dir ? (a.name > b.name ? 1 : -1) : a.dir > b.dir ? -1 : 1);
|
|
1375
|
+
detailedList.unshift({ name: '..', dir: true });
|
|
1376
|
+
setContentType('text/html');
|
|
1377
|
+
const styles = '<style type="text/css">\nhtml { font-family: monospace }\n' +
|
|
1378
|
+
'span { padding: 0px 2px }\n' +
|
|
1379
|
+
'.text { text-overflow: ellipsis; overflow: hidden; white-space: nowrap; }\n' +
|
|
1380
|
+
'.icon { width: 20px; display: inline-block; text-align: center; }\n' +
|
|
1381
|
+
'.name { width: 250px; display: inline-block; }\n' +
|
|
1382
|
+
'.size { width: 80px; display: inline-block; color: grey; text-align: right; }\n' +
|
|
1383
|
+
'.date { width: 200px; display: inline-block; color: grey; text-align: right; }\n' +
|
|
1384
|
+
'\n</style>';
|
|
1385
|
+
return ('<html><head><title>Dir</title> ' +
|
|
1386
|
+
styles +
|
|
1387
|
+
' </head><body><ul>' +
|
|
1388
|
+
detailedList
|
|
1389
|
+
.map((d) => `<li> <span class="icon">${d.dir ? '📁' : '🗎'}</span>` +
|
|
1390
|
+
`<a href="${path.join(url || '', d.name)}"><span class="name text">${d.name}</span></a>` +
|
|
1391
|
+
`<span class="size text">${(d.size &&
|
|
1392
|
+
(d.size > 10000
|
|
1393
|
+
? Math.round(d.size / 1024 / 1024).toString() +
|
|
1394
|
+
'Mb'
|
|
1395
|
+
: Math.round(d.size / 1024).toString() +
|
|
1396
|
+
'Kb')) ||
|
|
1397
|
+
''}</span>` +
|
|
1398
|
+
`<span class="date text">${(d.mtime && d.mtime.toISOString()) || ''}</li>`)
|
|
1399
|
+
.join('\n') +
|
|
1400
|
+
'</ul></body></html>');
|
|
1434
1401
|
}
|
|
1435
1402
|
|
|
1436
1403
|
exports.serveFile = serveFile;
|
package/dist/index.mjs
CHANGED
|
@@ -2,38 +2,6 @@ import { useHttpContext, useResponse, useSetHeaders, useHeaders, useRequest, use
|
|
|
2
2
|
import { createReadStream, promises } from 'fs';
|
|
3
3
|
import path from 'path';
|
|
4
4
|
|
|
5
|
-
/******************************************************************************
|
|
6
|
-
Copyright (c) Microsoft Corporation.
|
|
7
|
-
|
|
8
|
-
Permission to use, copy, modify, and/or distribute this software for any
|
|
9
|
-
purpose with or without fee is hereby granted.
|
|
10
|
-
|
|
11
|
-
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
|
12
|
-
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
|
13
|
-
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
|
14
|
-
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
|
15
|
-
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
|
16
|
-
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
|
17
|
-
PERFORMANCE OF THIS SOFTWARE.
|
|
18
|
-
***************************************************************************** */
|
|
19
|
-
/* global Reflect, Promise, SuppressedError, Symbol */
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
function __awaiter(thisArg, _arguments, P, generator) {
|
|
23
|
-
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
24
|
-
return new (P || (P = Promise))(function (resolve, reject) {
|
|
25
|
-
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
26
|
-
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
27
|
-
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
28
|
-
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
29
|
-
});
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
|
|
33
|
-
var e = new Error(message);
|
|
34
|
-
return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
|
|
35
|
-
};
|
|
36
|
-
|
|
37
5
|
const extensions = {
|
|
38
6
|
'123': 'application/vnd.lotus-1-2-3',
|
|
39
7
|
ez: 'application/andrew-inset',
|
|
@@ -1228,130 +1196,131 @@ function normalizePath(filePath, baseDir) {
|
|
|
1228
1196
|
}
|
|
1229
1197
|
|
|
1230
1198
|
const { stat, readdir } = promises;
|
|
1231
|
-
function serveFile(filePath, options = {}) {
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1199
|
+
async function serveFile(filePath, options = {}) {
|
|
1200
|
+
if (!options.allowDotDot && (/\/\.\.\//.test(filePath) || /^\.\.\//.test(filePath))) {
|
|
1201
|
+
throw new Error('Parent Traversal ("/../") is not allowed.');
|
|
1202
|
+
}
|
|
1203
|
+
const { restoreCtx } = useHttpContext();
|
|
1204
|
+
const { status } = useResponse();
|
|
1205
|
+
const { setHeader, removeHeader } = useSetHeaders();
|
|
1206
|
+
const headers = useHeaders();
|
|
1207
|
+
const { method, url } = useRequest();
|
|
1208
|
+
const { setCacheControl, setExpires, setPragmaNoCache } = useSetCacheControl();
|
|
1209
|
+
const normalizedPath = normalizePath(filePath, options.baseDir);
|
|
1210
|
+
let fileStats;
|
|
1211
|
+
try {
|
|
1212
|
+
fileStats = await stat(normalizedPath);
|
|
1213
|
+
}
|
|
1214
|
+
catch (e) {
|
|
1215
|
+
if (options.defaultExt) {
|
|
1216
|
+
const ext = path.extname(filePath);
|
|
1217
|
+
if (!ext) {
|
|
1218
|
+
restoreCtx();
|
|
1219
|
+
return serveFile(filePath + '.' + options.defaultExt);
|
|
1220
|
+
}
|
|
1235
1221
|
}
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1222
|
+
throw new HttpError(404);
|
|
1223
|
+
}
|
|
1224
|
+
status(200);
|
|
1225
|
+
// if-none-match & if-modified-since processing start
|
|
1226
|
+
// rfc7232
|
|
1227
|
+
const etag = `"${[
|
|
1228
|
+
fileStats.ino,
|
|
1229
|
+
fileStats.size,
|
|
1230
|
+
fileStats.mtime.toISOString(),
|
|
1231
|
+
].join('-')}"`;
|
|
1232
|
+
const lastModified = new Date(fileStats.mtime);
|
|
1233
|
+
if (isNotModified(etag, lastModified, headers['if-none-match'] || '', headers['if-modified-since'] || '')) {
|
|
1234
|
+
status(304);
|
|
1235
|
+
return '';
|
|
1236
|
+
}
|
|
1237
|
+
// if-none-match & if-modified-since processing end
|
|
1238
|
+
setHeader('etag', etag);
|
|
1239
|
+
setHeader('last-modified', lastModified.toUTCString());
|
|
1240
|
+
if (options.cacheControl !== undefined) {
|
|
1241
|
+
setCacheControl(options.cacheControl);
|
|
1242
|
+
}
|
|
1243
|
+
if (options.expires) {
|
|
1244
|
+
setExpires(options.expires);
|
|
1245
|
+
}
|
|
1246
|
+
if (options.pragmaNoCache) {
|
|
1247
|
+
setPragmaNoCache(options.pragmaNoCache);
|
|
1248
|
+
}
|
|
1249
|
+
if (fileStats.isDirectory()) {
|
|
1250
|
+
if (options.listDirectory) {
|
|
1251
|
+
restoreCtx();
|
|
1252
|
+
return listDirectory(normalizedPath);
|
|
1246
1253
|
}
|
|
1247
|
-
|
|
1248
|
-
if (
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
+
else if (options.index) {
|
|
1255
|
+
if (filePath[filePath.length - 1] !== '/' &&
|
|
1256
|
+
url &&
|
|
1257
|
+
url[url.length - 1] !== '/') {
|
|
1258
|
+
return new BaseHttpResponse()
|
|
1259
|
+
.setStatus(302)
|
|
1260
|
+
.setHeader('location', url + '/');
|
|
1254
1261
|
}
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
const etag = `"${[
|
|
1261
|
-
fileStats.ino,
|
|
1262
|
-
fileStats.size,
|
|
1263
|
-
fileStats.mtime.toISOString(),
|
|
1264
|
-
].join('-')}"`;
|
|
1265
|
-
const lastModified = new Date(fileStats.mtime);
|
|
1266
|
-
if (isNotModified(etag, lastModified, headers['if-none-match'] || '', headers['if-modified-since'] || '')) {
|
|
1267
|
-
status(304);
|
|
1268
|
-
return '';
|
|
1262
|
+
restoreCtx();
|
|
1263
|
+
return serveFile(path.join(filePath, options.index), {
|
|
1264
|
+
...options,
|
|
1265
|
+
index: '',
|
|
1266
|
+
});
|
|
1269
1267
|
}
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1268
|
+
removeHeader('etag');
|
|
1269
|
+
removeHeader('last-modified');
|
|
1270
|
+
throw new HttpError(404);
|
|
1271
|
+
}
|
|
1272
|
+
// range header processing start
|
|
1273
|
+
let range = headers.range;
|
|
1274
|
+
let start, end;
|
|
1275
|
+
let size = fileStats.size;
|
|
1276
|
+
if (range) {
|
|
1277
|
+
const rangeParts = range
|
|
1278
|
+
.trim()
|
|
1279
|
+
.replace(/bytes=/, '')
|
|
1280
|
+
.split('-');
|
|
1281
|
+
const [s, e] = rangeParts;
|
|
1282
|
+
start = parseInt(s);
|
|
1283
|
+
end = e ? parseInt(e) : size - 1;
|
|
1284
|
+
end = Math.min(size - 1, end);
|
|
1285
|
+
if (start > end || isNaN(start) || isNaN(end)) {
|
|
1286
|
+
throw new HttpError(416);
|
|
1275
1287
|
}
|
|
1276
|
-
|
|
1277
|
-
|
|
1288
|
+
size = end - start + 1;
|
|
1289
|
+
// if-range processing start
|
|
1290
|
+
// rfc7233#section-3.2\
|
|
1291
|
+
const ifRange = headers['if-range'] || '';
|
|
1292
|
+
const ifRangeTag = ifRange[0] === '"' ? ifRange : '';
|
|
1293
|
+
const ifRangeDate = ifRangeTag ? '' : ifRange;
|
|
1294
|
+
if (ifRange &&
|
|
1295
|
+
!isNotModified(etag, lastModified, ifRangeTag, ifRangeDate)) {
|
|
1296
|
+
// If the validator does not match, the server MUST ignore
|
|
1297
|
+
// the Range header field.
|
|
1298
|
+
status(200);
|
|
1299
|
+
size = fileStats.size;
|
|
1300
|
+
range = '';
|
|
1278
1301
|
}
|
|
1279
|
-
|
|
1280
|
-
|
|
1302
|
+
else {
|
|
1303
|
+
// If the validator given in the If-Range header field matches the
|
|
1304
|
+
// current validator for the selected representation of the target
|
|
1305
|
+
// resource, then the server SHOULD process the Range header field as
|
|
1306
|
+
// requested.
|
|
1307
|
+
setHeader('content-range', `bytes ${start}-${end}/${fileStats.size}`);
|
|
1308
|
+
status(206);
|
|
1281
1309
|
}
|
|
1282
|
-
if
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
return new BaseHttpResponse()
|
|
1292
|
-
.setStatus(302)
|
|
1293
|
-
.setHeader('location', url + '/');
|
|
1294
|
-
}
|
|
1295
|
-
restoreCtx();
|
|
1296
|
-
return serveFile(path.join(filePath, options.index), Object.assign(Object.assign({}, options), { index: '' }));
|
|
1297
|
-
}
|
|
1298
|
-
removeHeader('etag');
|
|
1299
|
-
removeHeader('last-modified');
|
|
1300
|
-
throw new HttpError(404);
|
|
1301
|
-
}
|
|
1302
|
-
// range header processing start
|
|
1303
|
-
let range = headers.range;
|
|
1304
|
-
let start, end;
|
|
1305
|
-
let size = fileStats.size;
|
|
1306
|
-
if (range) {
|
|
1307
|
-
const rangeParts = range
|
|
1308
|
-
.trim()
|
|
1309
|
-
.replace(/bytes=/, '')
|
|
1310
|
-
.split('-');
|
|
1311
|
-
const [s, e] = rangeParts;
|
|
1312
|
-
start = parseInt(s);
|
|
1313
|
-
end = e ? parseInt(e) : size - 1;
|
|
1314
|
-
end = Math.min(size - 1, end);
|
|
1315
|
-
if (start > end || isNaN(start) || isNaN(end)) {
|
|
1316
|
-
throw new HttpError(416);
|
|
1317
|
-
}
|
|
1318
|
-
size = end - start + 1;
|
|
1319
|
-
// if-range processing start
|
|
1320
|
-
// rfc7233#section-3.2\
|
|
1321
|
-
const ifRange = headers['if-range'] || '';
|
|
1322
|
-
const ifRangeTag = ifRange[0] === '"' ? ifRange : '';
|
|
1323
|
-
const ifRangeDate = ifRangeTag ? '' : ifRange;
|
|
1324
|
-
if (ifRange &&
|
|
1325
|
-
!isNotModified(etag, lastModified, ifRangeTag, ifRangeDate)) {
|
|
1326
|
-
// If the validator does not match, the server MUST ignore
|
|
1327
|
-
// the Range header field.
|
|
1328
|
-
status(200);
|
|
1329
|
-
size = fileStats.size;
|
|
1330
|
-
range = '';
|
|
1331
|
-
}
|
|
1332
|
-
else {
|
|
1333
|
-
// If the validator given in the If-Range header field matches the
|
|
1334
|
-
// current validator for the selected representation of the target
|
|
1335
|
-
// resource, then the server SHOULD process the Range header field as
|
|
1336
|
-
// requested.
|
|
1337
|
-
setHeader('content-range', `bytes ${start}-${end}/${fileStats.size}`);
|
|
1338
|
-
status(206);
|
|
1339
|
-
}
|
|
1340
|
-
// if-range processing end
|
|
1341
|
-
}
|
|
1342
|
-
// range header processing end
|
|
1343
|
-
setHeader('accept-ranges', 'bytes');
|
|
1344
|
-
setHeader('content-type', getMimeType(normalizedPath) || 'application/octet-stream');
|
|
1345
|
-
setHeader('content-length', size);
|
|
1346
|
-
if (options.headers) {
|
|
1347
|
-
for (const header of Object.keys(options.headers)) {
|
|
1348
|
-
setHeader(header, options.headers[header]);
|
|
1349
|
-
}
|
|
1310
|
+
// if-range processing end
|
|
1311
|
+
}
|
|
1312
|
+
// range header processing end
|
|
1313
|
+
setHeader('accept-ranges', 'bytes');
|
|
1314
|
+
setHeader('content-type', getMimeType(normalizedPath) || 'application/octet-stream');
|
|
1315
|
+
setHeader('content-length', size);
|
|
1316
|
+
if (options.headers) {
|
|
1317
|
+
for (const header of Object.keys(options.headers)) {
|
|
1318
|
+
setHeader(header, options.headers[header]);
|
|
1350
1319
|
}
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1320
|
+
}
|
|
1321
|
+
return method === 'HEAD'
|
|
1322
|
+
? ''
|
|
1323
|
+
: createReadStream(normalizedPath, !!range ? { start, end } : undefined);
|
|
1355
1324
|
}
|
|
1356
1325
|
// function toWeak(etag: string): string {
|
|
1357
1326
|
// return `W/${etag}`
|
|
@@ -1382,53 +1351,51 @@ function isNotModified(etag, lastModified, clientEtag, clientLM) {
|
|
|
1382
1351
|
}
|
|
1383
1352
|
return false;
|
|
1384
1353
|
}
|
|
1385
|
-
function listDirectory(dirPath) {
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
(d.size
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
'</ul></body></html>');
|
|
1431
|
-
});
|
|
1354
|
+
async function listDirectory(dirPath) {
|
|
1355
|
+
const { setContentType } = useSetHeaders();
|
|
1356
|
+
const { url } = useRequest();
|
|
1357
|
+
const list = await readdir(dirPath);
|
|
1358
|
+
const promises = [];
|
|
1359
|
+
let detailedList = [];
|
|
1360
|
+
for (const item of list) {
|
|
1361
|
+
promises.push({ name: item, promise: stat(path.join(dirPath, item)) });
|
|
1362
|
+
}
|
|
1363
|
+
for (const item of promises) {
|
|
1364
|
+
const data = await item.promise;
|
|
1365
|
+
detailedList.push({
|
|
1366
|
+
name: item.name,
|
|
1367
|
+
size: data.size,
|
|
1368
|
+
mtime: data.mtime,
|
|
1369
|
+
dir: data.isDirectory(),
|
|
1370
|
+
});
|
|
1371
|
+
}
|
|
1372
|
+
detailedList = detailedList.sort((a, b) => a.dir === b.dir ? (a.name > b.name ? 1 : -1) : a.dir > b.dir ? -1 : 1);
|
|
1373
|
+
detailedList.unshift({ name: '..', dir: true });
|
|
1374
|
+
setContentType('text/html');
|
|
1375
|
+
const styles = '<style type="text/css">\nhtml { font-family: monospace }\n' +
|
|
1376
|
+
'span { padding: 0px 2px }\n' +
|
|
1377
|
+
'.text { text-overflow: ellipsis; overflow: hidden; white-space: nowrap; }\n' +
|
|
1378
|
+
'.icon { width: 20px; display: inline-block; text-align: center; }\n' +
|
|
1379
|
+
'.name { width: 250px; display: inline-block; }\n' +
|
|
1380
|
+
'.size { width: 80px; display: inline-block; color: grey; text-align: right; }\n' +
|
|
1381
|
+
'.date { width: 200px; display: inline-block; color: grey; text-align: right; }\n' +
|
|
1382
|
+
'\n</style>';
|
|
1383
|
+
return ('<html><head><title>Dir</title> ' +
|
|
1384
|
+
styles +
|
|
1385
|
+
' </head><body><ul>' +
|
|
1386
|
+
detailedList
|
|
1387
|
+
.map((d) => `<li> <span class="icon">${d.dir ? '📁' : '🗎'}</span>` +
|
|
1388
|
+
`<a href="${path.join(url || '', d.name)}"><span class="name text">${d.name}</span></a>` +
|
|
1389
|
+
`<span class="size text">${(d.size &&
|
|
1390
|
+
(d.size > 10000
|
|
1391
|
+
? Math.round(d.size / 1024 / 1024).toString() +
|
|
1392
|
+
'Mb'
|
|
1393
|
+
: Math.round(d.size / 1024).toString() +
|
|
1394
|
+
'Kb')) ||
|
|
1395
|
+
''}</span>` +
|
|
1396
|
+
`<span class="date text">${(d.mtime && d.mtime.toISOString()) || ''}</li>`)
|
|
1397
|
+
.join('\n') +
|
|
1398
|
+
'</ul></body></html>');
|
|
1432
1399
|
}
|
|
1433
1400
|
|
|
1434
1401
|
export { serveFile };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wooksjs/http-static",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.11",
|
|
4
4
|
"description": "@wooksjs/http-static",
|
|
5
5
|
"main": "dist/index.cjs",
|
|
6
6
|
"module": "dist/index.mjs",
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"url": "https://github.com/wooksjs/wooksjs/issues"
|
|
32
32
|
},
|
|
33
33
|
"peerDependencies": {
|
|
34
|
-
"@wooksjs/event-http": "0.4.
|
|
34
|
+
"@wooksjs/event-http": "0.4.11"
|
|
35
35
|
},
|
|
36
36
|
"homepage": "https://github.com/wooksjs/wooksjs/tree/main/packages/http-static#readme"
|
|
37
37
|
}
|