@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 CHANGED
@@ -1589,7 +1589,7 @@ index/variables/VER.md
1589
1589
 
1590
1590
  # Variable: VER
1591
1591
 
1592
- > `const` **VER**: `"9.20.1"` = `'9.20.1'`
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:277](https://github.com/maiyunnet/kebab/blob/master/lib/fs.ts#L277)
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:385](https://github.com/maiyunnet/kebab/blob/master/lib/fs.ts#L385)
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:340](https://github.com/maiyunnet/kebab/blob/master/lib/fs.ts#L340)
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:400](https://github.com/maiyunnet/kebab/blob/master/lib/fs.ts#L400)
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:447](https://github.com/maiyunnet/kebab/blob/master/lib/fs.ts#L447)
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:14](https://github.com/maiyunnet/kebab/blob/master/lib/fs.ts#L14)
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:18](https://github.com/maiyunnet/kebab/blob/master/lib/fs.ts#L18)
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:181](https://github.com/maiyunnet/kebab/blob/master/lib/fs.ts#L181)
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:193](https://github.com/maiyunnet/kebab/blob/master/lib/fs.ts#L193)
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:206](https://github.com/maiyunnet/kebab/blob/master/lib/fs.ts#L206)
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:430](https://github.com/maiyunnet/kebab/blob/master/lib/fs.ts#L430)
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:92](https://github.com/maiyunnet/kebab/blob/master/lib/fs.ts#L92)
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:306](https://github.com/maiyunnet/kebab/blob/master/lib/fs.ts#L306)
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:114](https://github.com/maiyunnet/kebab/blob/master/lib/fs.ts#L114)
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:478](https://github.com/maiyunnet/kebab/blob/master/lib/fs.ts#L478)
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
- 读取文件并输出到 http 的 response
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:292](https://github.com/maiyunnet/kebab/blob/master/lib/fs.ts#L292)
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:245](https://github.com/maiyunnet/kebab/blob/master/lib/fs.ts#L245)
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:227](https://github.com/maiyunnet/kebab/blob/master/lib/fs.ts#L227)
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:168](https://github.com/maiyunnet/kebab/blob/master/lib/fs.ts#L168)
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:131](https://github.com/maiyunnet/kebab/blob/master/lib/fs.ts#L131)
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:145](https://github.com/maiyunnet/kebab/blob/master/lib/fs.ts#L145)
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
@@ -5,7 +5,7 @@
5
5
  * --- 本文件用来定义每个目录实体地址的常量 ---
6
6
  */
7
7
  /** --- 当前系统版本号 --- */
8
- export declare const VER = "9.20.1";
8
+ export declare const VER = "9.21.0";
9
9
  /** --- 框架根目录,以 / 结尾 --- */
10
10
  export declare const ROOT_PATH: string;
11
11
  /** --- 框架的 LIB,以 / 结尾 --- */
package/index.js CHANGED
@@ -6,7 +6,7 @@
6
6
  * --- 本文件用来定义每个目录实体地址的常量 ---
7
7
  */
8
8
  /** --- 当前系统版本号 --- */
9
- export const VER = '9.20.1';
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
- if (typeof options === 'string') {
19
- options = {
20
- 'encoding': options
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
- catch {
66
- return null;
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 <= 2; ++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
- await lCore.sleep(250);
93
+ if (i < 3) {
94
+ await lCore.sleep(250);
95
+ }
127
96
  }
128
97
  }
129
- try {
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
- if (!pstats?.isDirectory()) {
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
- if (!pstats?.isFile()) {
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 stat = await stats(item.name);
220
- if (!stat) {
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
- for (const item of dlist) {
279
- if (item.name === '.' || item.name === '..') {
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
- else {
323
- num += r;
324
- }
253
+ num += r;
325
254
  }
326
255
  else if (item.isFile()) {
327
256
  // --- 先判断本文件是否被排除 ---
328
- if (ignore.length > 0 && lText.match(item.name, ignore)) {
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
- if (typeof options === 'string') {
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
- if (typeof options === 'string') {
403
- options = {
404
- 'encoding': options
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
- else {
408
- options ??= {};
409
- }
410
- return fs.createWriteStream(path, {
411
- 'flags': options.flags,
412
- 'encoding': options.encoding,
413
- 'mode': options.mode,
414
- 'autoClose': options.autoClose,
415
- 'start': options.start
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
- * --- 读取文件并输出到 http 的 response ---
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
- // --- 设置 type ---
464
- res.setHeader('content-type', mimeData.mime + charset);
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 (mimeData.compressible && (stat.size >= 1024)) {
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
- res.end(compress.buffer);
501
+ lCore.writeHead(res, 200);
502
+ res.end(req.method === 'HEAD' ? '' : compress.buffer);
474
503
  return;
475
504
  }
476
505
  }
477
- // --- 不压缩 ---
478
- res.setHeader('content-length', stat.size);
479
- lCore.writeHead(res, 200);
480
- await pipe(path, res instanceof http2.Http2ServerResponse ? (res.stream ?? res) : res);
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@maiyunnet/kebab",
3
- "version": "9.20.1",
3
+ "version": "9.21.0",
4
4
  "description": "Simple, easy-to-use, and fully-featured Node.js framework that is ready-to-use out of the box.",
5
5
  "type": "module",
6
6
  "keywords": [