@maiyunnet/kebab 9.20.1 → 9.21.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/doc/kebab-rag.md +37 -24
- package/index.d.ts +1 -1
- package/index.js +1 -1
- package/lib/fs.d.ts +12 -1
- package/lib/fs.js +214 -159
- package/package.json +1 -1
package/doc/kebab-rag.md
CHANGED
|
@@ -1589,7 +1589,7 @@ index/variables/VER.md
|
|
|
1589
1589
|
|
|
1590
1590
|
# Variable: VER
|
|
1591
1591
|
|
|
1592
|
-
> `const` **VER**: `"9.
|
|
1592
|
+
> `const` **VER**: `"9.21.0"` = `'9.21.0'`
|
|
1593
1593
|
|
|
1594
1594
|
Defined in: [index.ts:10](https://github.com/maiyunnet/kebab/blob/master/index.ts#L10)
|
|
1595
1595
|
|
|
@@ -118073,7 +118073,7 @@ lib/fs/functions/chmod.md
|
|
|
118073
118073
|
|
|
118074
118074
|
> **chmod**(`path`, `mod`): `Promise`\<`boolean`\>
|
|
118075
118075
|
|
|
118076
|
-
Defined in: [lib/fs.ts:
|
|
118076
|
+
Defined in: [lib/fs.ts:224](https://github.com/maiyunnet/kebab/blob/master/lib/fs.ts#L224)
|
|
118077
118077
|
|
|
118078
118078
|
修改权限
|
|
118079
118079
|
|
|
@@ -118108,7 +118108,7 @@ lib/fs/functions/copyFile.md
|
|
|
118108
118108
|
|
|
118109
118109
|
> **copyFile**(`src`, `dest`): `Promise`\<`boolean`\>
|
|
118110
118110
|
|
|
118111
|
-
Defined in: [lib/fs.ts:
|
|
118111
|
+
Defined in: [lib/fs.ts:316](https://github.com/maiyunnet/kebab/blob/master/lib/fs.ts#L316)
|
|
118112
118112
|
|
|
118113
118113
|
复制文件
|
|
118114
118114
|
|
|
@@ -118143,7 +118143,7 @@ lib/fs/functions/copyFolder.md
|
|
|
118143
118143
|
|
|
118144
118144
|
> **copyFolder**(`from`, `to`, `ignore?`): `Promise`\<`number`\>
|
|
118145
118145
|
|
|
118146
|
-
Defined in: [lib/fs.ts:
|
|
118146
|
+
Defined in: [lib/fs.ts:273](https://github.com/maiyunnet/kebab/blob/master/lib/fs.ts#L273)
|
|
118147
118147
|
|
|
118148
118148
|
复制文件夹里的内容到另一个地方,失败不会回滚
|
|
118149
118149
|
|
|
@@ -118184,7 +118184,7 @@ lib/fs/functions/createReadStream.md
|
|
|
118184
118184
|
|
|
118185
118185
|
> **createReadStream**(`path`, `options?`): `ReadStream`
|
|
118186
118186
|
|
|
118187
|
-
Defined in: [lib/fs.ts:
|
|
118187
|
+
Defined in: [lib/fs.ts:331](https://github.com/maiyunnet/kebab/blob/master/lib/fs.ts#L331)
|
|
118188
118188
|
|
|
118189
118189
|
创建读取文件的流
|
|
118190
118190
|
|
|
@@ -118219,7 +118219,7 @@ lib/fs/functions/createWriteStream.md
|
|
|
118219
118219
|
|
|
118220
118220
|
> **createWriteStream**(`path`, `options?`): `WriteStream`
|
|
118221
118221
|
|
|
118222
|
-
Defined in: [lib/fs.ts:
|
|
118222
|
+
Defined in: [lib/fs.ts:364](https://github.com/maiyunnet/kebab/blob/master/lib/fs.ts#L364)
|
|
118223
118223
|
|
|
118224
118224
|
创建写入文件的流
|
|
118225
118225
|
|
|
@@ -118252,7 +118252,7 @@ lib/fs/functions/getContent.md
|
|
|
118252
118252
|
|
|
118253
118253
|
# Function: getContent()
|
|
118254
118254
|
|
|
118255
|
-
|
|
118255
|
+
读取完整文件或一段,区间包含首尾字节
|
|
118256
118256
|
|
|
118257
118257
|
## Param
|
|
118258
118258
|
|
|
@@ -118270,7 +118270,7 @@ lib/fs/functions/getContent.md
|
|
|
118270
118270
|
|
|
118271
118271
|
> **getContent**(`path`, `options?`): `Promise`\<`Buffer`\<`ArrayBufferLike`\> \| `null`\>
|
|
118272
118272
|
|
|
118273
|
-
Defined in: [lib/fs.ts:
|
|
118273
|
+
Defined in: [lib/fs.ts:16](https://github.com/maiyunnet/kebab/blob/master/lib/fs.ts#L16)
|
|
118274
118274
|
|
|
118275
118275
|
### Parameters
|
|
118276
118276
|
|
|
@@ -118296,7 +118296,7 @@ Defined in: [lib/fs.ts:14](https://github.com/maiyunnet/kebab/blob/master/lib/fs
|
|
|
118296
118296
|
|
|
118297
118297
|
> **getContent**(`path`, `options`): `Promise`\<`string` \| `null`\>
|
|
118298
118298
|
|
|
118299
|
-
Defined in: [lib/fs.ts:
|
|
118299
|
+
Defined in: [lib/fs.ts:20](https://github.com/maiyunnet/kebab/blob/master/lib/fs.ts#L20)
|
|
118300
118300
|
|
|
118301
118301
|
### Parameters
|
|
118302
118302
|
|
|
@@ -118325,7 +118325,7 @@ lib/fs/functions/isDir.md
|
|
|
118325
118325
|
|
|
118326
118326
|
> **isDir**(`path`): `Promise`\<`false` \| `Stats`\>
|
|
118327
118327
|
|
|
118328
|
-
Defined in: [lib/fs.ts:
|
|
118328
|
+
Defined in: [lib/fs.ts:144](https://github.com/maiyunnet/kebab/blob/master/lib/fs.ts#L144)
|
|
118329
118329
|
|
|
118330
118330
|
判断是否是目录或目录是否存在,是的话返回 stats
|
|
118331
118331
|
|
|
@@ -118354,7 +118354,7 @@ lib/fs/functions/isFile.md
|
|
|
118354
118354
|
|
|
118355
118355
|
> **isFile**(`path`): `Promise`\<`false` \| `Stats`\>
|
|
118356
118356
|
|
|
118357
|
-
Defined in: [lib/fs.ts:
|
|
118357
|
+
Defined in: [lib/fs.ts:153](https://github.com/maiyunnet/kebab/blob/master/lib/fs.ts#L153)
|
|
118358
118358
|
|
|
118359
118359
|
判断是否是文件或文件是否存在,是的话返回 stats
|
|
118360
118360
|
|
|
@@ -118383,7 +118383,7 @@ lib/fs/functions/mkdir.md
|
|
|
118383
118383
|
|
|
118384
118384
|
> **mkdir**(`path`, `mode?`): `Promise`\<`boolean`\>
|
|
118385
118385
|
|
|
118386
|
-
Defined in: [lib/fs.ts:
|
|
118386
|
+
Defined in: [lib/fs.ts:163](https://github.com/maiyunnet/kebab/blob/master/lib/fs.ts#L163)
|
|
118387
118387
|
|
|
118388
118388
|
深度创建目录,如果最末目录存在,则自动创建成功
|
|
118389
118389
|
|
|
@@ -118418,7 +118418,7 @@ lib/fs/functions/pipe.md
|
|
|
118418
118418
|
|
|
118419
118419
|
> **pipe**(`path`, `destination`, `options?`): `Promise`\<`boolean`\>
|
|
118420
118420
|
|
|
118421
|
-
Defined in: [lib/fs.ts:
|
|
118421
|
+
Defined in: [lib/fs.ts:347](https://github.com/maiyunnet/kebab/blob/master/lib/fs.ts#L347)
|
|
118422
118422
|
|
|
118423
118423
|
读取文件写入到流,并等待写入完成
|
|
118424
118424
|
|
|
@@ -118461,7 +118461,7 @@ lib/fs/functions/putContent.md
|
|
|
118461
118461
|
|
|
118462
118462
|
> **putContent**(`path`, `data`, `options?`): `Promise`\<`boolean`\>
|
|
118463
118463
|
|
|
118464
|
-
Defined in: [lib/fs.ts:
|
|
118464
|
+
Defined in: [lib/fs.ts:59](https://github.com/maiyunnet/kebab/blob/master/lib/fs.ts#L59)
|
|
118465
118465
|
|
|
118466
118466
|
写入文件内容
|
|
118467
118467
|
|
|
@@ -118512,7 +118512,7 @@ lib/fs/functions/readDir.md
|
|
|
118512
118512
|
|
|
118513
118513
|
> **readDir**(`path`, `encoding?`): `Promise`\<`Dirent`\<`string`\>[]\>
|
|
118514
118514
|
|
|
118515
|
-
Defined in: [lib/fs.ts:
|
|
118515
|
+
Defined in: [lib/fs.ts:253](https://github.com/maiyunnet/kebab/blob/master/lib/fs.ts#L253)
|
|
118516
118516
|
|
|
118517
118517
|
获取文件夹下文件列表
|
|
118518
118518
|
|
|
@@ -118545,7 +118545,7 @@ lib/fs/functions/readLink.md
|
|
|
118545
118545
|
|
|
118546
118546
|
> **readLink**(`path`, `encoding?`): `Promise`\<`string` \| `null`\>
|
|
118547
118547
|
|
|
118548
|
-
Defined in: [lib/fs.ts:
|
|
118548
|
+
Defined in: [lib/fs.ts:81](https://github.com/maiyunnet/kebab/blob/master/lib/fs.ts#L81)
|
|
118549
118549
|
|
|
118550
118550
|
读取链接的 target
|
|
118551
118551
|
|
|
@@ -118580,9 +118580,16 @@ lib/fs/functions/readToResponse.md
|
|
|
118580
118580
|
|
|
118581
118581
|
> **readToResponse**(`path`, `req`, `res`, `stat?`): `Promise`\<`void`\>
|
|
118582
118582
|
|
|
118583
|
-
Defined in: [lib/fs.ts:
|
|
118583
|
+
Defined in: [lib/fs.ts:480](https://github.com/maiyunnet/kebab/blob/master/lib/fs.ts#L480)
|
|
118584
|
+
|
|
118585
|
+
读取文件并输出到 http 的 response,支持 GET 字节范围与条件请求
|
|
118584
118586
|
|
|
118585
|
-
|
|
118587
|
+
Range 支持 start-end、start-、-length 及最多 16 段,请求头上限 8 KiB;相邻和重叠区间合并。
|
|
118588
|
+
有效区间返回 206,全部越界或区间过多返回 416;无效语法、未知单位和空文件忽略 Range。
|
|
118589
|
+
部分响应按原始文件字节流式输出,不进行动态压缩。If-Range 日期须匹配 Last-Modified,
|
|
118590
|
+
且文件不启用动态压缩、修改时间早于当前响应所在秒,否则退回完整响应;
|
|
118591
|
+
文件元数据生成的 ETag 为弱校验值,不接受其作为 If-Range 的强校验值。
|
|
118592
|
+
HEAD 忽略 Range 且不输出响应体。
|
|
118586
118593
|
|
|
118587
118594
|
## Parameters
|
|
118588
118595
|
|
|
@@ -118614,6 +118621,8 @@ http 响应对象
|
|
|
118614
118621
|
|
|
118615
118622
|
`Promise`\<`void`\>
|
|
118616
118623
|
|
|
118624
|
+
输出完成或连接终止后无返回值
|
|
118625
|
+
|
|
118617
118626
|
lib/fs/functions/rename.md
|
|
118618
118627
|
---
|
|
118619
118628
|
|
|
@@ -118627,7 +118636,7 @@ lib/fs/functions/rename.md
|
|
|
118627
118636
|
|
|
118628
118637
|
> **rename**(`oldPath`, `newPath`): `Promise`\<`boolean`\>
|
|
118629
118638
|
|
|
118630
|
-
Defined in: [lib/fs.ts:
|
|
118639
|
+
Defined in: [lib/fs.ts:239](https://github.com/maiyunnet/kebab/blob/master/lib/fs.ts#L239)
|
|
118631
118640
|
|
|
118632
118641
|
重命名/移动文件文件夹
|
|
118633
118642
|
|
|
@@ -118662,7 +118671,7 @@ lib/fs/functions/rmdirDeep.md
|
|
|
118662
118671
|
|
|
118663
118672
|
> **rmdirDeep**(`path`): `Promise`\<`boolean`\>
|
|
118664
118673
|
|
|
118665
|
-
Defined in: [lib/fs.ts:
|
|
118674
|
+
Defined in: [lib/fs.ts:200](https://github.com/maiyunnet/kebab/blob/master/lib/fs.ts#L200)
|
|
118666
118675
|
|
|
118667
118676
|
Danger 危险:危险函数,尽量不要使用
|
|
118668
118677
|
This is a danger function, please don't use it
|
|
@@ -118674,10 +118683,14 @@ This is a danger function, please don't use it
|
|
|
118674
118683
|
|
|
118675
118684
|
`string`
|
|
118676
118685
|
|
|
118686
|
+
目录路径,不递归读取符号链接指向的目录
|
|
118687
|
+
|
|
118677
118688
|
## Returns
|
|
118678
118689
|
|
|
118679
118690
|
`Promise`\<`boolean`\>
|
|
118680
118691
|
|
|
118692
|
+
删除成功或根路径不是目录返回 true,删除失败返回 false
|
|
118693
|
+
|
|
118681
118694
|
lib/fs/functions/rmdir.md
|
|
118682
118695
|
---
|
|
118683
118696
|
|
|
@@ -118691,7 +118704,7 @@ lib/fs/functions/rmdir.md
|
|
|
118691
118704
|
|
|
118692
118705
|
> **rmdir**(`path`): `Promise`\<`boolean`\>
|
|
118693
118706
|
|
|
118694
|
-
Defined in: [lib/fs.ts:
|
|
118707
|
+
Defined in: [lib/fs.ts:180](https://github.com/maiyunnet/kebab/blob/master/lib/fs.ts#L180)
|
|
118695
118708
|
|
|
118696
118709
|
删除空目录
|
|
118697
118710
|
|
|
@@ -118720,7 +118733,7 @@ lib/fs/functions/stats.md
|
|
|
118720
118733
|
|
|
118721
118734
|
> **stats**(`path`): `Promise`\<`Stats` \| `null`\>
|
|
118722
118735
|
|
|
118723
|
-
Defined in: [lib/fs.ts:
|
|
118736
|
+
Defined in: [lib/fs.ts:131](https://github.com/maiyunnet/kebab/blob/master/lib/fs.ts#L131)
|
|
118724
118737
|
|
|
118725
118738
|
获取对象是否存在,存在则返回 stats 对象,否则返回 null
|
|
118726
118739
|
|
|
@@ -118749,7 +118762,7 @@ lib/fs/functions/symlink.md
|
|
|
118749
118762
|
|
|
118750
118763
|
> **symlink**(`filePath`, `linkPath`, `type?`): `Promise`\<`boolean`\>
|
|
118751
118764
|
|
|
118752
|
-
Defined in: [lib/fs.ts:
|
|
118765
|
+
Defined in: [lib/fs.ts:98](https://github.com/maiyunnet/kebab/blob/master/lib/fs.ts#L98)
|
|
118753
118766
|
|
|
118754
118767
|
把源文件创建一个 link
|
|
118755
118768
|
|
|
@@ -118790,7 +118803,7 @@ lib/fs/functions/unlink.md
|
|
|
118790
118803
|
|
|
118791
118804
|
> **unlink**(`path`): `Promise`\<`boolean`\>
|
|
118792
118805
|
|
|
118793
|
-
Defined in: [lib/fs.ts:
|
|
118806
|
+
Defined in: [lib/fs.ts:112](https://github.com/maiyunnet/kebab/blob/master/lib/fs.ts#L112)
|
|
118794
118807
|
|
|
118795
118808
|
删除一个文件
|
|
118796
118809
|
|
package/index.d.ts
CHANGED
package/index.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* --- 本文件用来定义每个目录实体地址的常量 ---
|
|
7
7
|
*/
|
|
8
8
|
/** --- 当前系统版本号 --- */
|
|
9
|
-
export const VER = '9.
|
|
9
|
+
export const VER = '9.21.0';
|
|
10
10
|
// --- 服务端用的路径 ---
|
|
11
11
|
const imu = decodeURIComponent(import.meta.url).replace('file://', '').replace(/^\/(\w:)/, '$1');
|
|
12
12
|
/** --- /xxx/xxx --- */
|
package/lib/fs.d.ts
CHANGED
|
@@ -74,6 +74,8 @@ export declare function rmdir(path: string): Promise<boolean>;
|
|
|
74
74
|
* --- Danger 危险:危险函数,尽量不要使用 ---
|
|
75
75
|
* --- This is a danger function, please don't use it ---
|
|
76
76
|
* --- 删除一个非空目录 ---
|
|
77
|
+
* @param path 目录路径,不递归读取符号链接指向的目录
|
|
78
|
+
* @returns 删除成功或根路径不是目录返回 true,删除失败返回 false
|
|
77
79
|
*/
|
|
78
80
|
export declare function rmdirDeep(path: string): Promise<boolean>;
|
|
79
81
|
/**
|
|
@@ -140,10 +142,19 @@ export declare function createWriteStream(path: string, options?: BufferEncoding
|
|
|
140
142
|
'start'?: number;
|
|
141
143
|
}): fs.WriteStream;
|
|
142
144
|
/**
|
|
143
|
-
* --- 读取文件并输出到 http 的 response ---
|
|
145
|
+
* --- 读取文件并输出到 http 的 response,支持 GET 字节范围与条件请求 ---
|
|
146
|
+
*
|
|
147
|
+
* Range 支持 start-end、start-、-length 及最多 16 段,请求头上限 8 KiB;相邻和重叠区间合并。
|
|
148
|
+
* 有效区间返回 206,全部越界或区间过多返回 416;无效语法、未知单位和空文件忽略 Range。
|
|
149
|
+
* 部分响应按原始文件字节流式输出,不进行动态压缩。If-Range 日期须匹配 Last-Modified,
|
|
150
|
+
* 且文件不启用动态压缩、修改时间早于当前响应所在秒,否则退回完整响应;
|
|
151
|
+
* 文件元数据生成的 ETag 为弱校验值,不接受其作为 If-Range 的强校验值。
|
|
152
|
+
* HEAD 忽略 Range 且不输出响应体。
|
|
153
|
+
*
|
|
144
154
|
* @param path 文件绝对路径
|
|
145
155
|
* @param req http 请求对象
|
|
146
156
|
* @param res http 响应对象
|
|
147
157
|
* @param stat 文件的 stat(如果有)
|
|
158
|
+
* @returns 输出完成或连接终止后无返回值
|
|
148
159
|
*/
|
|
149
160
|
export declare function readToResponse(path: string, req: http2.Http2ServerRequest | http.IncomingMessage, res: http2.Http2ServerResponse | http.ServerResponse, stat?: fs.Stats | null): Promise<void>;
|
package/lib/fs.js
CHANGED
|
@@ -4,67 +4,34 @@
|
|
|
4
4
|
* Last: 2020-3-11 22:21:51, 2022-12-29 01:18:25, 2023-12-13 20:50:09
|
|
5
5
|
*/
|
|
6
6
|
import * as fs from 'fs';
|
|
7
|
+
import * as crypto from 'crypto';
|
|
7
8
|
import * as http2 from 'http2';
|
|
9
|
+
import * as stream from 'stream';
|
|
8
10
|
import * as mime from '@litert/mime';
|
|
9
11
|
import * as lText from './text.js';
|
|
10
12
|
import * as lCore from './core.js';
|
|
11
13
|
import * as lZlib from './zlib.js';
|
|
12
14
|
/**
|
|
13
|
-
* ---
|
|
15
|
+
* --- 读取完整文件或一段,区间包含首尾字节 ---
|
|
14
16
|
* @param path 文件路径
|
|
15
17
|
* @param options 编码或选项
|
|
18
|
+
* @returns 文件内容,读取失败返回 null
|
|
16
19
|
*/
|
|
17
20
|
export async function getContent(path, options) {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
}
|
|
23
|
-
else {
|
|
24
|
-
options ??= {};
|
|
25
|
-
}
|
|
26
|
-
const encoding = options.encoding;
|
|
27
|
-
const start = options.start;
|
|
28
|
-
const end = options.end;
|
|
29
|
-
if ((start !== undefined) || (end !== undefined)) {
|
|
30
|
-
return new Promise(function (resolve) {
|
|
31
|
-
const rs = createReadStream(path, {
|
|
32
|
-
'start': start,
|
|
33
|
-
'end': end
|
|
34
|
-
});
|
|
35
|
-
const data = [];
|
|
36
|
-
rs.on('data', (chunk) => {
|
|
37
|
-
if (!(chunk instanceof Buffer)) {
|
|
38
|
-
return;
|
|
39
|
-
}
|
|
40
|
-
data.push(chunk);
|
|
41
|
-
}).on('end', function () {
|
|
42
|
-
const buf = Buffer.concat(data);
|
|
43
|
-
if (encoding) {
|
|
44
|
-
resolve(buf.toString());
|
|
45
|
-
}
|
|
46
|
-
else {
|
|
47
|
-
resolve(buf);
|
|
48
|
-
}
|
|
49
|
-
}).on('error', function () {
|
|
50
|
-
resolve(null);
|
|
51
|
-
});
|
|
52
|
-
});
|
|
53
|
-
}
|
|
54
|
-
else {
|
|
55
|
-
try {
|
|
56
|
-
if (encoding) {
|
|
57
|
-
return await fs.promises.readFile(path, {
|
|
58
|
-
'encoding': encoding
|
|
59
|
-
});
|
|
60
|
-
}
|
|
61
|
-
else {
|
|
62
|
-
return await fs.promises.readFile(path);
|
|
63
|
-
}
|
|
21
|
+
const { encoding, start, end } = typeof options === 'string' ? { 'encoding': options } : options ?? {};
|
|
22
|
+
try {
|
|
23
|
+
if (start === undefined && end === undefined) {
|
|
24
|
+
return await fs.promises.readFile(path, { encoding });
|
|
64
25
|
}
|
|
65
|
-
|
|
66
|
-
|
|
26
|
+
const data = [];
|
|
27
|
+
for await (const chunk of createReadStream(path, { start, end })) {
|
|
28
|
+
data.push(chunk);
|
|
67
29
|
}
|
|
30
|
+
const content = Buffer.concat(data);
|
|
31
|
+
return encoding ? content.toString(encoding) : content;
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return null;
|
|
68
35
|
}
|
|
69
36
|
}
|
|
70
37
|
/**
|
|
@@ -117,22 +84,18 @@ export async function symlink(filePath, linkPath, type) {
|
|
|
117
84
|
* @param path 要删除的文件路径
|
|
118
85
|
*/
|
|
119
86
|
export async function unlink(path) {
|
|
120
|
-
for (let i = 0; i
|
|
87
|
+
for (let i = 0; i < 4; ++i) {
|
|
121
88
|
try {
|
|
122
89
|
await fs.promises.unlink(path);
|
|
123
90
|
return true;
|
|
124
91
|
}
|
|
125
92
|
catch {
|
|
126
|
-
|
|
93
|
+
if (i < 3) {
|
|
94
|
+
await lCore.sleep(250);
|
|
95
|
+
}
|
|
127
96
|
}
|
|
128
97
|
}
|
|
129
|
-
|
|
130
|
-
await fs.promises.unlink(path);
|
|
131
|
-
return true;
|
|
132
|
-
}
|
|
133
|
-
catch {
|
|
134
|
-
return false;
|
|
135
|
-
}
|
|
98
|
+
return false;
|
|
136
99
|
}
|
|
137
100
|
/**
|
|
138
101
|
* --- 获取对象是否存在,存在则返回 stats 对象,否则返回 null ---
|
|
@@ -152,10 +115,7 @@ export async function stats(path) {
|
|
|
152
115
|
*/
|
|
153
116
|
export async function isDir(path) {
|
|
154
117
|
const pstats = await stats(path);
|
|
155
|
-
|
|
156
|
-
return false;
|
|
157
|
-
}
|
|
158
|
-
return pstats;
|
|
118
|
+
return pstats?.isDirectory() ? pstats : false;
|
|
159
119
|
}
|
|
160
120
|
/**
|
|
161
121
|
* --- 判断是否是文件或文件是否存在,是的话返回 stats ---
|
|
@@ -163,10 +123,7 @@ export async function isDir(path) {
|
|
|
163
123
|
*/
|
|
164
124
|
export async function isFile(path) {
|
|
165
125
|
const pstats = await stats(path);
|
|
166
|
-
|
|
167
|
-
return false;
|
|
168
|
-
}
|
|
169
|
-
return pstats;
|
|
126
|
+
return pstats?.isFile() ? pstats : false;
|
|
170
127
|
}
|
|
171
128
|
/**
|
|
172
129
|
* --- 深度创建目录,如果最末目录存在,则自动创建成功 ---
|
|
@@ -174,10 +131,6 @@ export async function isFile(path) {
|
|
|
174
131
|
* @param mode 权限
|
|
175
132
|
*/
|
|
176
133
|
export async function mkdir(path, mode = 0o755) {
|
|
177
|
-
if (await isDir(path)) {
|
|
178
|
-
return true;
|
|
179
|
-
}
|
|
180
|
-
// --- 深度创建目录 ---
|
|
181
134
|
try {
|
|
182
135
|
await fs.promises.mkdir(path, {
|
|
183
136
|
'recursive': true,
|
|
@@ -209,30 +162,24 @@ export async function rmdir(path) {
|
|
|
209
162
|
* --- Danger 危险:危险函数,尽量不要使用 ---
|
|
210
163
|
* --- This is a danger function, please don't use it ---
|
|
211
164
|
* --- 删除一个非空目录 ---
|
|
165
|
+
* @param path 目录路径,不递归读取符号链接指向的目录
|
|
166
|
+
* @returns 删除成功或根路径不是目录返回 true,删除失败返回 false
|
|
212
167
|
*/
|
|
213
168
|
export async function rmdirDeep(path) {
|
|
169
|
+
// --- 去掉末尾斜线再用 lstat 检查,避免目录链接被跟随 ---
|
|
170
|
+
if (await isDir(path.replace(/\/+$/, '') || path) === false) {
|
|
171
|
+
return true;
|
|
172
|
+
}
|
|
214
173
|
if (!path.endsWith('/')) {
|
|
215
174
|
path += '/';
|
|
216
175
|
}
|
|
217
176
|
const list = await readDir(path);
|
|
218
177
|
for (const item of list) {
|
|
219
|
-
const
|
|
220
|
-
|
|
178
|
+
const target = path + item.name;
|
|
179
|
+
const result = item.isDirectory() ? await rmdirDeep(target) : await unlink(target);
|
|
180
|
+
if (!result) {
|
|
221
181
|
return false;
|
|
222
182
|
}
|
|
223
|
-
if (stat.isDirectory()) {
|
|
224
|
-
// --- 目录 ---
|
|
225
|
-
const rtn = await rmdirDeep(path + item.name);
|
|
226
|
-
if (!rtn) {
|
|
227
|
-
return false;
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
else {
|
|
231
|
-
const rtn = await unlink(path + item.name);
|
|
232
|
-
if (!rtn) {
|
|
233
|
-
return false;
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
183
|
}
|
|
237
184
|
return rmdir(path);
|
|
238
185
|
}
|
|
@@ -270,28 +217,12 @@ export async function rename(oldPath, newPath) {
|
|
|
270
217
|
*/
|
|
271
218
|
export async function readDir(path, encoding) {
|
|
272
219
|
try {
|
|
273
|
-
const list =
|
|
274
|
-
const dlist = await fs.promises.readdir(path, {
|
|
220
|
+
const list = await fs.promises.readdir(path, {
|
|
275
221
|
'encoding': encoding,
|
|
276
222
|
'withFileTypes': true
|
|
277
223
|
});
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
continue;
|
|
281
|
-
}
|
|
282
|
-
list.push(item);
|
|
283
|
-
}
|
|
284
|
-
// --- 将 list 根据先目录后文件排序,如果是同是目录或文件,则以名称排序 ---
|
|
285
|
-
list.sort((a, b) => {
|
|
286
|
-
// --- 目录排在文件前面 ---
|
|
287
|
-
if (a.isDirectory() && !b.isDirectory())
|
|
288
|
-
return -1;
|
|
289
|
-
if (!a.isDirectory() && b.isDirectory())
|
|
290
|
-
return 1;
|
|
291
|
-
// --- 同类型按名称排序 ---
|
|
292
|
-
return a.name.localeCompare(b.name);
|
|
293
|
-
});
|
|
294
|
-
return list;
|
|
224
|
+
// --- 目录优先,同类型按名称排序;readdir 本身不返回 . 和 .. ---
|
|
225
|
+
return list.sort((a, b) => Number(b.isDirectory()) - Number(a.isDirectory()) || a.name.localeCompare(b.name));
|
|
295
226
|
}
|
|
296
227
|
catch {
|
|
297
228
|
return [];
|
|
@@ -319,13 +250,11 @@ export async function copyFolder(from, to, ignore = []) {
|
|
|
319
250
|
if (r === -1) {
|
|
320
251
|
return r;
|
|
321
252
|
}
|
|
322
|
-
|
|
323
|
-
num += r;
|
|
324
|
-
}
|
|
253
|
+
num += r;
|
|
325
254
|
}
|
|
326
255
|
else if (item.isFile()) {
|
|
327
256
|
// --- 先判断本文件是否被排除 ---
|
|
328
|
-
if (
|
|
257
|
+
if (lText.match(item.name, ignore)) {
|
|
329
258
|
continue;
|
|
330
259
|
}
|
|
331
260
|
if (!checkTo) {
|
|
@@ -362,21 +291,7 @@ export async function copyFile(src, dest) {
|
|
|
362
291
|
* @param options 编码或配置
|
|
363
292
|
*/
|
|
364
293
|
export function createReadStream(path, options) {
|
|
365
|
-
|
|
366
|
-
options = {
|
|
367
|
-
'encoding': options
|
|
368
|
-
};
|
|
369
|
-
}
|
|
370
|
-
else {
|
|
371
|
-
options ??= {};
|
|
372
|
-
}
|
|
373
|
-
return fs.createReadStream(path, {
|
|
374
|
-
'flags': options.flags,
|
|
375
|
-
'encoding': options.encoding,
|
|
376
|
-
'autoClose': options.autoClose,
|
|
377
|
-
'start': options.start,
|
|
378
|
-
'end': options.end
|
|
379
|
-
});
|
|
294
|
+
return fs.createReadStream(path, typeof options === 'string' ? { 'encoding': options } : options);
|
|
380
295
|
}
|
|
381
296
|
/**
|
|
382
297
|
* --- 读取文件写入到流,并等待写入完成 ---
|
|
@@ -399,28 +314,102 @@ export function pipe(path, destination, options) {
|
|
|
399
314
|
* @param options 编码或配置
|
|
400
315
|
*/
|
|
401
316
|
export function createWriteStream(path, options) {
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
317
|
+
return fs.createWriteStream(path, typeof options === 'string' ? { 'encoding': options } : options);
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* --- 解析并合并字节区间,限制多段请求的读取开销 ---
|
|
321
|
+
* @param value Range 请求头
|
|
322
|
+
* @param size 文件字节数
|
|
323
|
+
* @returns 区间列表;false 表示全部越界或区间过多;null 表示忽略请求头
|
|
324
|
+
*/
|
|
325
|
+
function parseFileRanges(value, size) {
|
|
326
|
+
if (!/^bytes=/i.test(value) || size === 0 || !Number.isSafeInteger(size)) {
|
|
327
|
+
return null;
|
|
406
328
|
}
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
329
|
+
const values = value.slice(6).split(',');
|
|
330
|
+
// --- 防止客户端利用大量小区间放大文件读取与响应开销 ---
|
|
331
|
+
if (value.length > 8192 || values.length > 16) {
|
|
332
|
+
return false;
|
|
333
|
+
}
|
|
334
|
+
const total = BigInt(size);
|
|
335
|
+
const ranges = [];
|
|
336
|
+
let count = 0;
|
|
337
|
+
for (const item of values) {
|
|
338
|
+
const part = item.trim();
|
|
339
|
+
if (part === '') {
|
|
340
|
+
continue;
|
|
341
|
+
}
|
|
342
|
+
const match = /^(\d*)-(\d*)$/.exec(part);
|
|
343
|
+
if (!match || match[1] === '' && match[2] === '') {
|
|
344
|
+
return null;
|
|
345
|
+
}
|
|
346
|
+
++count;
|
|
347
|
+
// --- 先以 BigInt 解析,避免超大偏移被浮点取整后读错字节 ---
|
|
348
|
+
const start = match[1] !== '' ? BigInt(match[1]) : total - BigInt(match[2]);
|
|
349
|
+
const end = match[1] !== '' && match[2] !== '' ? BigInt(match[2]) : total - 1n;
|
|
350
|
+
if (match[1] !== '' && match[2] !== '' && end < start) {
|
|
351
|
+
return null;
|
|
352
|
+
}
|
|
353
|
+
if (start >= total) {
|
|
354
|
+
continue;
|
|
355
|
+
}
|
|
356
|
+
ranges.push({
|
|
357
|
+
'start': Number(start < 0n ? 0n : start),
|
|
358
|
+
'end': Number(end >= total ? total - 1n : end),
|
|
359
|
+
'order': count
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
if (count === 0) {
|
|
363
|
+
return null;
|
|
364
|
+
}
|
|
365
|
+
if (ranges.length === 0) {
|
|
366
|
+
return false;
|
|
367
|
+
}
|
|
368
|
+
ranges.sort((a, b) => a.start - b.start);
|
|
369
|
+
const merged = [];
|
|
370
|
+
for (const range of ranges) {
|
|
371
|
+
const previous = merged[merged.length - 1];
|
|
372
|
+
if (previous && range.start <= previous.end + 1) {
|
|
373
|
+
previous.end = Math.max(previous.end, range.end);
|
|
374
|
+
previous.order = Math.min(previous.order, range.order);
|
|
375
|
+
}
|
|
376
|
+
else {
|
|
377
|
+
merged.push(range);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
return merged.sort((a, b) => a.order - b.order);
|
|
417
381
|
}
|
|
418
382
|
/**
|
|
419
|
-
* ---
|
|
383
|
+
* --- 逐段读取 multipart 响应,保持背压且在取消时关闭当前文件流 ---
|
|
384
|
+
* @param path 文件路径
|
|
385
|
+
* @param ranges 文件区间
|
|
386
|
+
* @param headers 每段的 MIME 头
|
|
387
|
+
* @param footer 结束边界
|
|
388
|
+
* @returns multipart 数据流
|
|
389
|
+
*/
|
|
390
|
+
async function* readFileRanges(path, ranges, headers, footer) {
|
|
391
|
+
for (let i = 0; i < ranges.length; ++i) {
|
|
392
|
+
yield headers[i];
|
|
393
|
+
yield* createReadStream(path, ranges[i]);
|
|
394
|
+
yield Buffer.from('\r\n');
|
|
395
|
+
}
|
|
396
|
+
yield footer;
|
|
397
|
+
}
|
|
398
|
+
/**
|
|
399
|
+
* --- 读取文件并输出到 http 的 response,支持 GET 字节范围与条件请求 ---
|
|
400
|
+
*
|
|
401
|
+
* Range 支持 start-end、start-、-length 及最多 16 段,请求头上限 8 KiB;相邻和重叠区间合并。
|
|
402
|
+
* 有效区间返回 206,全部越界或区间过多返回 416;无效语法、未知单位和空文件忽略 Range。
|
|
403
|
+
* 部分响应按原始文件字节流式输出,不进行动态压缩。If-Range 日期须匹配 Last-Modified,
|
|
404
|
+
* 且文件不启用动态压缩、修改时间早于当前响应所在秒,否则退回完整响应;
|
|
405
|
+
* 文件元数据生成的 ETag 为弱校验值,不接受其作为 If-Range 的强校验值。
|
|
406
|
+
* HEAD 忽略 Range 且不输出响应体。
|
|
407
|
+
*
|
|
420
408
|
* @param path 文件绝对路径
|
|
421
409
|
* @param req http 请求对象
|
|
422
410
|
* @param res http 响应对象
|
|
423
411
|
* @param stat 文件的 stat(如果有)
|
|
412
|
+
* @returns 输出完成或连接终止后无返回值
|
|
424
413
|
*/
|
|
425
414
|
export async function readToResponse(path, req, res, stat) {
|
|
426
415
|
stat ??= await stats(path);
|
|
@@ -428,7 +417,7 @@ export async function readToResponse(path, req, res, stat) {
|
|
|
428
417
|
const content = '<h1>404 Not found</h1><hr>Kebab';
|
|
429
418
|
res.setHeader('content-length', Buffer.byteLength(content));
|
|
430
419
|
lCore.writeHead(res, 404);
|
|
431
|
-
res.end(content);
|
|
420
|
+
res.end(req.method === 'HEAD' ? '' : content);
|
|
432
421
|
return;
|
|
433
422
|
}
|
|
434
423
|
// --- 判断缓存以及 MIME 和编码 ---
|
|
@@ -438,44 +427,110 @@ export async function readToResponse(path, req, res, stat) {
|
|
|
438
427
|
if (mimeData.mime.startsWith('text/') || ['json', 'xml', 'svg', 'js', 'mjs', 'map', 'webmanifest'].includes(mimeData.extension)) {
|
|
439
428
|
charset = '; charset=utf-8';
|
|
440
429
|
}
|
|
430
|
+
const hash = `W/"${stat.size.toString(16)}-${stat.mtime.getTime().toString(16)}"`;
|
|
431
|
+
const lastModified = stat.mtime.toUTCString();
|
|
432
|
+
const modifiedTime = Date.parse(lastModified);
|
|
433
|
+
const canCompress = mimeData.compressible && stat.size >= 1024;
|
|
434
|
+
res.setHeader('etag', hash);
|
|
435
|
+
res.setHeader('last-modified', lastModified);
|
|
436
|
+
res.setHeader('accept-ranges', 'bytes');
|
|
437
|
+
if (canCompress) {
|
|
438
|
+
const vary = res.getHeader('vary');
|
|
439
|
+
const values = (Array.isArray(vary) ? vary.join(',') : String(vary ?? '')).split(',').map((item) => item.trim()).filter(Boolean);
|
|
440
|
+
if (!values.some((item) => item === '*' || item.toLowerCase() === 'accept-encoding')) {
|
|
441
|
+
values.push('Accept-Encoding');
|
|
442
|
+
res.setHeader('vary', values.join(', '));
|
|
443
|
+
}
|
|
444
|
+
}
|
|
441
445
|
// --- 这些文件可能需要缓存 ---
|
|
442
446
|
if (['htm', 'html', 'css', 'js', 'mjs', 'xml', 'jpg', 'jpeg', 'svg', 'gif', 'png', 'json'].includes(mimeData.extension)) {
|
|
443
447
|
/** --- 静态文件默认缓存秒数 --- */
|
|
444
448
|
const cacheTTL = 600;
|
|
445
|
-
const hash = `W/"${stat.size.toString(16)}-${stat.mtime.getTime().toString(16)}"`;
|
|
446
|
-
const lastModified = stat.mtime.toUTCString();
|
|
447
|
-
res.setHeader('etag', hash);
|
|
448
449
|
res.setHeader('expires', new Date(Date.now() + cacheTTL * 1_000).toUTCString());
|
|
449
450
|
res.setHeader('cache-control', 'public, max-age=' + cacheTTL.toString());
|
|
450
|
-
// --- 判断返回 304 吗 ---
|
|
451
|
-
const noneMatch = req.headers['if-none-match'];
|
|
452
|
-
const modifiedSince = req.headers['if-modified-since'];
|
|
453
|
-
if ((hash === noneMatch) && (lastModified === modifiedSince)) {
|
|
454
|
-
lCore.writeHead(res, 304);
|
|
455
|
-
res.end();
|
|
456
|
-
return;
|
|
457
|
-
}
|
|
458
|
-
res.setHeader('last-modified', lastModified);
|
|
459
451
|
}
|
|
460
452
|
else {
|
|
461
453
|
res.setHeader('cache-control', 'no-cache, must-revalidate');
|
|
462
454
|
}
|
|
463
|
-
// ---
|
|
464
|
-
|
|
455
|
+
// --- 条件请求优先于 Range;弱 ETag 只能用于缓存验证,不能用于 If-Match ---
|
|
456
|
+
const match = req.headers['if-match'];
|
|
457
|
+
const unmodifiedSince = req.headers['if-unmodified-since'];
|
|
458
|
+
if (match !== undefined ? match.trim() !== '*' : unmodifiedSince !== undefined && modifiedTime > Date.parse(unmodifiedSince)) {
|
|
459
|
+
res.setHeader('content-length', 0);
|
|
460
|
+
lCore.writeHead(res, 412);
|
|
461
|
+
res.end();
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
const noneMatch = req.headers['if-none-match'];
|
|
465
|
+
const modifiedSince = req.headers['if-modified-since'];
|
|
466
|
+
const isRead = req.method === 'GET' || req.method === 'HEAD';
|
|
467
|
+
const unchanged = noneMatch !== undefined ? noneMatch.trim() === '*' || noneMatch.split(',').some((item) => item.trim().replace(/^W\//, '') === hash.slice(2)) :
|
|
468
|
+
isRead && modifiedSince !== undefined && modifiedTime <= Date.parse(modifiedSince);
|
|
469
|
+
if (unchanged) {
|
|
470
|
+
if (!isRead) {
|
|
471
|
+
res.setHeader('content-length', 0);
|
|
472
|
+
}
|
|
473
|
+
lCore.writeHead(res, isRead ? 304 : 412);
|
|
474
|
+
res.end();
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
const contentType = mimeData.mime + charset;
|
|
478
|
+
res.setHeader('content-type', contentType);
|
|
479
|
+
const rangeHeader = req.headers['range'];
|
|
480
|
+
const ifRange = req.headers['if-range'];
|
|
481
|
+
// --- 日期必须精确匹配且至少早一秒;压缩与原始字节共享的日期不能作强校验,避免跨编码续传 ---
|
|
482
|
+
const rangeMatches = ifRange === undefined || typeof ifRange === 'string' && !ifRange.includes('"') &&
|
|
483
|
+
Date.parse(ifRange) === modifiedTime && modifiedTime <= Date.now() - 1_000 && !canCompress;
|
|
484
|
+
const ranges = req.method === 'GET' && rangeHeader !== undefined &&
|
|
485
|
+
rangeMatches ? parseFileRanges(rangeHeader, stat.size) : null;
|
|
486
|
+
if (ranges === false) {
|
|
487
|
+
res.setHeader('content-range', `bytes */${stat.size}`);
|
|
488
|
+
res.setHeader('content-length', 0);
|
|
489
|
+
lCore.writeHead(res, 416);
|
|
490
|
+
res.end();
|
|
491
|
+
return;
|
|
492
|
+
}
|
|
465
493
|
// --- 判断客户端支持的压缩模式 ---
|
|
466
494
|
const encoding = req.headers['accept-encoding'] ?? '';
|
|
467
|
-
if (
|
|
495
|
+
if (ranges === null && canCompress && encoding !== '') {
|
|
468
496
|
// --- 压缩 ---
|
|
469
497
|
const compress = await lZlib.compress(encoding, await getContent(path));
|
|
470
498
|
if (compress) {
|
|
471
499
|
res.setHeader('content-encoding', compress.type);
|
|
472
500
|
res.setHeader('content-length', Buffer.byteLength(compress.buffer));
|
|
473
|
-
|
|
501
|
+
lCore.writeHead(res, 200);
|
|
502
|
+
res.end(req.method === 'HEAD' ? '' : compress.buffer);
|
|
474
503
|
return;
|
|
475
504
|
}
|
|
476
505
|
}
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
506
|
+
let headers = [];
|
|
507
|
+
let footer = Buffer.alloc(0);
|
|
508
|
+
if (ranges !== null && ranges.length > 1) {
|
|
509
|
+
const boundary = `kebab-${crypto.randomBytes(16).toString('hex')}`;
|
|
510
|
+
headers = ranges.map((range) => Buffer.from(`--${boundary}\r\nContent-Type: ${contentType}\r\nContent-Range: bytes ${range.start}-${range.end}/${stat.size}\r\n\r\n`));
|
|
511
|
+
footer = Buffer.from(`--${boundary}--\r\n`);
|
|
512
|
+
res.setHeader('content-type', `multipart/byteranges; boundary=${boundary}`);
|
|
513
|
+
res.setHeader('content-length', ranges.reduce((length, range, i) => length + headers[i].length + (range.end - range.start + 1) + 2, footer.length));
|
|
514
|
+
}
|
|
515
|
+
else if (ranges !== null) {
|
|
516
|
+
res.setHeader('content-range', `bytes ${ranges[0].start}-${ranges[0].end}/${stat.size}`);
|
|
517
|
+
res.setHeader('content-length', ranges[0].end - ranges[0].start + 1);
|
|
518
|
+
}
|
|
519
|
+
else {
|
|
520
|
+
res.setHeader('content-length', stat.size);
|
|
521
|
+
}
|
|
522
|
+
lCore.writeHead(res, ranges === null ? 200 : 206);
|
|
523
|
+
if (req.method === 'HEAD') {
|
|
524
|
+
res.end();
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
527
|
+
const source = ranges !== null && ranges.length > 1 ?
|
|
528
|
+
stream.Readable.from(readFileRanges(path, ranges, headers, footer)) : createReadStream(path, ranges?.[0]);
|
|
529
|
+
try {
|
|
530
|
+
await stream.promises.pipeline(source, res instanceof http2.Http2ServerResponse ? (res.stream ?? res) : res);
|
|
531
|
+
}
|
|
532
|
+
catch {
|
|
533
|
+
// --- 已提交响应头后不能再发送错误页;管道会关闭文件流及断开的响应 ---
|
|
534
|
+
res.destroy();
|
|
535
|
+
}
|
|
481
536
|
}
|