secure-upload-fastify-sdk 1.0.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/README.md +467 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2069 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +2023 -0
- package/dist/index.mjs.map +1 -0
- package/dist/server/adapters/secure-crypto.d.ts +28 -0
- package/dist/server/adapters/secure-crypto.d.ts.map +1 -0
- package/dist/server/errors.d.ts +48 -0
- package/dist/server/errors.d.ts.map +1 -0
- package/dist/server/errors.js +112 -0
- package/dist/server/errors.js.map +1 -0
- package/dist/server/errors.mjs +85 -0
- package/dist/server/errors.mjs.map +1 -0
- package/dist/server/fastify-plugin.d.ts +38 -0
- package/dist/server/fastify-plugin.d.ts.map +1 -0
- package/dist/server/index.d.ts +17 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +2070 -0
- package/dist/server/index.js.map +1 -0
- package/dist/server/index.mjs +2024 -0
- package/dist/server/index.mjs.map +1 -0
- package/dist/server/routes/_internal.d.ts +42 -0
- package/dist/server/routes/_internal.d.ts.map +1 -0
- package/dist/server/routes/multipart.d.ts +22 -0
- package/dist/server/routes/multipart.d.ts.map +1 -0
- package/dist/server/routes/smart.d.ts +16 -0
- package/dist/server/routes/smart.d.ts.map +1 -0
- package/dist/server/session.d.ts +19 -0
- package/dist/server/session.d.ts.map +1 -0
- package/dist/server/types.d.ts +233 -0
- package/dist/server/types.d.ts.map +1 -0
- package/dist/server/utils/cleaner.d.ts +20 -0
- package/dist/server/utils/cleaner.d.ts.map +1 -0
- package/dist/server/utils/memory-rate-limit.d.ts +29 -0
- package/dist/server/utils/memory-rate-limit.d.ts.map +1 -0
- package/dist/server/utils/orphan-scanner.d.ts +34 -0
- package/dist/server/utils/orphan-scanner.d.ts.map +1 -0
- package/dist/server/utils/redis-helpers.d.ts +25 -0
- package/dist/server/utils/redis-helpers.d.ts.map +1 -0
- package/dist/server/utils/repository/index.d.ts +27 -0
- package/dist/server/utils/repository/index.d.ts.map +1 -0
- package/dist/server/utils/repository/index.js +501 -0
- package/dist/server/utils/repository/index.js.map +1 -0
- package/dist/server/utils/repository/index.mjs +469 -0
- package/dist/server/utils/repository/index.mjs.map +1 -0
- package/dist/server/utils/repository/memory.d.ts +33 -0
- package/dist/server/utils/repository/memory.d.ts.map +1 -0
- package/dist/server/utils/repository/prisma.d.ts +49 -0
- package/dist/server/utils/repository/prisma.d.ts.map +1 -0
- package/dist/server/utils/repository/types-extra.d.ts +9 -0
- package/dist/server/utils/repository/types-extra.d.ts.map +1 -0
- package/dist/server/utils/repository/types.d.ts +128 -0
- package/dist/server/utils/repository/types.d.ts.map +1 -0
- package/dist/server/utils/storage/index.d.ts +115 -0
- package/dist/server/utils/storage/index.d.ts.map +1 -0
- package/dist/server/utils/storage/index.js +285 -0
- package/dist/server/utils/storage/index.js.map +1 -0
- package/dist/server/utils/storage/index.mjs +247 -0
- package/dist/server/utils/storage/index.mjs.map +1 -0
- package/dist/server/utils/storage/local.d.ts +46 -0
- package/dist/server/utils/storage/local.d.ts.map +1 -0
- package/dist/server/utils/storage/memory.d.ts +34 -0
- package/dist/server/utils/storage/memory.d.ts.map +1 -0
- package/dist/server/utils/storage/types.d.ts +78 -0
- package/dist/server/utils/storage/types.d.ts.map +1 -0
- package/dist/server/utils/strategy.d.ts +64 -0
- package/dist/server/utils/strategy.d.ts.map +1 -0
- package/dist/server/utils/strategy.js +130 -0
- package/dist/server/utils/strategy.js.map +1 -0
- package/dist/server/utils/strategy.mjs +103 -0
- package/dist/server/utils/strategy.mjs.map +1 -0
- package/package.json +97 -0
package/README.md
ADDED
|
@@ -0,0 +1,467 @@
|
|
|
1
|
+
# secure-upload-fastify-sdk
|
|
2
|
+
|
|
3
|
+
> 面向 Fastify 的可插拔上传 SDK:分片上传 / 秒传 / 断点续传 / 智能分流一站式解决方案。
|
|
4
|
+
> 存储可插拔(Local / S3 / OSS / COS),数据层可插拔(Prisma 或自实现),可选桥接 `secure-crypto-top-zod-sdk`。
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## ✨ 特性
|
|
9
|
+
|
|
10
|
+
- **5 + 1 端点**,覆盖所有上传场景
|
|
11
|
+
- `POST /multipart/{init, part, complete, abort, status}` — 通用分片上传
|
|
12
|
+
- `POST /smart/init` — 智能批量 init(自动按 `totalSize` 选 `ONE_SHOT` / `MULTIPART`)
|
|
13
|
+
- **存储可插拔** — 实现一次 `StorageAdapter` 接口,就能在 Local / S3 / OSS / COS 之间切换
|
|
14
|
+
- **数据层可插拔** — 内置 Prisma 实现,也支持 Drizzle / TypeORM / Mongo / 内存自实现
|
|
15
|
+
- **可选 Redis** — 分布式限流 + 分布式锁;不传则自动降级为内存版(单进程)
|
|
16
|
+
- **可选 Secure-Crypto 桥接** — 与 `secure-crypto-top-zod-sdk` 配对,从 `req.secureRequest` 读数据;纯 HTTP 场景用 `raw` 模式
|
|
17
|
+
- **可自定义 Session 校验** — 默认 base64-decode JWT payload,可注入 `jwt.verify()` 完整验签
|
|
18
|
+
- **后台任务** — 过期 `uploadId` 清理 + 孤儿文件扫描,全部可关闭
|
|
19
|
+
- **严格秒传隔离(v1.0.0+)** — 按 `(contentHash, isPublic, userId)` 三元组匹配,公开池/私有池/不同用户之间完全隔离
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## 📦 安装
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
pnpm add secure-upload-fastify-sdk
|
|
27
|
+
# 必装 peer
|
|
28
|
+
pnpm add fastify fastify-plugin zod
|
|
29
|
+
# 按需装
|
|
30
|
+
pnpm add @prisma/client # 用 Prisma 数据层时
|
|
31
|
+
pnpm add ioredis # 用分布式限流 / 锁时
|
|
32
|
+
# 可选:与加密 SDK 桥接
|
|
33
|
+
pnpm add secure-crypto-top-zod-sdk
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
> `secure-crypto-top-zod-sdk` / `@prisma/client` / `ioredis` 在 `peerDependenciesMeta.optional` 中标记为可选,SDK 在不安装这些包时仍可独立运行(用 `raw` 模式 + 内存版限流)。
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## 🚀 快速开始
|
|
41
|
+
|
|
42
|
+
### 1. 接入 Prisma Schema(仅当用 Prisma 数据层时)
|
|
43
|
+
|
|
44
|
+
把 [`prisma/schema.prisma`](./prisma/schema.prisma) 中的两张表(`File` / `MultipartUpload`)以及两个枚举(`FileStatus` / `MultipartStatus`)复制到你自己项目的 `prisma/schema.prisma`,然后:
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
npx prisma db push
|
|
48
|
+
npx prisma generate
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
> 如果你的项目里已经有 `File` / `MultipartUpload`,按 SDK 的字段对一下即可,不需要新建表。
|
|
52
|
+
|
|
53
|
+
### 2. 注册插件
|
|
54
|
+
|
|
55
|
+
```ts
|
|
56
|
+
import Fastify from 'fastify';
|
|
57
|
+
import { PrismaClient } from '@prisma/client';
|
|
58
|
+
import Redis from 'ioredis';
|
|
59
|
+
import secureUploadPlugin, {
|
|
60
|
+
type SecureUploadOptions,
|
|
61
|
+
} from 'secure-upload-fastify-sdk/server';
|
|
62
|
+
|
|
63
|
+
const app = Fastify({ logger: true });
|
|
64
|
+
const prisma = new PrismaClient();
|
|
65
|
+
const redis = new Redis(process.env.REDIS_URL!);
|
|
66
|
+
|
|
67
|
+
await app.register(secureUploadPlugin, {
|
|
68
|
+
// 1. 存储:本地磁盘(默认)
|
|
69
|
+
storage: { adapter: 'local', options: { baseDir: '/var/upload' } },
|
|
70
|
+
|
|
71
|
+
// 2. 数据层:Prisma(也可传 'custom' 注入自己的实现)
|
|
72
|
+
repository: { kind: 'prisma', prisma },
|
|
73
|
+
|
|
74
|
+
// 3. Redis(可选,不传则用内存版限流 + 锁)
|
|
75
|
+
redis,
|
|
76
|
+
|
|
77
|
+
// 4. 路径前缀(默认 '/multipart' '/smart',挂到 /api/v1/upload 时设这个)
|
|
78
|
+
pathPrefix: {
|
|
79
|
+
multipart: '/api/v1/upload/multipart',
|
|
80
|
+
smart: '/api/v1/upload/smart',
|
|
81
|
+
},
|
|
82
|
+
// complete 响应里的 url 字段前缀
|
|
83
|
+
fileUrlPrefix: '/api/v1/upload/files',
|
|
84
|
+
|
|
85
|
+
// 5. 可选:与 secure-crypto-top-zod-sdk 桥接
|
|
86
|
+
secure: { mode: 'secure-crypto' },
|
|
87
|
+
|
|
88
|
+
// 6. 可选:策略、限流、TTL(都有默认值,按需覆盖)
|
|
89
|
+
// strategy: { oneShotMax: 1024 * 1024, defaultPartSize: 1024 * 1024, ... },
|
|
90
|
+
// rateLimit: { initPerMin: 30, partPerMin: 600 },
|
|
91
|
+
// ttlSeconds: 24 * 60 * 60,
|
|
92
|
+
} satisfies SecureUploadOptions);
|
|
93
|
+
|
|
94
|
+
await app.listen({ port: 8083 });
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### 3. 验证
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
# 1) init 一笔分片上传
|
|
101
|
+
curl -X POST http://localhost:8083/api/v1/upload/multipart/init \
|
|
102
|
+
-H 'Content-Type: application/json' \
|
|
103
|
+
-d '{"filename":"a.txt","totalSize":1024,"isPublic":true}'
|
|
104
|
+
# → { "success": true, "data": { "uploadId": "mp_...", "partSize": 20971520, "totalParts": 1, ... } }
|
|
105
|
+
|
|
106
|
+
# 2) 或者用智能 init(一次批量,服务端自动分流 ONE_SHOT / MULTIPART)
|
|
107
|
+
curl -X POST http://localhost:8083/api/v1/upload/smart/init \
|
|
108
|
+
-H 'Content-Type: application/json' \
|
|
109
|
+
-H 'x-secure-session: <jwt>' \
|
|
110
|
+
-d '{"files":[{"filename":"a.txt","size":1024,"isPublic":false}]}'
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
## 🔌 扩展示例
|
|
116
|
+
|
|
117
|
+
### A. S3 / 阿里云 OSS / 腾讯云 COS
|
|
118
|
+
|
|
119
|
+
实现 `StorageAdapter`:
|
|
120
|
+
|
|
121
|
+
```ts
|
|
122
|
+
// my-storage.ts
|
|
123
|
+
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
|
|
124
|
+
import type { StorageAdapter } from 'secure-upload-fastify-sdk/storage';
|
|
125
|
+
|
|
126
|
+
export class S3Storage implements StorageAdapter {
|
|
127
|
+
constructor(
|
|
128
|
+
private client: S3Client,
|
|
129
|
+
private bucket: string,
|
|
130
|
+
private prefix = 'uploads/',
|
|
131
|
+
) {}
|
|
132
|
+
|
|
133
|
+
async put(data, meta) { /* ... */ }
|
|
134
|
+
get(key) { /* ... */ }
|
|
135
|
+
async delete(key) { /* ... */ }
|
|
136
|
+
async exists(key) { /* ... */ }
|
|
137
|
+
async putPart(uploadId, partNumber, data) { /* ... */ }
|
|
138
|
+
async listParts(uploadId) { /* ... */ }
|
|
139
|
+
async mergeParts(uploadId, totalParts, target) { /* ... */ }
|
|
140
|
+
async computeMergedHash(uploadId, totalParts) { /* ... */ }
|
|
141
|
+
async cleanupMultipart(uploadId) { /* ... */ }
|
|
142
|
+
}
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
然后直接传实例:
|
|
146
|
+
|
|
147
|
+
```ts
|
|
148
|
+
import { S3Storage } from './my-storage';
|
|
149
|
+
await app.register(secureUploadPlugin, {
|
|
150
|
+
storage: new S3Storage(new S3Client({ region: 'us-east-1' }), 'my-bucket'),
|
|
151
|
+
// ... 其他 options
|
|
152
|
+
});
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
> SDK 识别 `StorageAdapter` 实例的方式是 duck-typing:只要对象实现了 `put/get/delete/...` 全部方法,就视为自定义存储,不再走 `adapter: 'local'` 分支。
|
|
156
|
+
|
|
157
|
+
### B. Drizzle / TypeORM / MongoDB(不用 Prisma)
|
|
158
|
+
|
|
159
|
+
```ts
|
|
160
|
+
import type { MultipartRepository, FileRepository } from 'secure-upload-fastify-sdk/repository';
|
|
161
|
+
|
|
162
|
+
class DrizzleMultipartRepo implements MultipartRepository { /* ... */ }
|
|
163
|
+
class DrizzleFileRepo implements FileRepository { /* ... */ }
|
|
164
|
+
|
|
165
|
+
await app.register(secureUploadPlugin, {
|
|
166
|
+
storage: { adapter: 'local', options: { baseDir: '/var/upload' } },
|
|
167
|
+
repository: {
|
|
168
|
+
kind: 'custom',
|
|
169
|
+
multipart: new DrizzleMultipartRepo(db),
|
|
170
|
+
file: new DrizzleFileRepo(db),
|
|
171
|
+
},
|
|
172
|
+
redis,
|
|
173
|
+
});
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
### C. 内存版(测试 / POC)
|
|
177
|
+
|
|
178
|
+
```ts
|
|
179
|
+
import {
|
|
180
|
+
InMemoryStorage,
|
|
181
|
+
InMemoryMultipartRepository,
|
|
182
|
+
InMemoryFileRepository,
|
|
183
|
+
} from 'secure-upload-fastify-sdk';
|
|
184
|
+
|
|
185
|
+
await app.register(secureUploadPlugin, {
|
|
186
|
+
storage: new InMemoryStorage(),
|
|
187
|
+
repository: {
|
|
188
|
+
kind: 'custom',
|
|
189
|
+
multipart: new InMemoryMultipartRepository(),
|
|
190
|
+
file: new InMemoryFileRepository(),
|
|
191
|
+
},
|
|
192
|
+
// 不传 redis → 自动用内存版限流 + 锁
|
|
193
|
+
});
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
### D. 自定义 Session 校验(完整 JWT 验签)
|
|
197
|
+
|
|
198
|
+
```ts
|
|
199
|
+
import jwt from 'jsonwebtoken';
|
|
200
|
+
|
|
201
|
+
await app.register(secureUploadPlugin, {
|
|
202
|
+
// ... storage & repository
|
|
203
|
+
sessionValidator: (token, isPublic) => {
|
|
204
|
+
if (isPublic) return null;
|
|
205
|
+
if (!token) throw new Error('session required');
|
|
206
|
+
try {
|
|
207
|
+
const payload = jwt.verify(token, process.env.JWT_SECRET!) as { sub: number };
|
|
208
|
+
return payload.sub;
|
|
209
|
+
} catch {
|
|
210
|
+
throw new Error('invalid session');
|
|
211
|
+
}
|
|
212
|
+
},
|
|
213
|
+
});
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
> 默认 `sessionValidator` 只 base64-decode JWT payload 并校验 `exp` + 读 `sub`,不做 HMAC 验签(SDK 不持有 `appSecret`)。
|
|
217
|
+
> 完整安全链路:gateway 已做 JWT 验签,业务服务信任透传的 session;若业务服务要自己验签,通过 `options.sessionValidator` 注入即可。
|
|
218
|
+
> 校验失败应抛 `SecureError(ErrorCode.BINARY_AUTH_REQUIRED / BINARY_AUTH_FAILED)`,SDK 会自动转成 401 响应。
|
|
219
|
+
|
|
220
|
+
---
|
|
221
|
+
|
|
222
|
+
## 🔐 Secure-Crypto 模式
|
|
223
|
+
|
|
224
|
+
默认情况下,SDK 从 `req.body` 和 `req.headers['x-secure-raw-base64']` 读数据(`raw` 模式)。
|
|
225
|
+
如果项目里装了 [`secure-crypto-top-zod-sdk`](https://www.npmjs.com/package/secure-crypto-top-zod-sdk) 并在上传插件**之前**注册,切到 `secure-crypto` 模式:
|
|
226
|
+
|
|
227
|
+
```ts
|
|
228
|
+
import secureCryptoPlugin from 'secure-crypto-top-zod-sdk/server';
|
|
229
|
+
import secureUploadPlugin from 'secure-upload-fastify-sdk/server';
|
|
230
|
+
|
|
231
|
+
await app.register(secureCryptoPlugin, {
|
|
232
|
+
apps: [{ appKey: 'demo', appSecret: 'xxx', allowedCiphers: ['aes-256-gcm'], allowedDigests: ['hmac-sha256'] }],
|
|
233
|
+
encryptionKeys: { demo: process.env.ENCRYPTION_KEY! },
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
await app.register(secureUploadPlugin, {
|
|
237
|
+
// ... storage & repository
|
|
238
|
+
secure: { mode: 'secure-crypto' },
|
|
239
|
+
});
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
在该模式下,SDK 读:
|
|
243
|
+
|
|
244
|
+
- `req.secureRequest.biz` — JSON 业务字段
|
|
245
|
+
- `req.secureRequest.raw.data` — binary part 负载
|
|
246
|
+
|
|
247
|
+
> SDK **不**在运行时 import 加密 SDK,只对 `req.secureRequest` 做 duck-typing。
|
|
248
|
+
> 该依赖在 `peerDependenciesMeta.optional` 标记为可选,完全 opt-in。
|
|
249
|
+
|
|
250
|
+
---
|
|
251
|
+
|
|
252
|
+
## 📐 架构
|
|
253
|
+
|
|
254
|
+
```
|
|
255
|
+
┌────────────────────────────────────────────────────────────┐
|
|
256
|
+
│ Fastify App │
|
|
257
|
+
│ ├─ secure-crypto-top-zod-sdk (optional, before upload) │
|
|
258
|
+
│ └─ secure-upload-fastify-sdk ← this SDK │
|
|
259
|
+
│ │ │
|
|
260
|
+
│ ├─ Routes ─┬─ POST /multipart/init │
|
|
261
|
+
│ │ ├─ POST /multipart/part (binary) │
|
|
262
|
+
│ │ ├─ POST /multipart/complete │
|
|
263
|
+
│ │ ├─ POST /multipart/abort │
|
|
264
|
+
│ │ ├─ POST /multipart/status │
|
|
265
|
+
│ │ └─ POST /smart/init │
|
|
266
|
+
│ │ │
|
|
267
|
+
│ ├─ Strategy (chooseStrategy: ONE_SHOT / MULTIPART) │
|
|
268
|
+
│ ├─ Storage (StorageAdapter interface) │
|
|
269
|
+
│ │ ├─ LocalStorage (built-in) │
|
|
270
|
+
│ │ └─ your S3Storage / OSSStorage / COSStorage │
|
|
271
|
+
│ │ │
|
|
272
|
+
│ ├─ Repository (MultipartRepo + FileRepo interface) │
|
|
273
|
+
│ │ ├─ PrismaMultipartRepo + PrismaFileRepo (built-in)│
|
|
274
|
+
│ │ └─ your Drizzle / TypeORM / Mongo │
|
|
275
|
+
│ │ │
|
|
276
|
+
│ ├─ RateLimit & Lock │
|
|
277
|
+
│ │ ├─ ioredis (if redis: provided) │
|
|
278
|
+
│ │ └─ in-memory fallback (single-process) │
|
|
279
|
+
│ │ │
|
|
280
|
+
│ └─ Background tasks (setInterval) │
|
|
281
|
+
│ ├─ multipart cleanup (expire INIT/UPLOADING) │
|
|
282
|
+
│ └─ orphan file scan (DB has / disk missing) │
|
|
283
|
+
└────────────────────────────────────────────────────────────┘
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
---
|
|
287
|
+
|
|
288
|
+
## 🧠 Smart 智能分流策略
|
|
289
|
+
|
|
290
|
+
`/smart/init` 服务端按 `totalSize` 选 `ONE_SHOT` / `MULTIPART`,动态放大 partSize 减少 part 数:
|
|
291
|
+
|
|
292
|
+
| 文件大小 | 策略 | partSize | 客户端 RTT |
|
|
293
|
+
|---------|------|----------|-----------|
|
|
294
|
+
| 0 字节 | `ONE_SHOT` | 0 | 2 (part+complete) |
|
|
295
|
+
| ≤ 1MB (`oneShotMax`) | `ONE_SHOT` | = 文件大小 | 2 |
|
|
296
|
+
| 1MB ~ 1GB | `MULTIPART` | 1MB | N+1 |
|
|
297
|
+
| 1GB ~ 10GB | `MULTIPART` | 5MB | N+1 |
|
|
298
|
+
| > 10GB | `MULTIPART` | 10MB | N+1 |
|
|
299
|
+
|
|
300
|
+
> 5GB 用 1MB/part → 5120 part,DB / Redis / 客户端并发调度压力大;
|
|
301
|
+
> 5GB 用 5MB/part → 1024 part,网络利用率高、开销可接受。
|
|
302
|
+
|
|
303
|
+
并发建议 (`suggestConcurrency`):
|
|
304
|
+
|
|
305
|
+
- `ONE_SHOT` → 1
|
|
306
|
+
- `totalParts ≤ 4` → `totalParts`
|
|
307
|
+
- 其他 → 4
|
|
308
|
+
|
|
309
|
+
---
|
|
310
|
+
|
|
311
|
+
## ⚡ 秒传隔离
|
|
312
|
+
|
|
313
|
+
`/multipart/init` 和 `/smart/init` 都支持秒传:传 `expectedHash` 时,服务端按 `(contentHash, isPublic, userId)` 三元组查询已有 `File` 记录。
|
|
314
|
+
|
|
315
|
+
| 维度 | 公开上传 | 私有上传 |
|
|
316
|
+
|------|----------|----------|
|
|
317
|
+
| `contentHash` | 必须相同 | 必须相同 |
|
|
318
|
+
| `isPublic` | 必须 `true` | 必须 `false` |
|
|
319
|
+
| `userId` | 不参与 | 必须与上传者一致 |
|
|
320
|
+
| `deletedAt` | 排除 | 排除 |
|
|
321
|
+
|
|
322
|
+
> **为什么三元组而不是单 `contentHash`?**
|
|
323
|
+
> 只用 `contentHash` 会导致公开/私密串号,两个不同用户的私密文件也会互相命中。
|
|
324
|
+
> 公开池共享一份、私密池每人一份,完全隔离。
|
|
325
|
+
|
|
326
|
+
---
|
|
327
|
+
|
|
328
|
+
## 🔧 选项参考
|
|
329
|
+
|
|
330
|
+
| 字段 | 类型 | 默认值 | 说明 |
|
|
331
|
+
|---|---|---|---|
|
|
332
|
+
| `storage` | `{ adapter: 'local', options: { baseDir } } \| StorageAdapter` | — (必填) | 存储后端 |
|
|
333
|
+
| `repository` | `{ kind: 'prisma', prisma } \| { kind: 'custom', multipart, file }` | — (必填) | 数据层 |
|
|
334
|
+
| `redis` | `Redis` (ioredis) | `undefined` | 不传则用内存版限流 + 锁 |
|
|
335
|
+
| `secure.mode` | `'raw' \| 'secure-crypto'` | `'raw'` | `req.body` 或 `req.secureRequest` |
|
|
336
|
+
| `enabled` | `boolean` | `true` | 上传路由总开关 |
|
|
337
|
+
| `fileMaxSize` | `number` (字节) | `5 GB` | 单文件大小上限 |
|
|
338
|
+
| `partSize.min` | `number` (字节) | `5 MB` | 最小分片 |
|
|
339
|
+
| `partSize.max` | `number` (字节) | `50 MB` | 最大分片 |
|
|
340
|
+
| `partSize.default` | `number` (字节) | `20 MB` | 默认分片(客户端不传时) |
|
|
341
|
+
| `ttlSeconds` | `number` | `86400` (24h) | `uploadId` 有效期 |
|
|
342
|
+
| `suggestedClientConcurrency` | `number` | `3` | 建议客户端并发 part 数 |
|
|
343
|
+
| `rateLimit.initPerMin` | `number` | `30` | `/multipart/init` 限流 |
|
|
344
|
+
| `rateLimit.partPerMin` | `number` | `600` | `/multipart/part` 限流 |
|
|
345
|
+
| `rateLimit.smartMaxFilesPerInit` | `number` | `50` | `/smart/init` 单次最多文件数 |
|
|
346
|
+
| `pathPrefix.multipart` | `string` | `'/multipart'` | 分片路由挂载前缀 |
|
|
347
|
+
| `pathPrefix.smart` | `string` | `'/smart'` | smart 路由挂载前缀 |
|
|
348
|
+
| `fileUrlPrefix` | `string` | `'/files'` | complete 响应 `url` 字段前缀 |
|
|
349
|
+
| `sessionValidator` | `(token, isPublic) => userId \| null` | base64-decode JWT | 自定义 session 校验 |
|
|
350
|
+
| `strategy` | `Partial<StrategyConfig>` | `DEFAULT_STRATEGY_CONFIG` | smart 策略调优 |
|
|
351
|
+
| `cleanupTask.enabled` | `boolean` | `true` | 过期 `uploadId` 清理 |
|
|
352
|
+
| `cleanupTask.intervalSeconds` | `number` | `300` | 清理间隔(秒) |
|
|
353
|
+
| `cleanupTask.batchLimit` | `number` | `100` | 单次扫描条数 |
|
|
354
|
+
| `orphanScan.enabled` | `boolean` | `true` | 孤儿文件扫描 |
|
|
355
|
+
| `orphanScan.intervalSeconds` | `number` | `300` | 扫描间隔(秒) |
|
|
356
|
+
| `orphanScan.lookbackDays` | `number` | `7` | 回溯天数 |
|
|
357
|
+
| `orphanScan.batchSize` | `number` | `2000` | 单次扫描条数 |
|
|
358
|
+
|
|
359
|
+
---
|
|
360
|
+
|
|
361
|
+
## 🧪 测试
|
|
362
|
+
|
|
363
|
+
```bash
|
|
364
|
+
pnpm test # 跑全部 node:test 用例
|
|
365
|
+
pnpm test:strategy # 只跑 strategy.test.ts
|
|
366
|
+
pnpm test:e2e # SDK 独立启动 e2e(server-only,不依赖 MySQL/Redis)
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
测试套件覆盖:
|
|
370
|
+
|
|
371
|
+
| 用例 | 覆盖点 |
|
|
372
|
+
|------|-------|
|
|
373
|
+
| `strategy.test.ts` | `chooseStrategy` / `suggestConcurrency` 纯函数分支 |
|
|
374
|
+
| `local-storage.test.ts` | `LocalStorage` put / part / merge / hash / cleanup |
|
|
375
|
+
| `memory-rate-limit.test.ts` | 内存版限流 + 锁 |
|
|
376
|
+
| `memory-repo.test.ts` | `InMemoryMultipartRepository` / `InMemoryFileRepository` |
|
|
377
|
+
| `plugin.test.ts` | 端到端 init / part / complete / abort / status / smart |
|
|
378
|
+
| `dedup.test.ts` | 秒传命中 + 隔离(可见性 / userId 不同时不命中) |
|
|
379
|
+
| `e2e-with-prefix.mjs` / `standalone-e2e.*` | 启动完整 Fastify 实例,跑通 5 个集成流程 |
|
|
380
|
+
|
|
381
|
+
---
|
|
382
|
+
|
|
383
|
+
## 🧰 Prisma Schema 速览
|
|
384
|
+
|
|
385
|
+
完整定义见 [`prisma/schema.prisma`](./prisma/schema.prisma),核心两张表:
|
|
386
|
+
|
|
387
|
+
```prisma
|
|
388
|
+
enum FileStatus { PENDING UPLOADING COMPLETED FAILED }
|
|
389
|
+
enum MultipartStatus { INIT UPLOADING COMPLETED ABORTED EXPIRED FAILED }
|
|
390
|
+
|
|
391
|
+
model File {
|
|
392
|
+
id String @id
|
|
393
|
+
userId Int?
|
|
394
|
+
filename String @db.VarChar(255)
|
|
395
|
+
contentType String @db.VarChar(128)
|
|
396
|
+
size BigInt
|
|
397
|
+
storageKey String @unique @db.VarChar(191)
|
|
398
|
+
isPublic Boolean @default(false)
|
|
399
|
+
status FileStatus @default(COMPLETED)
|
|
400
|
+
uploadId String? @db.VarChar(128) @unique
|
|
401
|
+
totalParts Int?
|
|
402
|
+
contentHash String? @db.VarChar(64) // SHA-256 hex,用于秒传
|
|
403
|
+
meta Json?
|
|
404
|
+
createdAt DateTime @default(now())
|
|
405
|
+
updatedAt DateTime @updatedAt
|
|
406
|
+
deletedAt DateTime?
|
|
407
|
+
@@index([userId, createdAt])
|
|
408
|
+
@@index([isPublic, createdAt])
|
|
409
|
+
@@index([status])
|
|
410
|
+
@@index([contentHash])
|
|
411
|
+
@@map("files")
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
model MultipartUpload {
|
|
415
|
+
id String @id
|
|
416
|
+
userId Int?
|
|
417
|
+
filename String @db.VarChar(255)
|
|
418
|
+
contentType String @db.VarChar(128)
|
|
419
|
+
totalSize BigInt
|
|
420
|
+
partSize Int
|
|
421
|
+
totalParts Int
|
|
422
|
+
isPublic Boolean @default(false)
|
|
423
|
+
status MultipartStatus @default(INIT)
|
|
424
|
+
receivedParts Json @default("[]") // number[]
|
|
425
|
+
partHashes Json @default("{}") // { [partNumber]: sha256 }
|
|
426
|
+
meta Json?
|
|
427
|
+
expectedHash String? @db.VarChar(64)
|
|
428
|
+
fileId String? @db.VarChar(128) @unique
|
|
429
|
+
createdAt DateTime @default(now())
|
|
430
|
+
updatedAt DateTime @updatedAt
|
|
431
|
+
expiresAt DateTime
|
|
432
|
+
completedAt DateTime?
|
|
433
|
+
abortedAt DateTime?
|
|
434
|
+
@@index([userId, createdAt])
|
|
435
|
+
@@index([status])
|
|
436
|
+
@@index([status, expiresAt])
|
|
437
|
+
@@map("multipart_uploads")
|
|
438
|
+
}
|
|
439
|
+
```
|
|
440
|
+
|
|
441
|
+
---
|
|
442
|
+
|
|
443
|
+
## 🚨 错误码
|
|
444
|
+
|
|
445
|
+
定义在 `errors.ts`,SDK 内部统一返回 `{ success: false, code, error, message }` 格式:
|
|
446
|
+
|
|
447
|
+
| 范围 | 类别 | 示例 |
|
|
448
|
+
|------|------|------|
|
|
449
|
+
| `1001` | 通用 | `INVALID_REQUEST` |
|
|
450
|
+
| `1400-1499` | HTTP 语义 | `NOT_FOUND` / `FORBIDDEN` / `RATE_LIMITED` |
|
|
451
|
+
| `1500-1599` | 上传相关 | `BINARY_AUTH_REQUIRED` / `MULTIPART_*` / `BATCH_*` |
|
|
452
|
+
|
|
453
|
+
完整列表见 [`src/server/errors.ts`](./src/server/errors.ts)。
|
|
454
|
+
HTTP 状态码映射由 `getHttpStatus(code)` 给出,用户业务侧可直接用 `ErrorCode` 枚举做分支判断。
|
|
455
|
+
|
|
456
|
+
---
|
|
457
|
+
|
|
458
|
+
## 📚 相关项目
|
|
459
|
+
|
|
460
|
+
- [`secure-crypto-top-zod-sdk`](../secure-crypto-top-zod-sdk) — 加密/解密协议,`req.secureRequest` 装饰器
|
|
461
|
+
- `backend/apps/upload-service` — SDK 在本仓库的集成实例(v2-SDK,见 `backend/README.md`)
|
|
462
|
+
|
|
463
|
+
---
|
|
464
|
+
|
|
465
|
+
## 📄 License
|
|
466
|
+
|
|
467
|
+
MIT
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 顶层入口
|
|
3
|
+
*
|
|
4
|
+
* 直接 re-export server/index.ts 的全部内容,
|
|
5
|
+
* 用户可以从 'secure-upload-fastify-sdk' 单点 import:
|
|
6
|
+
*
|
|
7
|
+
* import secureUploadPlugin, { LocalStorage, ErrorCode, SecureError } from 'secure-upload-fastify-sdk';
|
|
8
|
+
*
|
|
9
|
+
* 也可以从子路径精确 import:
|
|
10
|
+
* import { LocalStorage } from 'secure-upload-fastify-sdk/storage';
|
|
11
|
+
* import { ErrorCode } from 'secure-upload-fastify-sdk/errors';
|
|
12
|
+
*/
|
|
13
|
+
export * from './server/index';
|
|
14
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,cAAc,gBAAgB,CAAC"}
|