@wooksjs/http-static 0.6.6 → 0.7.1
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/README.md +8 -63
- package/dist/index.cjs +20 -22
- package/dist/index.mjs +21 -23
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
#
|
|
2
|
-
|
|
3
|
-
**!!! This is work-in-progress library, breaking changes are expected !!!**
|
|
1
|
+
# @wooksjs/http-static
|
|
4
2
|
|
|
5
3
|
<p align="center">
|
|
6
4
|
<img src="../../wooks-logo.png" width="450px"><br>
|
|
@@ -9,71 +7,18 @@
|
|
|
9
7
|
</a>
|
|
10
8
|
</p>
|
|
11
9
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
`serveFile` returns a readable stream and prepares all the neccessary response headers (like content-length, content-type etc).
|
|
15
|
-
|
|
16
|
-
- ✅ returns a readable stream
|
|
17
|
-
- ✅ prepares all the neccessary response headers (like content-length, content-type etc)
|
|
18
|
-
- ✅ can handle etag
|
|
19
|
-
- ✅ can handle ranges
|
|
10
|
+
Composable static file server for [@wooksjs/event-http](https://github.com/wooksjs/wooksjs/tree/main/packages/event-http). Serves files as readable streams with automatic content-type detection, ETag support, and range requests.
|
|
20
11
|
|
|
21
12
|
## Installation
|
|
22
13
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
## Usage
|
|
26
|
-
|
|
27
|
-
```js
|
|
28
|
-
import { serveFile } from '@wooksjs/http-static'
|
|
29
|
-
// ...
|
|
30
|
-
serveFile(filePath, options)
|
|
14
|
+
```sh
|
|
15
|
+
npm install @wooksjs/http-static
|
|
31
16
|
```
|
|
32
17
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
```ts
|
|
36
|
-
{
|
|
37
|
-
// Any header to add
|
|
38
|
-
headers?: Record<string, string>,
|
|
39
|
-
|
|
40
|
-
// Cache-Control header
|
|
41
|
-
cacheControl?: TCacheControl,
|
|
42
|
-
|
|
43
|
-
// Expires header
|
|
44
|
-
expires?: Date | string | number,
|
|
45
|
-
|
|
46
|
-
// when true a header "Pragma: no-cache" will be added
|
|
47
|
-
pragmaNoCache?: boolean,
|
|
48
|
-
|
|
49
|
-
// the base directory path
|
|
50
|
-
baseDir?: string,
|
|
51
|
-
|
|
52
|
-
// default extension will be added to the filePath
|
|
53
|
-
defaultExt?: string,
|
|
54
|
-
|
|
55
|
-
// when true lists files in directory
|
|
56
|
-
listDirectory?: boolean,
|
|
57
|
-
|
|
58
|
-
// put 'index.html'
|
|
59
|
-
// to automatically serve it from the folder
|
|
60
|
-
index?: string,
|
|
61
|
-
}
|
|
62
|
-
```
|
|
63
|
-
|
|
64
|
-
### Built-in file server example:
|
|
65
|
-
|
|
66
|
-
```js
|
|
67
|
-
import { useRouteParams } from 'wooks'
|
|
68
|
-
import { serveFile } from '@wooksjs/http-static'
|
|
69
|
-
app.get('static/*', () => {
|
|
70
|
-
const { get } = useRouteParams()
|
|
71
|
-
return serveFile(get('*'), { cacheControl: { maxAge: '10m' } })
|
|
72
|
-
})
|
|
73
|
-
```
|
|
18
|
+
## Documentation
|
|
74
19
|
|
|
75
|
-
|
|
20
|
+
For full documentation, visit [wooks.moost.org/webapp/static](https://wooks.moost.org/webapp/static).
|
|
76
21
|
|
|
77
|
-
##
|
|
22
|
+
## License
|
|
78
23
|
|
|
79
|
-
|
|
24
|
+
MIT
|
package/dist/index.cjs
CHANGED
|
@@ -1247,11 +1247,9 @@ const { stat, readdir } = fs.promises;
|
|
|
1247
1247
|
* @returns A readable stream, string body, or HTML directory listing.
|
|
1248
1248
|
*/
|
|
1249
1249
|
async function serveFile(filePath, options = {}) {
|
|
1250
|
-
const
|
|
1251
|
-
const { setHeader, removeHeader } = (0, __wooksjs_event_http.useSetHeaders)();
|
|
1250
|
+
const response = (0, __wooksjs_event_http.useResponse)();
|
|
1252
1251
|
const headers = (0, __wooksjs_event_http.useHeaders)();
|
|
1253
1252
|
const { method, url } = (0, __wooksjs_event_http.useRequest)();
|
|
1254
|
-
const { setCacheControl, setExpires, setPragmaNoCache } = (0, __wooksjs_event_http.useSetCacheControl)();
|
|
1255
1253
|
const normalizedPath = normalizePath(filePath, options.baseDir);
|
|
1256
1254
|
if (!options.allowDotDot) {
|
|
1257
1255
|
const resolvedBase = path.default.resolve(options.baseDir || process.cwd());
|
|
@@ -1267,7 +1265,7 @@ async function serveFile(filePath, options = {}) {
|
|
|
1267
1265
|
}
|
|
1268
1266
|
throw new __wooksjs_event_http.HttpError(404);
|
|
1269
1267
|
}
|
|
1270
|
-
status
|
|
1268
|
+
response.status = 200;
|
|
1271
1269
|
const etag = `"${[
|
|
1272
1270
|
fileStats.ino,
|
|
1273
1271
|
fileStats.size,
|
|
@@ -1275,25 +1273,25 @@ async function serveFile(filePath, options = {}) {
|
|
|
1275
1273
|
].join("-")}"`;
|
|
1276
1274
|
const lastModified = new Date(fileStats.mtime);
|
|
1277
1275
|
if (isNotModified(etag, lastModified, headers["if-none-match"] || "", headers["if-modified-since"] || "")) {
|
|
1278
|
-
status
|
|
1276
|
+
response.status = 304;
|
|
1279
1277
|
return "";
|
|
1280
1278
|
}
|
|
1281
|
-
setHeader("etag", etag);
|
|
1282
|
-
setHeader("last-modified", lastModified.toUTCString());
|
|
1283
|
-
if (options.cacheControl !== void 0) setCacheControl(options.cacheControl);
|
|
1284
|
-
if (options.expires) setExpires(options.expires);
|
|
1285
|
-
if (options.pragmaNoCache) setPragmaNoCache(options.pragmaNoCache);
|
|
1279
|
+
response.setHeader("etag", etag);
|
|
1280
|
+
response.setHeader("last-modified", lastModified.toUTCString());
|
|
1281
|
+
if (options.cacheControl !== void 0) response.setCacheControl(options.cacheControl);
|
|
1282
|
+
if (options.expires) response.setExpires(options.expires);
|
|
1283
|
+
if (options.pragmaNoCache) response.setPragmaNoCache(options.pragmaNoCache);
|
|
1286
1284
|
if (fileStats.isDirectory()) {
|
|
1287
1285
|
if (options.listDirectory) return listDirectory(normalizedPath);
|
|
1288
1286
|
else if (options.index) {
|
|
1289
|
-
if (!filePath.endsWith("/") && url && !url.endsWith("/")) return
|
|
1287
|
+
if (!filePath.endsWith("/") && url && !url.endsWith("/")) return response.setStatus(302).setHeader("location", `${url}/`);
|
|
1290
1288
|
return serveFile(path.default.join(filePath, options.index), {
|
|
1291
1289
|
...options,
|
|
1292
1290
|
index: ""
|
|
1293
1291
|
});
|
|
1294
1292
|
}
|
|
1295
|
-
removeHeader("etag");
|
|
1296
|
-
removeHeader("last-modified");
|
|
1293
|
+
response.removeHeader("etag");
|
|
1294
|
+
response.removeHeader("last-modified");
|
|
1297
1295
|
throw new __wooksjs_event_http.HttpError(404);
|
|
1298
1296
|
}
|
|
1299
1297
|
let range = headers.range;
|
|
@@ -1312,18 +1310,18 @@ async function serveFile(filePath, options = {}) {
|
|
|
1312
1310
|
const ifRangeTag = ifRange.startsWith("\"") ? ifRange : "";
|
|
1313
1311
|
const ifRangeDate = ifRangeTag ? "" : ifRange;
|
|
1314
1312
|
if (ifRange && !isNotModified(etag, lastModified, ifRangeTag, ifRangeDate)) {
|
|
1315
|
-
status
|
|
1313
|
+
response.status = 200;
|
|
1316
1314
|
size = fileStats.size;
|
|
1317
1315
|
range = "";
|
|
1318
1316
|
} else {
|
|
1319
|
-
setHeader("content-range", `bytes ${start}-${end}/${fileStats.size}`);
|
|
1320
|
-
status
|
|
1317
|
+
response.setHeader("content-range", `bytes ${start}-${end}/${fileStats.size}`);
|
|
1318
|
+
response.status = 206;
|
|
1321
1319
|
}
|
|
1322
1320
|
}
|
|
1323
|
-
setHeader("accept-ranges", "bytes");
|
|
1324
|
-
setHeader("content-type", getMimeType(normalizedPath) || "application/octet-stream");
|
|
1325
|
-
setHeader("content-length", size);
|
|
1326
|
-
if (options.headers) for (const header of Object.keys(options.headers)) setHeader(header, options.headers[header]);
|
|
1321
|
+
response.setHeader("accept-ranges", "bytes");
|
|
1322
|
+
response.setHeader("content-type", getMimeType(normalizedPath) || "application/octet-stream");
|
|
1323
|
+
response.setHeader("content-length", size);
|
|
1324
|
+
if (options.headers) for (const header of Object.keys(options.headers)) response.setHeader(header, options.headers[header]);
|
|
1327
1325
|
return method === "HEAD" ? "" : (0, fs.createReadStream)(normalizedPath, range ? {
|
|
1328
1326
|
start,
|
|
1329
1327
|
end
|
|
@@ -1340,7 +1338,7 @@ function isNotModified(etag, lastModified, clientEtag, clientLM) {
|
|
|
1340
1338
|
return false;
|
|
1341
1339
|
}
|
|
1342
1340
|
async function listDirectory(dirPath) {
|
|
1343
|
-
const
|
|
1341
|
+
const response = (0, __wooksjs_event_http.useResponse)();
|
|
1344
1342
|
const { url } = (0, __wooksjs_event_http.useRequest)();
|
|
1345
1343
|
const list = await readdir(dirPath);
|
|
1346
1344
|
const promises = [];
|
|
@@ -1363,7 +1361,7 @@ async function listDirectory(dirPath) {
|
|
|
1363
1361
|
name: "..",
|
|
1364
1362
|
dir: true
|
|
1365
1363
|
});
|
|
1366
|
-
setContentType("text/html");
|
|
1364
|
+
response.setContentType("text/html");
|
|
1367
1365
|
const styles = "<style type=\"text/css\">\nhtml { font-family: monospace }\nspan { padding: 0px 2px }\n.text { text-overflow: ellipsis; overflow: hidden; white-space: nowrap; }\n.icon { width: 20px; display: inline-block; text-align: center; }\n.name { width: 250px; display: inline-block; }\n.size { width: 80px; display: inline-block; color: grey; text-align: right; }\n.date { width: 200px; display: inline-block; color: grey; text-align: right; }\n\n</style>";
|
|
1368
1366
|
return `<html><head><title>Dir</title> ${styles} </head><body><ul>${detailedList.map((d) => `<li> <span class="icon">${d.dir ? "📁" : "🗎"}</span><a href="${escapeHtml(path.default.join(url || "", d.name))}"><span class="name text">${escapeHtml(d.name)}</span></a><span class="size text">${d.size && (d.size > 1e4 ? `${Math.round(d.size / 1024 / 1024).toString()}Mb` : `${Math.round(d.size / 1024).toString()}Kb`) || ""}</span><span class="date text">${d.mtime && d.mtime.toISOString() || ""}</li>`).join("\n")}</ul></body></html>`;
|
|
1369
1367
|
}
|
package/dist/index.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { HttpError, useHeaders, useRequest, useResponse } from "@wooksjs/event-http";
|
|
2
2
|
import { createReadStream, promises } from "fs";
|
|
3
3
|
import path from "path";
|
|
4
4
|
|
|
@@ -1224,11 +1224,9 @@ const { stat, readdir } = promises;
|
|
|
1224
1224
|
* @returns A readable stream, string body, or HTML directory listing.
|
|
1225
1225
|
*/
|
|
1226
1226
|
async function serveFile(filePath, options = {}) {
|
|
1227
|
-
const
|
|
1228
|
-
const { setHeader, removeHeader } = useSetHeaders();
|
|
1227
|
+
const response = useResponse();
|
|
1229
1228
|
const headers = useHeaders();
|
|
1230
1229
|
const { method, url } = useRequest();
|
|
1231
|
-
const { setCacheControl, setExpires, setPragmaNoCache } = useSetCacheControl();
|
|
1232
1230
|
const normalizedPath = normalizePath(filePath, options.baseDir);
|
|
1233
1231
|
if (!options.allowDotDot) {
|
|
1234
1232
|
const resolvedBase = path.resolve(options.baseDir || process.cwd());
|
|
@@ -1244,7 +1242,7 @@ async function serveFile(filePath, options = {}) {
|
|
|
1244
1242
|
}
|
|
1245
1243
|
throw new HttpError(404);
|
|
1246
1244
|
}
|
|
1247
|
-
status
|
|
1245
|
+
response.status = 200;
|
|
1248
1246
|
const etag = `"${[
|
|
1249
1247
|
fileStats.ino,
|
|
1250
1248
|
fileStats.size,
|
|
@@ -1252,25 +1250,25 @@ async function serveFile(filePath, options = {}) {
|
|
|
1252
1250
|
].join("-")}"`;
|
|
1253
1251
|
const lastModified = new Date(fileStats.mtime);
|
|
1254
1252
|
if (isNotModified(etag, lastModified, headers["if-none-match"] || "", headers["if-modified-since"] || "")) {
|
|
1255
|
-
status
|
|
1253
|
+
response.status = 304;
|
|
1256
1254
|
return "";
|
|
1257
1255
|
}
|
|
1258
|
-
setHeader("etag", etag);
|
|
1259
|
-
setHeader("last-modified", lastModified.toUTCString());
|
|
1260
|
-
if (options.cacheControl !== void 0) setCacheControl(options.cacheControl);
|
|
1261
|
-
if (options.expires) setExpires(options.expires);
|
|
1262
|
-
if (options.pragmaNoCache) setPragmaNoCache(options.pragmaNoCache);
|
|
1256
|
+
response.setHeader("etag", etag);
|
|
1257
|
+
response.setHeader("last-modified", lastModified.toUTCString());
|
|
1258
|
+
if (options.cacheControl !== void 0) response.setCacheControl(options.cacheControl);
|
|
1259
|
+
if (options.expires) response.setExpires(options.expires);
|
|
1260
|
+
if (options.pragmaNoCache) response.setPragmaNoCache(options.pragmaNoCache);
|
|
1263
1261
|
if (fileStats.isDirectory()) {
|
|
1264
1262
|
if (options.listDirectory) return listDirectory(normalizedPath);
|
|
1265
1263
|
else if (options.index) {
|
|
1266
|
-
if (!filePath.endsWith("/") && url && !url.endsWith("/")) return
|
|
1264
|
+
if (!filePath.endsWith("/") && url && !url.endsWith("/")) return response.setStatus(302).setHeader("location", `${url}/`);
|
|
1267
1265
|
return serveFile(path.join(filePath, options.index), {
|
|
1268
1266
|
...options,
|
|
1269
1267
|
index: ""
|
|
1270
1268
|
});
|
|
1271
1269
|
}
|
|
1272
|
-
removeHeader("etag");
|
|
1273
|
-
removeHeader("last-modified");
|
|
1270
|
+
response.removeHeader("etag");
|
|
1271
|
+
response.removeHeader("last-modified");
|
|
1274
1272
|
throw new HttpError(404);
|
|
1275
1273
|
}
|
|
1276
1274
|
let range = headers.range;
|
|
@@ -1289,18 +1287,18 @@ async function serveFile(filePath, options = {}) {
|
|
|
1289
1287
|
const ifRangeTag = ifRange.startsWith("\"") ? ifRange : "";
|
|
1290
1288
|
const ifRangeDate = ifRangeTag ? "" : ifRange;
|
|
1291
1289
|
if (ifRange && !isNotModified(etag, lastModified, ifRangeTag, ifRangeDate)) {
|
|
1292
|
-
status
|
|
1290
|
+
response.status = 200;
|
|
1293
1291
|
size = fileStats.size;
|
|
1294
1292
|
range = "";
|
|
1295
1293
|
} else {
|
|
1296
|
-
setHeader("content-range", `bytes ${start}-${end}/${fileStats.size}`);
|
|
1297
|
-
status
|
|
1294
|
+
response.setHeader("content-range", `bytes ${start}-${end}/${fileStats.size}`);
|
|
1295
|
+
response.status = 206;
|
|
1298
1296
|
}
|
|
1299
1297
|
}
|
|
1300
|
-
setHeader("accept-ranges", "bytes");
|
|
1301
|
-
setHeader("content-type", getMimeType(normalizedPath) || "application/octet-stream");
|
|
1302
|
-
setHeader("content-length", size);
|
|
1303
|
-
if (options.headers) for (const header of Object.keys(options.headers)) setHeader(header, options.headers[header]);
|
|
1298
|
+
response.setHeader("accept-ranges", "bytes");
|
|
1299
|
+
response.setHeader("content-type", getMimeType(normalizedPath) || "application/octet-stream");
|
|
1300
|
+
response.setHeader("content-length", size);
|
|
1301
|
+
if (options.headers) for (const header of Object.keys(options.headers)) response.setHeader(header, options.headers[header]);
|
|
1304
1302
|
return method === "HEAD" ? "" : createReadStream(normalizedPath, range ? {
|
|
1305
1303
|
start,
|
|
1306
1304
|
end
|
|
@@ -1317,7 +1315,7 @@ function isNotModified(etag, lastModified, clientEtag, clientLM) {
|
|
|
1317
1315
|
return false;
|
|
1318
1316
|
}
|
|
1319
1317
|
async function listDirectory(dirPath) {
|
|
1320
|
-
const
|
|
1318
|
+
const response = useResponse();
|
|
1321
1319
|
const { url } = useRequest();
|
|
1322
1320
|
const list = await readdir(dirPath);
|
|
1323
1321
|
const promises$1 = [];
|
|
@@ -1340,7 +1338,7 @@ async function listDirectory(dirPath) {
|
|
|
1340
1338
|
name: "..",
|
|
1341
1339
|
dir: true
|
|
1342
1340
|
});
|
|
1343
|
-
setContentType("text/html");
|
|
1341
|
+
response.setContentType("text/html");
|
|
1344
1342
|
const styles = "<style type=\"text/css\">\nhtml { font-family: monospace }\nspan { padding: 0px 2px }\n.text { text-overflow: ellipsis; overflow: hidden; white-space: nowrap; }\n.icon { width: 20px; display: inline-block; text-align: center; }\n.name { width: 250px; display: inline-block; }\n.size { width: 80px; display: inline-block; color: grey; text-align: right; }\n.date { width: 200px; display: inline-block; color: grey; text-align: right; }\n\n</style>";
|
|
1345
1343
|
return `<html><head><title>Dir</title> ${styles} </head><body><ul>${detailedList.map((d) => `<li> <span class="icon">${d.dir ? "📁" : "🗎"}</span><a href="${escapeHtml(path.join(url || "", d.name))}"><span class="name text">${escapeHtml(d.name)}</span></a><span class="size text">${d.size && (d.size > 1e4 ? `${Math.round(d.size / 1024 / 1024).toString()}Mb` : `${Math.round(d.size / 1024).toString()}Kb`) || ""}</span><span class="date text">${d.mtime && d.mtime.toISOString() || ""}</li>`).join("\n")}</ul></body></html>`;
|
|
1346
1344
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wooksjs/http-static",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.1",
|
|
4
4
|
"description": "@wooksjs/http-static",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"api",
|
|
@@ -42,10 +42,10 @@
|
|
|
42
42
|
"devDependencies": {
|
|
43
43
|
"typescript": "^5.9.3",
|
|
44
44
|
"vitest": "^3.2.4",
|
|
45
|
-
"@wooksjs/event-http": "^0.
|
|
45
|
+
"@wooksjs/event-http": "^0.7.1"
|
|
46
46
|
},
|
|
47
47
|
"peerDependencies": {
|
|
48
|
-
"@wooksjs/event-http": "^0.
|
|
48
|
+
"@wooksjs/event-http": "^0.7.1"
|
|
49
49
|
},
|
|
50
50
|
"scripts": {
|
|
51
51
|
"build": "rolldown -c ../../rolldown.config.mjs"
|