@maiyunnet/kebab 9.20.0 → 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.0"` = `'9.20.0'`
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
 
@@ -122018,7 +122031,7 @@ Defined in: [lib/s3.ts:88](https://github.com/maiyunnet/kebab/blob/master/lib/s3
122018
122031
 
122019
122032
  > **deleteObject**(`key`, `bucket?`): `Promise`\<`boolean`\>
122020
122033
 
122021
- Defined in: [lib/s3.ts:279](https://github.com/maiyunnet/kebab/blob/master/lib/s3.ts#L279)
122034
+ Defined in: [lib/s3.ts:367](https://github.com/maiyunnet/kebab/blob/master/lib/s3.ts#L367)
122022
122035
 
122023
122036
  删除对象
122024
122037
 
@@ -122046,7 +122059,7 @@ bucket 名
122046
122059
 
122047
122060
  > **deleteObjects**(`keys`, `bucket?`): `Promise`\<`boolean`\>
122048
122061
 
122049
- Defined in: [lib/s3.ts:299](https://github.com/maiyunnet/kebab/blob/master/lib/s3.ts#L299)
122062
+ Defined in: [lib/s3.ts:387](https://github.com/maiyunnet/kebab/blob/master/lib/s3.ts#L387)
122050
122063
 
122051
122064
  批量删除对象
122052
122065
 
@@ -122074,7 +122087,7 @@ bucket 名
122074
122087
 
122075
122088
  > **destroy**(): `void`
122076
122089
 
122077
- Defined in: [lib/s3.ts:341](https://github.com/maiyunnet/kebab/blob/master/lib/s3.ts#L341)
122090
+ Defined in: [lib/s3.ts:429](https://github.com/maiyunnet/kebab/blob/master/lib/s3.ts#L429)
122078
122091
 
122079
122092
  销毁连接,释放资源
122080
122093
  一般会自动垃圾回收,但高频接口也可主动调用
@@ -122089,7 +122102,7 @@ Defined in: [lib/s3.ts:341](https://github.com/maiyunnet/kebab/blob/master/lib/s
122089
122102
 
122090
122103
  > **getObject**(`key`, `bucket?`): `Promise`\<`false` \| `Readable` & `SdkStreamMixin` \| `Blob` & `SdkStreamMixin` \| `ReadableStream`\<`any`\> & `SdkStreamMixin` \| `undefined`\>
122091
122104
 
122092
- Defined in: [lib/s3.ts:235](https://github.com/maiyunnet/kebab/blob/master/lib/s3.ts#L235)
122105
+ Defined in: [lib/s3.ts:323](https://github.com/maiyunnet/kebab/blob/master/lib/s3.ts#L323)
122093
122106
 
122094
122107
  获取对象流,可通过流获取 buffer 或 text
122095
122108
 
@@ -122117,7 +122130,7 @@ bucket 名
122117
122130
 
122118
122131
  > **getObjectBuffer**(`key`, `bucket?`): `Promise`\<`false` \| `Buffer`\<`ArrayBufferLike`\>\>
122119
122132
 
122120
- Defined in: [lib/s3.ts:256](https://github.com/maiyunnet/kebab/blob/master/lib/s3.ts#L256)
122133
+ Defined in: [lib/s3.ts:344](https://github.com/maiyunnet/kebab/blob/master/lib/s3.ts#L344)
122121
122134
 
122122
122135
  获取对象 Buffer
122123
122136
 
@@ -122147,7 +122160,7 @@ bucket 名
122147
122160
 
122148
122161
  > **headObject**(`key`, `bucket?`): `Promise`\<`false` \| `HeadObjectCommandOutput`\>
122149
122162
 
122150
- Defined in: [lib/s3.ts:321](https://github.com/maiyunnet/kebab/blob/master/lib/s3.ts#L321)
122163
+ Defined in: [lib/s3.ts:409](https://github.com/maiyunnet/kebab/blob/master/lib/s3.ts#L409)
122151
122164
 
122152
122165
  检测对象是否存在
122153
122166
 
@@ -122175,9 +122188,9 @@ bucket 名
122175
122188
 
122176
122189
  > **putObject**(`key`, `content`, `length?`, `bucket?`): `Promise`\<`false` \| `CompleteMultipartUploadCommandOutput`\>
122177
122190
 
122178
- Defined in: [lib/s3.ts:139](https://github.com/maiyunnet/kebab/blob/master/lib/s3.ts#L139)
122191
+ Defined in: [lib/s3.ts:140](https://github.com/maiyunnet/kebab/blob/master/lib/s3.ts#L140)
122179
122192
 
122180
- 上传对象(可传流且也可无需设置 length) --
122193
+ 上传对象,流内容可省略 length
122181
122194
 
122182
122195
  #### Parameters
122183
122196
 
@@ -122195,9 +122208,9 @@ Defined in: [lib/s3.ts:139](https://github.com/maiyunnet/kebab/blob/master/lib/s
122195
122208
 
122196
122209
  ##### length?
122197
122210
 
122198
- `number` \| \{ `bucket?`: `string`; `disposition?`: `string`; `length?`: `number`; `type?`: `string`; \}
122211
+ `number` \| \{ `bucket?`: `string`; `disposition?`: `string`; `length?`: `number`; `signal?`: `AbortSignal`; `type?`: `string`; \}
122199
122212
 
122200
- 设置 contentLength,如果是流模式则需要设置此项,也可以设置为对象参数
122213
+ contentLength 或上传选项
122201
122214
 
122202
122215
  `number`
122203
122216
 
@@ -122205,22 +122218,36 @@ Defined in: [lib/s3.ts:139](https://github.com/maiyunnet/kebab/blob/master/lib/s
122205
122218
 
122206
122219
  ###### Type Literal
122207
122220
 
122208
- \{ `bucket?`: `string`; `disposition?`: `string`; `length?`: `number`; `type?`: `string`; \}
122221
+ \{ `bucket?`: `string`; `disposition?`: `string`; `length?`: `number`; `signal?`: `AbortSignal`; `type?`: `string`; \}
122209
122222
 
122210
- 设置 contentLength,如果是流模式则需要设置此项,也可以设置为对象参数
122223
+ contentLength 或上传选项
122211
122224
 
122212
122225
  ###### bucket?
122213
122226
 
122214
122227
  `string`
122215
122228
 
122229
+ bucket 名,省略时使用预定义 bucket
122230
+
122216
122231
  ###### disposition?
122217
122232
 
122218
122233
  `string`
122219
122234
 
122235
+ content-disposition,如 attachment
122236
+
122220
122237
  ###### length?
122221
122238
 
122222
122239
  `number`
122223
122240
 
122241
+ contentLength,流内容可省略
122242
+
122243
+ ###### signal?
122244
+
122245
+ `AbortSignal`
122246
+
122247
+ 取消上传请求并销毁输入流,等待已知的未完成分片清理后返回 false
122248
+ 清理使用独立的 30 秒超时信号,失败时记录错误;不传则沿用原上传流程
122249
+ 不删除同名对象,上传完成与取消同时发生时,由调用方清理独立任务文件
122250
+
122224
122251
  ###### type?
122225
122252
 
122226
122253
  `string`
@@ -122237,13 +122264,15 @@ bucket 名
122237
122264
 
122238
122265
  `Promise`\<`false` \| `CompleteMultipartUploadCommandOutput`\>
122239
122266
 
122267
+ 上传结果,失败或取消返回 false;传入 signal 时,返回前会尝试清理已知的未完成分片
122268
+
122240
122269
  ***
122241
122270
 
122242
122271
  ### putObjects()
122243
122272
 
122244
122273
  > **putObjects**(`items`, `options?`): `Promise`\<[`IPutObjectsItemResult`](../interfaces/IPutObjectsItemResult.md)[]\>
122245
122274
 
122246
- Defined in: [lib/s3.ts:184](https://github.com/maiyunnet/kebab/blob/master/lib/s3.ts#L184)
122275
+ Defined in: [lib/s3.ts:272](https://github.com/maiyunnet/kebab/blob/master/lib/s3.ts#L272)
122247
122276
 
122248
122277
  批量上传对象,并发控制,单次失败不影响其他项
122249
122278
 
@@ -122347,7 +122376,7 @@ lib/s3/functions/get.md
122347
122376
 
122348
122377
  > **get**(`ctr`, `opt`): [`S3`](../classes/S3.md)
122349
122378
 
122350
- Defined in: [lib/s3.ts:351](https://github.com/maiyunnet/kebab/blob/master/lib/s3.ts#L351)
122379
+ Defined in: [lib/s3.ts:439](https://github.com/maiyunnet/kebab/blob/master/lib/s3.ts#L439)
122351
122380
 
122352
122381
  创建一个对象存储对象
122353
122382
 
package/index.d.ts CHANGED
@@ -5,7 +5,7 @@
5
5
  * --- 本文件用来定义每个目录实体地址的常量 ---
6
6
  */
7
7
  /** --- 当前系统版本号 --- */
8
- export declare const VER = "9.20.0";
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.0';
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/lib/s3.d.ts CHANGED
@@ -3,8 +3,8 @@
3
3
  * Date: 2024-2-18 18:32:45
4
4
  * Last: 2024-2-18 18:32:47, 2024-3-16 16:42:27, 2024-5-31 21:36:26, 2024-7-8 00:28:42, 2024-7-19 11:32:43, 2025-6-10 21:45:34, 2025-12-5 23:42:59
5
5
  */
6
- import * as s3 from '@aws-sdk/client-s3';
7
6
  import * as stream from 'stream';
7
+ import * as s3 from '@aws-sdk/client-s3';
8
8
  import * as sCtr from '#kebab/sys/ctr.js';
9
9
  /**
10
10
  * --- s3 文档:https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/client/s3/ ---
@@ -76,18 +76,28 @@ export declare class S3 {
76
76
  */
77
77
  setBucket(bucket: string): void;
78
78
  /**
79
- * --- 上传对象(可传流且也可无需设置 length) --
79
+ * --- 上传对象,流内容可省略 length ---
80
80
  * @param key 对象路径
81
81
  * @param content 内容
82
- * @param length 设置 contentLength,如果是流模式则需要设置此项,也可以设置为对象参数
82
+ * @param length contentLength 或上传选项
83
83
  * @param bucket bucket 名
84
+ * @returns 上传结果,失败或取消返回 false;传入 signal 时,返回前会尝试清理已知的未完成分片
84
85
  */
85
86
  putObject(key: string, content: string | Buffer | stream.Readable, length?: number | {
87
+ /** --- contentLength,流内容可省略 --- */
86
88
  'length'?: number;
87
89
  /** --- content-type,如 application/javascript --- */
88
90
  'type'?: string;
91
+ /** --- content-disposition,如 attachment --- */
89
92
  'disposition'?: string;
93
+ /** --- bucket 名,省略时使用预定义 bucket --- */
90
94
  'bucket'?: string;
95
+ /**
96
+ * --- 取消上传请求并销毁输入流,等待已知的未完成分片清理后返回 false ---
97
+ * --- 清理使用独立的 30 秒超时信号,失败时记录错误;不传则沿用原上传流程 ---
98
+ * --- 不删除同名对象,上传完成与取消同时发生时,由调用方清理独立任务文件 ---
99
+ */
100
+ 'signal'?: AbortSignal;
91
101
  }, bucket?: string): Promise<s3.CompleteMultipartUploadCommandOutput | false>;
92
102
  /**
93
103
  * --- 批量上传对象,并发控制,单次失败不影响其他项 ---
package/lib/s3.js CHANGED
@@ -4,6 +4,7 @@
4
4
  * Last: 2024-2-18 18:32:47, 2024-3-16 16:42:27, 2024-5-31 21:36:26, 2024-7-8 00:28:42, 2024-7-19 11:32:43, 2025-6-10 21:45:34, 2025-12-5 23:42:59
5
5
  */
6
6
  // --- 库和定义 ---
7
+ import * as stream from 'stream';
7
8
  import * as s3 from '@aws-sdk/client-s3';
8
9
  import * as ls from '@aws-sdk/lib-storage';
9
10
  import * as lCore from '#kebab/lib/core.js';
@@ -67,28 +68,79 @@ export class S3 {
67
68
  this._bucket = bucket;
68
69
  }
69
70
  /**
70
- * --- 上传对象(可传流且也可无需设置 length) --
71
+ * --- 上传对象,流内容可省略 length ---
71
72
  * @param key 对象路径
72
73
  * @param content 内容
73
- * @param length 设置 contentLength,如果是流模式则需要设置此项,也可以设置为对象参数
74
+ * @param length contentLength 或上传选项
74
75
  * @param bucket bucket 名
76
+ * @returns 上传结果,失败或取消返回 false;传入 signal 时,返回前会尝试清理已知的未完成分片
75
77
  */
76
78
  async putObject(key, content, length, bucket) {
79
+ /** --- 对象内容类型 --- */
80
+ let type;
81
+ /** --- 对象下载方式 --- */
82
+ let disposition;
83
+ /** --- 调用方传入的取消信号,仅作用于本次上传 --- */
84
+ let signal;
85
+ if (typeof length !== 'number') {
86
+ type = length?.type;
87
+ disposition = length?.disposition;
88
+ bucket = length?.bucket;
89
+ signal = length?.signal;
90
+ length = length?.length;
91
+ }
92
+ bucket ??= this._bucket;
93
+ /** --- 输入流,传入 signal 时在取消或失败后停止读取;字符串与 Buffer 无需释放 --- */
94
+ const source = content instanceof stream.Readable ? content : undefined;
95
+ /**
96
+ * --- 停止流读取,避免取消时仍阻塞在等待下一块数据的阶段 ---
97
+ * @returns 无返回值
98
+ */
99
+ const onAbort = () => {
100
+ source?.destroy();
101
+ };
102
+ /** --- 尚未确认合并或清理完成的分片上传 ID,用于失败后的兜底清理 --- */
103
+ let uploadId;
104
+ /** --- 是否取得有效上传结果,失败时需要释放输入流 --- */
105
+ let success = false;
106
+ /** --- 分片清理请求的超时毫秒数,避免清理阶段无限等待 --- */
107
+ const cleanupTimeout = 30000;
77
108
  try {
78
- /** --- content type --- */
79
- let type = undefined;
80
- /** --- content disposition --- */
81
- let disposition = undefined;
82
- if (typeof length !== 'number') {
83
- type = length?.type;
84
- disposition = length?.disposition;
85
- bucket = length?.bucket;
86
- length = length?.length;
109
+ if (signal?.aborted) {
110
+ return false;
87
111
  }
112
+ signal?.addEventListener('abort', onAbort, { 'once': true });
113
+ /** --- 本次上传的客户端入口,共享连接配置但不修改共享客户端的 send --- */
114
+ const client = signal ? Object.create(this._link) : this._link;
115
+ if (signal) {
116
+ /**
117
+ * --- 为上传命令传递取消信号,并跟踪未完成的分片上传 ---
118
+ * --- Upload.abort() 会提前结束 done(),且不会取消正在发送的请求 ---
119
+ * --- 逐条命令传入信号,让 done() 等待分片任务和清理结束 ---
120
+ * @param command SDK 上传或分片清理命令;Upload 仅使用 send 的 Promise 调用形式
121
+ * @returns SDK 命令结果
122
+ */
123
+ client.send = (async (command) => {
124
+ /** --- 清理请求必须使用独立信号,避免随上传请求一起取消 --- */
125
+ const isCleanup = command instanceof s3.AbortMultipartUploadCommand;
126
+ /** --- 命令成功后再更新分片 ID,失败时保留 ID 供 finally 重试清理 --- */
127
+ const res = await this._link.send(command, {
128
+ 'abortSignal': isCleanup ? AbortSignal.timeout(cleanupTimeout) : signal,
129
+ });
130
+ if (command instanceof s3.CreateMultipartUploadCommand) {
131
+ uploadId = res.UploadId;
132
+ }
133
+ else if (isCleanup || (command instanceof s3.CompleteMultipartUploadCommand)) {
134
+ uploadId = undefined;
135
+ }
136
+ return res;
137
+ });
138
+ }
139
+ /** --- SDK 上传任务,继续由 SDK 管理分片和并发 --- */
88
140
  const upload = new ls.Upload({
89
- 'client': this._link,
141
+ 'client': client,
90
142
  'params': {
91
- 'Bucket': bucket ?? this._bucket,
143
+ 'Bucket': bucket,
92
144
  'Key': key,
93
145
  'Body': content,
94
146
  'ContentLength': length,
@@ -96,13 +148,40 @@ export class S3 {
96
148
  'ContentDisposition': disposition,
97
149
  }
98
150
  });
151
+ /** --- 上传结果;取消与上传完成同时发生时仍向调用方返回 false --- */
99
152
  const res = await upload.done();
100
- return (res.Location && res.Bucket && res.Key) ? res : false;
153
+ if (signal?.aborted || !res.Location || !res.Bucket || !res.Key) {
154
+ return false;
155
+ }
156
+ success = true;
157
+ return res;
101
158
  }
102
159
  catch (e) {
103
- lCore.log(this._ctr, '[LIB][S3][putObject] ' + lText.stringifyJson(e.message ?? '').slice(1, -1).replace(/"/g, '""'), '-error');
160
+ if (!signal?.aborted) {
161
+ lCore.log(this._ctr, `[LIB][S3][putObject] ${lText.stringifyError(e)}`, '-error');
162
+ }
104
163
  return false;
105
164
  }
165
+ finally {
166
+ signal?.removeEventListener('abort', onAbort);
167
+ if (signal && !success) {
168
+ source?.destroy();
169
+ }
170
+ // --- SDK 的合并失败分支不会清理分片;SDK 清理失败时也保留 ID,在这里兜底 ---
171
+ // --- 只清理本次未完成的分片,不删除可能已存在的同名对象 ---
172
+ if (uploadId) {
173
+ try {
174
+ await this._link.send(new s3.AbortMultipartUploadCommand({
175
+ 'Bucket': bucket,
176
+ 'Key': key,
177
+ 'UploadId': uploadId,
178
+ }), { 'abortSignal': AbortSignal.timeout(cleanupTimeout) });
179
+ }
180
+ catch (e) {
181
+ lCore.log(this._ctr, `[LIB][S3][putObject][cleanup] ${lText.stringifyError(e)}`, '-error');
182
+ }
183
+ }
184
+ }
106
185
  }
107
186
  /**
108
187
  * --- 批量上传对象,并发控制,单次失败不影响其他项 ---
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@maiyunnet/kebab",
3
- "version": "9.20.0",
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": [