fsd 0.14.1 → 0.15.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/LICENSE +21 -0
- package/README.md +748 -2
- package/index.d.ts +1083 -71
- package/package.json +8 -3
package/README.md
CHANGED
|
@@ -1,5 +1,751 @@
|
|
|
1
1
|
# fsd
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
通用文件系统驱动库 - 为 Node.js 提供统一的文件存储抽象层。
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
[](https://www.npmjs.com/package/fsd)
|
|
6
|
+
|
|
7
|
+
## 概述
|
|
8
|
+
|
|
9
|
+
FSD (File System Driver) 是一个适配器模式的文件存储抽象层,提供统一的 API 接口,支持多种存储后端(本地磁盘、阿里云 OSS、阿里云 VOD)。
|
|
10
|
+
|
|
11
|
+
### 核心特性
|
|
12
|
+
|
|
13
|
+
- **统一 API** - 一套 API,多种存储后端
|
|
14
|
+
- **适配器模式** - 轻松扩展新存储后端
|
|
15
|
+
- **Promise 风格** - 所有操作返回 Promise
|
|
16
|
+
- **流式支持** - 支持大文件的流式读写
|
|
17
|
+
- **类型安全** - 完整的 TypeScript 类型定义
|
|
18
|
+
|
|
19
|
+
### 架构
|
|
20
|
+
|
|
21
|
+
```
|
|
22
|
+
┌─────────────────────────────────────┐
|
|
23
|
+
│ Your Application │
|
|
24
|
+
└──────────────┬──────────────────────┘
|
|
25
|
+
│
|
|
26
|
+
▼
|
|
27
|
+
┌─────────────────────────────────────┐
|
|
28
|
+
│ FSD Core │
|
|
29
|
+
│ (统一 API 抽象层) │
|
|
30
|
+
└──────┬──────────┬──────────┬────────┘
|
|
31
|
+
│ │ │
|
|
32
|
+
▼ ▼ ▼
|
|
33
|
+
┌──────────┐ ┌──────────┐ ┌──────────┐
|
|
34
|
+
│ FS │ │ OSS │ │ VOD │
|
|
35
|
+
│ Adapter │ │ Adapter │ │ Adapter │
|
|
36
|
+
│(本地磁盘) │ │ (阿里云) │ │ (阿里云) │
|
|
37
|
+
└──────────┘ └──────────┘ └──────────┘
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## 安装
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
npm install fsd
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
还需要安装对应的适配器包:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
# 本地文件系统
|
|
50
|
+
npm install fsd-fs
|
|
51
|
+
|
|
52
|
+
# 阿里云 OSS
|
|
53
|
+
npm install fsd-oss
|
|
54
|
+
|
|
55
|
+
# 阿里云 VOD
|
|
56
|
+
npm install fsd-vod
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## 快速开始
|
|
60
|
+
|
|
61
|
+
### 基础用法
|
|
62
|
+
|
|
63
|
+
```typescript
|
|
64
|
+
import FSD from 'fsd';
|
|
65
|
+
import FSAdapter from 'fsd-fs';
|
|
66
|
+
|
|
67
|
+
// 创建 FSD 实例
|
|
68
|
+
const fsd = FSD({
|
|
69
|
+
adapter: new FSAdapter({ root: '/uploads' })
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// 创建文件对象
|
|
73
|
+
const file = fsd('/test.txt');
|
|
74
|
+
|
|
75
|
+
// 写入文件
|
|
76
|
+
await file.write('Hello, FSD!');
|
|
77
|
+
|
|
78
|
+
// 读取文件
|
|
79
|
+
const content = await file.read('utf8');
|
|
80
|
+
console.log(content); // 'Hello, FSD!'
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### 目录操作
|
|
84
|
+
|
|
85
|
+
```typescript
|
|
86
|
+
// 创建目录(注意:目录路径必须以 / 结尾)
|
|
87
|
+
const dir = fsd('/photos/');
|
|
88
|
+
await dir.mkdir(true); // 递归创建
|
|
89
|
+
|
|
90
|
+
// 列出目录内容
|
|
91
|
+
const files = await dir.readdir();
|
|
92
|
+
for (const f of files) {
|
|
93
|
+
console.log(f.name); // 文件名
|
|
94
|
+
console.log(f.path); // 完整路径
|
|
95
|
+
}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## 路径约定(重要)
|
|
99
|
+
|
|
100
|
+
FSD 对文件和目录路径有严格的约定,违反会抛出错误:
|
|
101
|
+
|
|
102
|
+
### 文件路径
|
|
103
|
+
|
|
104
|
+
- **必须** 不以 `/` 结尾
|
|
105
|
+
- 自动补全前导 `/`
|
|
106
|
+
|
|
107
|
+
```typescript
|
|
108
|
+
// ✅ 正确
|
|
109
|
+
const file = fsd('/test.txt'); // 正确
|
|
110
|
+
const file = fsd('test.txt'); // 自动补全为 '/test.txt'
|
|
111
|
+
|
|
112
|
+
// ❌ 错误
|
|
113
|
+
const file = fsd('/test.txt/'); // Error: file path should not ends with /
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### 目录路径
|
|
117
|
+
|
|
118
|
+
- **必须** 以 `/` 结尾
|
|
119
|
+
- 自动补全前导 `/`
|
|
120
|
+
|
|
121
|
+
```typescript
|
|
122
|
+
// ✅ 正确
|
|
123
|
+
const dir = fsd('/uploads/'); // 正确
|
|
124
|
+
const dir = fsd('uploads'); // 错误:必须以 / 结尾
|
|
125
|
+
|
|
126
|
+
// ❌ 错误
|
|
127
|
+
const dir = fsd('/uploads'); // Error: directory path should be ends with /
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## 使用不同适配器
|
|
131
|
+
|
|
132
|
+
### 本地文件系统 (fsd-fs)
|
|
133
|
+
|
|
134
|
+
```typescript
|
|
135
|
+
import FSD from 'fsd';
|
|
136
|
+
import FSAdapter from 'fsd-fs';
|
|
137
|
+
|
|
138
|
+
const fsd = FSD({
|
|
139
|
+
adapter: new FSAdapter({
|
|
140
|
+
root: '/app/uploads', // 必需:存储根目录
|
|
141
|
+
mode: 0o644, // 可选:文件权限,默认 0o644
|
|
142
|
+
tmpdir: '/tmp/fsd-tmp', // 可选:临时目录(分段上传时使用)
|
|
143
|
+
urlPrefix: 'https://cdn.example.com' // 可选:URL 前缀
|
|
144
|
+
})
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
const file = fsd('/test.txt');
|
|
148
|
+
await file.write('Local file content');
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### 阿里云 OSS (fsd-oss)
|
|
152
|
+
|
|
153
|
+
```typescript
|
|
154
|
+
import FSD from 'fsd';
|
|
155
|
+
import OSSAdapter from 'fsd-oss';
|
|
156
|
+
|
|
157
|
+
const fsd = FSD({
|
|
158
|
+
adapter: new OSSAdapter({
|
|
159
|
+
accessKeyId: 'your-access-key-id', // 必需
|
|
160
|
+
accessKeySecret: 'your-access-key-secret', // 必需
|
|
161
|
+
region: 'oss-cn-hangzhou', // 必需:区域
|
|
162
|
+
bucket: 'your-bucket-name', // 可选:Bucket 名称
|
|
163
|
+
root: '/uploads', // 可选:存储根路径
|
|
164
|
+
urlPrefix: 'https://cdn.example.com', // 可选:URL 前缀
|
|
165
|
+
publicRead: true, // 可选:公共读
|
|
166
|
+
internal: false, // 可选:内网访问
|
|
167
|
+
secure: true // 可选:HTTPS
|
|
168
|
+
})
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
const file = fsd('/uploads/test.txt');
|
|
172
|
+
await file.write('OSS file content');
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
**⚠️ 注意**: `endpoint` 选项已废弃,请使用 `region/[internal]/[secure]`。
|
|
176
|
+
|
|
177
|
+
### 阿里云 VOD (fsd-vod)
|
|
178
|
+
|
|
179
|
+
```typescript
|
|
180
|
+
import FSD from 'fsd';
|
|
181
|
+
import VODAdapter from 'fsd-vod';
|
|
182
|
+
|
|
183
|
+
const fsd = FSD({
|
|
184
|
+
adapter: new VODAdapter({
|
|
185
|
+
accessKeyId: 'your-access-key-id', // 必需
|
|
186
|
+
accessKeySecret: 'your-access-key-secret', // 必需
|
|
187
|
+
region: 'cn-shanghai', // 可选:默认 cn-shanghai
|
|
188
|
+
privateKey: 'your-rsa-private-key', // 必需:视频上传签名私钥
|
|
189
|
+
templateGroupId: 'your-template-group-id', // 可选:转码模板组
|
|
190
|
+
workflowId: 'your-workflow-id', // 可选:工作流 ID
|
|
191
|
+
urlPrefix: 'https://cdn.example.com' // 可选:URL 前缀
|
|
192
|
+
})
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
// VOD 必须先分配视频 ID
|
|
196
|
+
const videoId = await fsd.adapter.alloc({ name: 'video.mp4' });
|
|
197
|
+
const file = fsd(videoId);
|
|
198
|
+
|
|
199
|
+
await file.write(videoBuffer);
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
## API 参考
|
|
203
|
+
|
|
204
|
+
### FSDFile 对象属性
|
|
205
|
+
|
|
206
|
+
```typescript
|
|
207
|
+
const file = fsd('/path/to/document.pdf');
|
|
208
|
+
|
|
209
|
+
console.log(file.path); // '/path/to/document.pdf' - 完整路径
|
|
210
|
+
console.log(file.dir); // '/path/to/' - 目录路径
|
|
211
|
+
console.log(file.base); // 'document.pdf' - 文件名(含扩展名)
|
|
212
|
+
console.log(file.name); // 'document' - 文件名(不含扩展名)
|
|
213
|
+
console.log(file.ext); // '.pdf' - 扩展名
|
|
214
|
+
console.log(file.needEnsureDir); // false/true - 是否需要确保目录存在
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
### 文件操作
|
|
218
|
+
|
|
219
|
+
#### write(data) - 写入文件
|
|
220
|
+
|
|
221
|
+
```typescript
|
|
222
|
+
// 写入字符串
|
|
223
|
+
await file.write('Hello, World!');
|
|
224
|
+
|
|
225
|
+
// 写入 Buffer
|
|
226
|
+
await file.write(Buffer.from([0x48, 0x65, 0x6c, 0x6c, 0x6f]));
|
|
227
|
+
|
|
228
|
+
// 写入流
|
|
229
|
+
const readStream = fs.createReadStream('/local/file.txt');
|
|
230
|
+
await file.write(readStream);
|
|
231
|
+
|
|
232
|
+
// 创建空文件
|
|
233
|
+
await file.write();
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
#### read([encoding] | [position, length] | [position, length, encoding]) - 读取文件
|
|
237
|
+
|
|
238
|
+
```typescript
|
|
239
|
+
// 读取为 UTF-8 字符串
|
|
240
|
+
const text = await file.read('utf8');
|
|
241
|
+
|
|
242
|
+
// 读取为 Buffer
|
|
243
|
+
const buffer = await file.read();
|
|
244
|
+
|
|
245
|
+
// 读取指定范围(前 100 字节)
|
|
246
|
+
const partial = await file.read(0, 100);
|
|
247
|
+
|
|
248
|
+
// 读取指定范围并解码
|
|
249
|
+
const text = await file.read(100, 50, 'utf8');
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
#### append(data) - 追加内容
|
|
253
|
+
|
|
254
|
+
```typescript
|
|
255
|
+
// 追加字符串
|
|
256
|
+
await file.append('\nNew line');
|
|
257
|
+
|
|
258
|
+
// 追加 Buffer
|
|
259
|
+
await file.append(Buffer.from('additional data'));
|
|
260
|
+
|
|
261
|
+
// 追加流
|
|
262
|
+
const sourceStream = fs.createReadStream('/append.txt');
|
|
263
|
+
await file.append(sourceStream);
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
### 流式操作
|
|
267
|
+
|
|
268
|
+
#### createReadStream([options]) - 创建可读流
|
|
269
|
+
|
|
270
|
+
```typescript
|
|
271
|
+
// 创建完整文件流
|
|
272
|
+
const readStream = await file.createReadStream();
|
|
273
|
+
readStream.pipe(process.stdout);
|
|
274
|
+
|
|
275
|
+
// 指定范围读取(从第 100 字节到 199 字节)
|
|
276
|
+
const partialStream = await file.createReadStream({
|
|
277
|
+
start: 100,
|
|
278
|
+
end: 199
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
// 流式下载到本地
|
|
282
|
+
const fs = require('fs');
|
|
283
|
+
const readStream = await file.createReadStream();
|
|
284
|
+
const writeStream = fs.createWriteStream('/local/download.txt');
|
|
285
|
+
readStream.pipe(writeStream);
|
|
286
|
+
await new Promise(resolve => writeStream.on('finish', resolve));
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
#### createWriteStream([options]) - 创建可写流
|
|
290
|
+
|
|
291
|
+
```typescript
|
|
292
|
+
// 创建可写流
|
|
293
|
+
const writeStream = await file.createWriteStream();
|
|
294
|
+
writeStream.write('Hello');
|
|
295
|
+
writeStream.end();
|
|
296
|
+
|
|
297
|
+
// 等待流完成(如果适配器支持 promise 属性)
|
|
298
|
+
if (writeStream.promise) {
|
|
299
|
+
await writeStream.promise;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// 从本地文件流式上传
|
|
303
|
+
const fs = require('fs');
|
|
304
|
+
const readStream = fs.createReadStream('/local/file.txt');
|
|
305
|
+
const writeStream = await file.createWriteStream();
|
|
306
|
+
readStream.pipe(writeStream);
|
|
307
|
+
if (writeStream.promise) {
|
|
308
|
+
await writeStream.promise;
|
|
309
|
+
}
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
### 目录操作
|
|
313
|
+
|
|
314
|
+
#### mkdir([recursive]) - 创建目录
|
|
315
|
+
|
|
316
|
+
```typescript
|
|
317
|
+
// 创建单级目录(父目录必须存在)
|
|
318
|
+
await dir.mkdir();
|
|
319
|
+
|
|
320
|
+
// 递归创建多级目录
|
|
321
|
+
await dir.mkdir(true); // 等价于 mkdir -p
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
#### readdir([recursion]) - 读取目录
|
|
325
|
+
|
|
326
|
+
```typescript
|
|
327
|
+
// 列出直接子项
|
|
328
|
+
const files = await dir.readdir();
|
|
329
|
+
for (const f of files) {
|
|
330
|
+
console.log(f.name);
|
|
331
|
+
if (await f.isFile()) {
|
|
332
|
+
console.log(`File: ${f.path}`);
|
|
333
|
+
} else if (await f.isDirectory()) {
|
|
334
|
+
console.log(`Directory: ${f.path}`);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// 递归列出所有文件
|
|
339
|
+
const allFiles = await dir.readdir(true);
|
|
340
|
+
console.log(`Total: ${allFiles.length} files`);
|
|
341
|
+
|
|
342
|
+
// 使用 glob 模式筛选
|
|
343
|
+
const images = await dir.readdir('**/*.jpg');
|
|
344
|
+
for (const img of images) {
|
|
345
|
+
console.log(img.path);
|
|
346
|
+
}
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
### 文件信息
|
|
350
|
+
|
|
351
|
+
#### exists() - 判断文件/目录是否存在
|
|
352
|
+
|
|
353
|
+
```typescript
|
|
354
|
+
if (await file.exists()) {
|
|
355
|
+
console.log('File exists');
|
|
356
|
+
} else {
|
|
357
|
+
await file.write('Create this file');
|
|
358
|
+
}
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
#### isFile() / isDirectory() - 判断类型
|
|
362
|
+
|
|
363
|
+
```typescript
|
|
364
|
+
if (await file.isFile()) {
|
|
365
|
+
console.log('This is a file');
|
|
366
|
+
console.log(`Size: ${await file.size()} bytes`);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
if (await dir.isDirectory()) {
|
|
370
|
+
console.log('This is a directory');
|
|
371
|
+
}
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
#### size() - 获取文件大小
|
|
375
|
+
|
|
376
|
+
```typescript
|
|
377
|
+
const size = await file.size();
|
|
378
|
+
console.log(`File size: ${size} bytes`);
|
|
379
|
+
|
|
380
|
+
// 格式化显示
|
|
381
|
+
const sizeMB = size / (1024 * 1024);
|
|
382
|
+
console.log(`File size: ${sizeMB.toFixed(2)} MB`);
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
#### lastModified() - 获取最后修改时间
|
|
386
|
+
|
|
387
|
+
```typescript
|
|
388
|
+
const modified = await file.lastModified();
|
|
389
|
+
console.log('Last modified:', modified.toISOString());
|
|
390
|
+
|
|
391
|
+
// 判断文件是否过期(例如 1 天前)
|
|
392
|
+
const oneDayAgo = new Date(Date.now() - 24 * 60 * 60 * 1000);
|
|
393
|
+
if (modified < oneDayAgo) {
|
|
394
|
+
console.log('File is older than 1 day');
|
|
395
|
+
}
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
### 文件管理
|
|
399
|
+
|
|
400
|
+
#### copy(dest) - 复制文件
|
|
401
|
+
|
|
402
|
+
```typescript
|
|
403
|
+
// 复制到同级目录
|
|
404
|
+
const copy = await file.copy('file_copy.txt');
|
|
405
|
+
|
|
406
|
+
// 复制到子目录
|
|
407
|
+
const copy = await file.copy('backup/file.txt');
|
|
408
|
+
|
|
409
|
+
// 复制到绝对路径
|
|
410
|
+
const copy = await file.copy('/other/path/file.txt');
|
|
411
|
+
|
|
412
|
+
// 复制目录(递归)
|
|
413
|
+
const dirCopy = await dir.copy('backup/');
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
#### rename(dest) - 重命名/移动
|
|
417
|
+
|
|
418
|
+
```typescript
|
|
419
|
+
// 重命名文件
|
|
420
|
+
await file.rename('new_name.txt');
|
|
421
|
+
|
|
422
|
+
// 移动文件到子目录
|
|
423
|
+
await file.rename('backup/file.txt');
|
|
424
|
+
|
|
425
|
+
// 移动文件到绝对路径
|
|
426
|
+
await file.rename('/other/path/file.txt');
|
|
427
|
+
|
|
428
|
+
// 重命名目录
|
|
429
|
+
await dir.rename('new_folder/');
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
#### unlink() - 删除文件/目录
|
|
433
|
+
|
|
434
|
+
```typescript
|
|
435
|
+
// 删除文件
|
|
436
|
+
await file.unlink();
|
|
437
|
+
|
|
438
|
+
// 删除目录及其所有内容
|
|
439
|
+
await dir.unlink();
|
|
440
|
+
```
|
|
441
|
+
|
|
442
|
+
### URL 生成
|
|
443
|
+
|
|
444
|
+
#### createUrl([options]) - 创建访问 URL
|
|
445
|
+
|
|
446
|
+
```typescript
|
|
447
|
+
// 创建默认链接(1 小时后过期)
|
|
448
|
+
const url = await file.createUrl();
|
|
449
|
+
console.log('Download URL:', url);
|
|
450
|
+
|
|
451
|
+
// 创建 10 分钟后过期的链接
|
|
452
|
+
const url = await file.createUrl({ expires: 600 });
|
|
453
|
+
|
|
454
|
+
// 创建带下载提示的链接
|
|
455
|
+
const url = await file.createUrl({
|
|
456
|
+
expires: 3600,
|
|
457
|
+
response: {
|
|
458
|
+
'content-type': 'application/pdf',
|
|
459
|
+
'content-disposition': 'attachment; filename="document.pdf"'
|
|
460
|
+
}
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
// VOD 适配器:获取不同清晰度的视频播放地址
|
|
464
|
+
const hdUrl = await file.createUrl({ path: '/video/HD' });
|
|
465
|
+
const sdUrl = await file.createUrl({ path: '/video/SD' });
|
|
466
|
+
```
|
|
467
|
+
|
|
468
|
+
### 分片上传(大文件)
|
|
469
|
+
|
|
470
|
+
#### initMultipartUpload(partCount) - 初始化分片上传
|
|
471
|
+
|
|
472
|
+
```typescript
|
|
473
|
+
// 初始化 3 个分片
|
|
474
|
+
const tasks = await file.initMultipartUpload(3);
|
|
475
|
+
console.log(tasks);
|
|
476
|
+
// ['task://uploadId123?1', 'task://uploadId123?2', 'task://uploadId123?3']
|
|
477
|
+
```
|
|
478
|
+
|
|
479
|
+
#### writePart(partTask, data, [size]) - 上传分片
|
|
480
|
+
|
|
481
|
+
```typescript
|
|
482
|
+
const tasks = await file.initMultipartUpload(3);
|
|
483
|
+
const parts = [];
|
|
484
|
+
|
|
485
|
+
// 上传第一个分片(Buffer)
|
|
486
|
+
const part1 = await file.writePart(tasks[0], Buffer.from('part1 data'));
|
|
487
|
+
parts.push(part1);
|
|
488
|
+
|
|
489
|
+
// 上传第二个分片(流)
|
|
490
|
+
const stream = fs.createReadStream('/tmp/part2');
|
|
491
|
+
const part2 = await file.writePart(tasks[1], stream, 1024);
|
|
492
|
+
parts.push(part2);
|
|
493
|
+
|
|
494
|
+
// 上传第三个分片(字符串)
|
|
495
|
+
const part3 = await file.writePart(tasks[2], 'part3 data');
|
|
496
|
+
parts.push(part3);
|
|
497
|
+
```
|
|
498
|
+
|
|
499
|
+
#### completeMultipartUpload(parts) - 完成分片上传
|
|
500
|
+
|
|
501
|
+
```typescript
|
|
502
|
+
const tasks = await file.initMultipartUpload(3);
|
|
503
|
+
const parts = [];
|
|
504
|
+
|
|
505
|
+
for (let i = 0; i < tasks.length; i++) {
|
|
506
|
+
const part = await file.writePart(tasks[i], getDataForPart(i));
|
|
507
|
+
parts.push(part);
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// 完成上传(注意:parts 顺序可以与 tasks 顺序不同)
|
|
511
|
+
await file.completeMultipartUpload(parts);
|
|
512
|
+
```
|
|
513
|
+
|
|
514
|
+
### 完整示例:文件上传服务
|
|
515
|
+
|
|
516
|
+
```typescript
|
|
517
|
+
import FSD from 'fsd';
|
|
518
|
+
import FSAdapter from 'fsd-fs';
|
|
519
|
+
import express from 'express';
|
|
520
|
+
|
|
521
|
+
const app = express();
|
|
522
|
+
const fsd = FSD({
|
|
523
|
+
adapter: new FSAdapter({ root: '/uploads' })
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
// 单文件上传
|
|
527
|
+
app.post('/upload', async (req, res) => {
|
|
528
|
+
const file = req.files.file;
|
|
529
|
+
const destFile = fsd(`/uploads/${file.name}`);
|
|
530
|
+
|
|
531
|
+
// 使用流式上传
|
|
532
|
+
await destFile.write(file.data);
|
|
533
|
+
|
|
534
|
+
// 生成下载链接(24 小时后过期)
|
|
535
|
+
const downloadUrl = await destFile.createUrl({ expires: 86400 });
|
|
536
|
+
|
|
537
|
+
res.json({
|
|
538
|
+
path: destFile.path,
|
|
539
|
+
size: await destFile.size(),
|
|
540
|
+
downloadUrl
|
|
541
|
+
});
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
// 分片上传大文件
|
|
545
|
+
app.post('/upload/multipart', async (req, res) => {
|
|
546
|
+
const { fileName, partCount } = req.body;
|
|
547
|
+
const file = fsd(`/uploads/${fileName}`);
|
|
548
|
+
|
|
549
|
+
// 初始化分片上传
|
|
550
|
+
const tasks = await file.initMultipartUpload(partCount);
|
|
551
|
+
res.json({ tasks });
|
|
552
|
+
});
|
|
553
|
+
|
|
554
|
+
app.post('/upload/part', async (req, res) => {
|
|
555
|
+
const { task } = req.body;
|
|
556
|
+
const file = fsd('/uploads/target.txt');
|
|
557
|
+
|
|
558
|
+
// 上传分片
|
|
559
|
+
const part = await file.writePart(task, req.body.data, req.body.size);
|
|
560
|
+
res.json({ part });
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
app.post('/upload/complete', async (req, res) => {
|
|
564
|
+
const { fileName, parts } = req.body;
|
|
565
|
+
const file = fsd(`/uploads/${fileName}`);
|
|
566
|
+
|
|
567
|
+
// 完成上传
|
|
568
|
+
await file.completeMultipartUpload(parts);
|
|
569
|
+
|
|
570
|
+
res.json({ success: true });
|
|
571
|
+
});
|
|
572
|
+
|
|
573
|
+
// 文件列表
|
|
574
|
+
app.get('/files/:path', async (req, res) => {
|
|
575
|
+
const dir = fsd(`/${req.params.path}/`);
|
|
576
|
+
const files = await dir.readdir(true); // 递归列出
|
|
577
|
+
|
|
578
|
+
const fileList = await Promise.all(files.map(async f => ({
|
|
579
|
+
name: f.name,
|
|
580
|
+
path: f.path,
|
|
581
|
+
size: await f.size(),
|
|
582
|
+
isFile: await f.isFile()
|
|
583
|
+
})));
|
|
584
|
+
|
|
585
|
+
res.json(fileList);
|
|
586
|
+
});
|
|
587
|
+
|
|
588
|
+
app.listen(3000);
|
|
589
|
+
```
|
|
590
|
+
|
|
591
|
+
### 使用 OSS 实现云存储
|
|
592
|
+
|
|
593
|
+
```typescript
|
|
594
|
+
import FSD from 'fsd';
|
|
595
|
+
import OSSAdapter from 'fsd-oss';
|
|
596
|
+
|
|
597
|
+
const fsd = FSD({
|
|
598
|
+
adapter: new OSSAdapter({
|
|
599
|
+
accessKeyId: process.env.OSS_ACCESS_KEY_ID,
|
|
600
|
+
accessKeySecret: process.env.OSS_ACCESS_KEY_SECRET,
|
|
601
|
+
region: 'oss-cn-hangzhou',
|
|
602
|
+
bucket: 'my-bucket',
|
|
603
|
+
root: '/uploads'
|
|
604
|
+
})
|
|
605
|
+
});
|
|
606
|
+
|
|
607
|
+
// 上传文件并获取访问 URL
|
|
608
|
+
async function uploadFile(filename, data) {
|
|
609
|
+
const file = fsd(`/uploads/${filename}`);
|
|
610
|
+
await file.write(data);
|
|
611
|
+
|
|
612
|
+
const url = await file.createUrl({ expires: 3600 });
|
|
613
|
+
return { path: file.path, url };
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
// 批量上传
|
|
617
|
+
async function uploadMultiple(files) {
|
|
618
|
+
const results = await Promise.all(
|
|
619
|
+
files.map(f => uploadFile(f.name, f.data))
|
|
620
|
+
);
|
|
621
|
+
return results;
|
|
622
|
+
}
|
|
623
|
+
```
|
|
624
|
+
|
|
625
|
+
### 自定义适配器
|
|
626
|
+
|
|
627
|
+
你可以创建自己的存储适配器:
|
|
628
|
+
|
|
629
|
+
```typescript
|
|
630
|
+
import { Adapter } from 'fsd';
|
|
631
|
+
|
|
632
|
+
class MyCustomAdapter extends Adapter<MyOptions> {
|
|
633
|
+
readonly instanceOfFSDAdapter = true;
|
|
634
|
+
readonly name = 'MyCustomAdapter';
|
|
635
|
+
readonly needEnsureDir = false;
|
|
636
|
+
|
|
637
|
+
constructor(options: MyOptions) {
|
|
638
|
+
super(options);
|
|
639
|
+
// 初始化你的存储系统
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
async write(path: string, data: any): Promise<void> {
|
|
643
|
+
// 实现写入逻辑
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
async read(path: string, options?: any): Promise<any> {
|
|
647
|
+
// 实现读取逻辑
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
// ... 实现其他必需方法
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
// 使用自定义适配器
|
|
654
|
+
const fsd = FSD({
|
|
655
|
+
adapter: new MyCustomAdapter({ /* options */ })
|
|
656
|
+
});
|
|
657
|
+
```
|
|
658
|
+
|
|
659
|
+
## TypeScript 支持
|
|
660
|
+
|
|
661
|
+
完整的 TypeScript 类型定义已包含在包中:
|
|
662
|
+
|
|
663
|
+
```typescript
|
|
664
|
+
import FSD, { FSDFile, Adapter, ReadStreamOptions } from 'fsd';
|
|
665
|
+
|
|
666
|
+
const fsd: ReturnType<typeof FSD> = FSD({
|
|
667
|
+
adapter: myAdapter
|
|
668
|
+
});
|
|
669
|
+
|
|
670
|
+
const file: FSDFile = fsd('/test.txt');
|
|
671
|
+
```
|
|
672
|
+
|
|
673
|
+
## 常见问题
|
|
674
|
+
|
|
675
|
+
### Q: 如何切换存储后端?
|
|
676
|
+
|
|
677
|
+
A: 只需更换适配器,应用代码无需修改:
|
|
678
|
+
|
|
679
|
+
```typescript
|
|
680
|
+
// 开发环境使用本地文件系统
|
|
681
|
+
const devFsd = FSD({
|
|
682
|
+
adapter: new FSAdapter({ root: '/uploads' })
|
|
683
|
+
});
|
|
684
|
+
|
|
685
|
+
// 生产环境使用 OSS
|
|
686
|
+
const prodFsd = FSD({
|
|
687
|
+
adapter: new OSSAdapter({
|
|
688
|
+
accessKeyId: process.env.OSS_ACCESS_KEY_ID,
|
|
689
|
+
accessKeySecret: process.env.OSS_ACCESS_KEY_SECRET,
|
|
690
|
+
region: process.env.OSS_REGION,
|
|
691
|
+
bucket: process.env.OSS_BUCKET
|
|
692
|
+
})
|
|
693
|
+
});
|
|
694
|
+
|
|
695
|
+
// 使用方式完全相同
|
|
696
|
+
await devFsd('/test.txt').write('Hello');
|
|
697
|
+
await prodFsd('/test.txt').write('Hello');
|
|
698
|
+
```
|
|
699
|
+
|
|
700
|
+
### Q: 如何处理大文件?
|
|
701
|
+
|
|
702
|
+
A: 使用流式操作或分片上传:
|
|
703
|
+
|
|
704
|
+
```typescript
|
|
705
|
+
// 流式上传(适合中等大小文件)
|
|
706
|
+
const stream = await file.createWriteStream();
|
|
707
|
+
sourceStream.pipe(stream);
|
|
708
|
+
await stream.promise;
|
|
709
|
+
|
|
710
|
+
// 分片上传(适合大文件)
|
|
711
|
+
const tasks = await file.initMultipartUpload(5);
|
|
712
|
+
// ... 上传分片
|
|
713
|
+
await file.completeMultipartUpload(parts);
|
|
714
|
+
```
|
|
715
|
+
|
|
716
|
+
### Q: 如何检查适配器类型?
|
|
717
|
+
|
|
718
|
+
A: 使用 `adapter.name` 属性:
|
|
719
|
+
|
|
720
|
+
```typescript
|
|
721
|
+
console.log(fsd.adapter.name); // 'FSAdapter', 'OSSAdapter', 'VODAdapter'
|
|
722
|
+
|
|
723
|
+
if (fsd.adapter.name === 'OSSAdapter') {
|
|
724
|
+
console.log('Using OSS storage');
|
|
725
|
+
}
|
|
726
|
+
```
|
|
727
|
+
|
|
728
|
+
### Q: 目录操作失败怎么办?
|
|
729
|
+
|
|
730
|
+
A: 确保目录路径以 `/` 结尾,或使用递归创建:
|
|
731
|
+
|
|
732
|
+
```typescript
|
|
733
|
+
// ❌ 错误
|
|
734
|
+
await fsd('/uploads').mkdir(); // Error
|
|
735
|
+
|
|
736
|
+
// ✅ 正确
|
|
737
|
+
await fsd('/uploads/').mkdir(); // Success
|
|
738
|
+
|
|
739
|
+
// ✅ 或递归创建
|
|
740
|
+
await fsd('/uploads/subdir/').mkdir(true); // Success
|
|
741
|
+
```
|
|
742
|
+
|
|
743
|
+
## 相关包
|
|
744
|
+
|
|
745
|
+
- [fsd-fs](https://www.npmjs.com/package/fsd-fs) - 本地文件系统适配器
|
|
746
|
+
- [fsd-oss](https://www.npmjs.com/package/fsd-oss) - 阿里云 OSS 适配器
|
|
747
|
+
- [fsd-vod](https://www.npmjs.com/package/fsd-vod) - 阿里云 VOD 适配器
|
|
748
|
+
|
|
749
|
+
## License
|
|
750
|
+
|
|
751
|
+
MIT
|