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.
Files changed (74) hide show
  1. package/README.md +467 -0
  2. package/dist/index.d.ts +14 -0
  3. package/dist/index.d.ts.map +1 -0
  4. package/dist/index.js +2069 -0
  5. package/dist/index.js.map +1 -0
  6. package/dist/index.mjs +2023 -0
  7. package/dist/index.mjs.map +1 -0
  8. package/dist/server/adapters/secure-crypto.d.ts +28 -0
  9. package/dist/server/adapters/secure-crypto.d.ts.map +1 -0
  10. package/dist/server/errors.d.ts +48 -0
  11. package/dist/server/errors.d.ts.map +1 -0
  12. package/dist/server/errors.js +112 -0
  13. package/dist/server/errors.js.map +1 -0
  14. package/dist/server/errors.mjs +85 -0
  15. package/dist/server/errors.mjs.map +1 -0
  16. package/dist/server/fastify-plugin.d.ts +38 -0
  17. package/dist/server/fastify-plugin.d.ts.map +1 -0
  18. package/dist/server/index.d.ts +17 -0
  19. package/dist/server/index.d.ts.map +1 -0
  20. package/dist/server/index.js +2070 -0
  21. package/dist/server/index.js.map +1 -0
  22. package/dist/server/index.mjs +2024 -0
  23. package/dist/server/index.mjs.map +1 -0
  24. package/dist/server/routes/_internal.d.ts +42 -0
  25. package/dist/server/routes/_internal.d.ts.map +1 -0
  26. package/dist/server/routes/multipart.d.ts +22 -0
  27. package/dist/server/routes/multipart.d.ts.map +1 -0
  28. package/dist/server/routes/smart.d.ts +16 -0
  29. package/dist/server/routes/smart.d.ts.map +1 -0
  30. package/dist/server/session.d.ts +19 -0
  31. package/dist/server/session.d.ts.map +1 -0
  32. package/dist/server/types.d.ts +233 -0
  33. package/dist/server/types.d.ts.map +1 -0
  34. package/dist/server/utils/cleaner.d.ts +20 -0
  35. package/dist/server/utils/cleaner.d.ts.map +1 -0
  36. package/dist/server/utils/memory-rate-limit.d.ts +29 -0
  37. package/dist/server/utils/memory-rate-limit.d.ts.map +1 -0
  38. package/dist/server/utils/orphan-scanner.d.ts +34 -0
  39. package/dist/server/utils/orphan-scanner.d.ts.map +1 -0
  40. package/dist/server/utils/redis-helpers.d.ts +25 -0
  41. package/dist/server/utils/redis-helpers.d.ts.map +1 -0
  42. package/dist/server/utils/repository/index.d.ts +27 -0
  43. package/dist/server/utils/repository/index.d.ts.map +1 -0
  44. package/dist/server/utils/repository/index.js +501 -0
  45. package/dist/server/utils/repository/index.js.map +1 -0
  46. package/dist/server/utils/repository/index.mjs +469 -0
  47. package/dist/server/utils/repository/index.mjs.map +1 -0
  48. package/dist/server/utils/repository/memory.d.ts +33 -0
  49. package/dist/server/utils/repository/memory.d.ts.map +1 -0
  50. package/dist/server/utils/repository/prisma.d.ts +49 -0
  51. package/dist/server/utils/repository/prisma.d.ts.map +1 -0
  52. package/dist/server/utils/repository/types-extra.d.ts +9 -0
  53. package/dist/server/utils/repository/types-extra.d.ts.map +1 -0
  54. package/dist/server/utils/repository/types.d.ts +128 -0
  55. package/dist/server/utils/repository/types.d.ts.map +1 -0
  56. package/dist/server/utils/storage/index.d.ts +115 -0
  57. package/dist/server/utils/storage/index.d.ts.map +1 -0
  58. package/dist/server/utils/storage/index.js +285 -0
  59. package/dist/server/utils/storage/index.js.map +1 -0
  60. package/dist/server/utils/storage/index.mjs +247 -0
  61. package/dist/server/utils/storage/index.mjs.map +1 -0
  62. package/dist/server/utils/storage/local.d.ts +46 -0
  63. package/dist/server/utils/storage/local.d.ts.map +1 -0
  64. package/dist/server/utils/storage/memory.d.ts +34 -0
  65. package/dist/server/utils/storage/memory.d.ts.map +1 -0
  66. package/dist/server/utils/storage/types.d.ts +78 -0
  67. package/dist/server/utils/storage/types.d.ts.map +1 -0
  68. package/dist/server/utils/strategy.d.ts +64 -0
  69. package/dist/server/utils/strategy.d.ts.map +1 -0
  70. package/dist/server/utils/strategy.js +130 -0
  71. package/dist/server/utils/strategy.js.map +1 -0
  72. package/dist/server/utils/strategy.mjs +103 -0
  73. package/dist/server/utils/strategy.mjs.map +1 -0
  74. package/package.json +97 -0
@@ -0,0 +1,469 @@
1
+ // src/server/utils/repository/types.ts
2
+ var MultipartStatus = {
3
+ INIT: "INIT",
4
+ UPLOADING: "UPLOADING",
5
+ COMPLETED: "COMPLETED",
6
+ ABORTED: "ABORTED",
7
+ EXPIRED: "EXPIRED",
8
+ FAILED: "FAILED"
9
+ };
10
+ var FileStatus = {
11
+ PENDING: "PENDING",
12
+ UPLOADING: "UPLOADING",
13
+ COMPLETED: "COMPLETED",
14
+ FAILED: "FAILED"
15
+ };
16
+
17
+ // src/server/utils/repository/prisma.ts
18
+ function toMultipartRecord(row) {
19
+ return {
20
+ id: row.id,
21
+ userId: row.userId,
22
+ filename: row.filename,
23
+ contentType: row.contentType,
24
+ totalSize: row.totalSize,
25
+ partSize: row.partSize,
26
+ totalParts: row.totalParts,
27
+ isPublic: row.isPublic,
28
+ status: row.status,
29
+ receivedParts: Array.isArray(row.receivedParts) ? row.receivedParts : [],
30
+ partHashes: row.partHashes && typeof row.partHashes === "object" ? row.partHashes : {},
31
+ meta: row.meta && typeof row.meta === "object" ? row.meta : null,
32
+ expectedHash: row.expectedHash,
33
+ fileId: row.fileId,
34
+ createdAt: row.createdAt,
35
+ updatedAt: row.updatedAt,
36
+ expiresAt: row.expiresAt,
37
+ completedAt: row.completedAt,
38
+ abortedAt: row.abortedAt
39
+ };
40
+ }
41
+ function toFileRecord(row) {
42
+ return {
43
+ id: row.id,
44
+ userId: row.userId,
45
+ filename: row.filename,
46
+ contentType: row.contentType,
47
+ size: row.size,
48
+ storageKey: row.storageKey,
49
+ isPublic: row.isPublic,
50
+ status: row.status,
51
+ uploadId: row.uploadId,
52
+ totalParts: row.totalParts,
53
+ contentHash: row.contentHash,
54
+ meta: row.meta && typeof row.meta === "object" ? row.meta : null,
55
+ createdAt: row.createdAt,
56
+ updatedAt: row.updatedAt,
57
+ deletedAt: row.deletedAt
58
+ };
59
+ }
60
+ var PrismaMultipartRepository = class {
61
+ constructor(prisma) {
62
+ this.prisma = prisma;
63
+ }
64
+ prisma;
65
+ async createInit(input) {
66
+ const expiresAt = new Date(Date.now() + input.ttlSeconds * 1e3);
67
+ const row = await this.prisma.multipartUpload.create({
68
+ data: {
69
+ userId: input.userId,
70
+ filename: input.filename,
71
+ contentType: input.contentType,
72
+ totalSize: BigInt(input.totalSize),
73
+ partSize: input.partSize,
74
+ totalParts: input.totalParts,
75
+ isPublic: input.isPublic,
76
+ status: MultipartStatus.INIT,
77
+ receivedParts: [],
78
+ partHashes: {},
79
+ meta: input.meta ?? void 0,
80
+ expectedHash: input.expectedHash ?? null,
81
+ expiresAt
82
+ }
83
+ });
84
+ return toMultipartRecord(row);
85
+ }
86
+ async findById(id) {
87
+ const row = await this.prisma.multipartUpload.findUnique({ where: { id } });
88
+ return row ? toMultipartRecord(row) : null;
89
+ }
90
+ async addPart(id, partNumber, partHash) {
91
+ return await this.prisma.$transaction(async (tx) => {
92
+ const row = await tx.multipartUpload.findUnique({ where: { id } });
93
+ if (!row) throw new Error("NOT_FOUND");
94
+ const received = Array.isArray(row.receivedParts) ? row.receivedParts : [];
95
+ const hashes = row.partHashes && typeof row.partHashes === "object" ? row.partHashes : {};
96
+ if (received.includes(partNumber)) {
97
+ return {
98
+ updated: false,
99
+ duplicate: true,
100
+ existingHash: hashes[String(partNumber)] ?? null,
101
+ record: toMultipartRecord(row)
102
+ };
103
+ }
104
+ const next = [...received, partNumber].sort((a, b) => a - b);
105
+ const nextHashes = { ...hashes };
106
+ if (partHash) nextHashes[String(partNumber)] = partHash;
107
+ const newStatus = row.status === MultipartStatus.INIT ? MultipartStatus.UPLOADING : row.status;
108
+ if (row.status === MultipartStatus.COMPLETED || row.status === MultipartStatus.ABORTED || row.status === MultipartStatus.EXPIRED || row.status === MultipartStatus.FAILED) {
109
+ throw new Error("STATUS_INVALID");
110
+ }
111
+ const updated = await tx.multipartUpload.update({
112
+ where: { id },
113
+ data: {
114
+ receivedParts: next,
115
+ partHashes: nextHashes,
116
+ status: newStatus
117
+ }
118
+ });
119
+ return { updated: true, record: toMultipartRecord(updated) };
120
+ });
121
+ }
122
+ async markCompleted(id, fileId) {
123
+ const row = await this.prisma.multipartUpload.updateMany({
124
+ where: { id, status: { in: [MultipartStatus.INIT, MultipartStatus.UPLOADING] } },
125
+ data: {
126
+ status: MultipartStatus.COMPLETED,
127
+ fileId,
128
+ completedAt: /* @__PURE__ */ new Date()
129
+ }
130
+ });
131
+ if (row.count === 0) {
132
+ const cur = await this.prisma.multipartUpload.findUnique({ where: { id } });
133
+ if (!cur) throw new Error("NOT_FOUND");
134
+ throw new Error("STATUS_INVALID");
135
+ }
136
+ const fresh = await this.prisma.multipartUpload.findUnique({ where: { id } });
137
+ return toMultipartRecord(fresh);
138
+ }
139
+ async markAborted(id) {
140
+ const row = await this.prisma.multipartUpload.updateMany({
141
+ where: { id, status: { in: [MultipartStatus.INIT, MultipartStatus.UPLOADING] } },
142
+ data: {
143
+ status: MultipartStatus.ABORTED,
144
+ abortedAt: /* @__PURE__ */ new Date()
145
+ }
146
+ });
147
+ if (row.count === 0) {
148
+ const cur = await this.prisma.multipartUpload.findUnique({ where: { id } });
149
+ if (!cur) throw new Error("NOT_FOUND");
150
+ if (cur.status === MultipartStatus.COMPLETED) throw new Error("ALREADY_COMPLETED");
151
+ throw new Error("STATUS_INVALID");
152
+ }
153
+ const fresh = await this.prisma.multipartUpload.findUnique({ where: { id } });
154
+ return toMultipartRecord(fresh);
155
+ }
156
+ async markExpired(id) {
157
+ await this.prisma.multipartUpload.updateMany({
158
+ where: { id, status: { in: [MultipartStatus.INIT, MultipartStatus.UPLOADING] } },
159
+ data: {
160
+ status: MultipartStatus.EXPIRED,
161
+ abortedAt: /* @__PURE__ */ new Date()
162
+ }
163
+ });
164
+ }
165
+ async listExpired(limit) {
166
+ const rows = await this.prisma.multipartUpload.findMany({
167
+ where: {
168
+ status: { in: [MultipartStatus.INIT, MultipartStatus.UPLOADING] },
169
+ expiresAt: { lt: /* @__PURE__ */ new Date() }
170
+ },
171
+ take: limit,
172
+ orderBy: { expiresAt: "asc" }
173
+ });
174
+ return rows.map(toMultipartRecord);
175
+ }
176
+ };
177
+ var PrismaFileRepository = class {
178
+ constructor(prisma) {
179
+ this.prisma = prisma;
180
+ }
181
+ prisma;
182
+ async findByHash(hash) {
183
+ const row = await this.prisma.file.findFirst({
184
+ where: { contentHash: hash, deletedAt: null }
185
+ });
186
+ return row ? toFileRecord(row) : null;
187
+ }
188
+ async findDedupCandidate(hash, isPublic, userId) {
189
+ const row = await this.prisma.file.findFirst({
190
+ where: {
191
+ contentHash: hash,
192
+ deletedAt: null,
193
+ isPublic,
194
+ ...isPublic ? {} : { userId }
195
+ },
196
+ orderBy: { createdAt: "asc" }
197
+ });
198
+ return row ? toFileRecord(row) : null;
199
+ }
200
+ async findById(id) {
201
+ const row = await this.prisma.file.findUnique({ where: { id } });
202
+ return row ? toFileRecord(row) : null;
203
+ }
204
+ async create(input) {
205
+ const row = await this.prisma.file.create({
206
+ data: {
207
+ id: input.id,
208
+ userId: input.userId,
209
+ filename: input.filename,
210
+ contentType: input.contentType,
211
+ size: BigInt(input.size),
212
+ storageKey: input.storageKey,
213
+ isPublic: input.isPublic,
214
+ status: FileStatus.COMPLETED,
215
+ uploadId: input.uploadId,
216
+ totalParts: input.totalParts,
217
+ contentHash: input.contentHash,
218
+ meta: input.meta ?? void 0
219
+ }
220
+ });
221
+ return toFileRecord(row);
222
+ }
223
+ async softDelete(id, metaPatch) {
224
+ if (metaPatch) {
225
+ const cur = await this.prisma.file.findUnique({ where: { id } });
226
+ const next = { ...cur?.meta ?? {}, ...metaPatch };
227
+ await this.prisma.file.update({
228
+ where: { id },
229
+ data: { deletedAt: /* @__PURE__ */ new Date(), meta: next }
230
+ });
231
+ } else {
232
+ await this.prisma.file.update({
233
+ where: { id },
234
+ data: { deletedAt: /* @__PURE__ */ new Date() }
235
+ });
236
+ }
237
+ }
238
+ async listOrphans(opts) {
239
+ const lookbackMs = opts.sinceDays * 24 * 3600 * 1e3;
240
+ const rows = await this.prisma.file.findMany({
241
+ where: {
242
+ deletedAt: null,
243
+ storageKey: { not: "" },
244
+ createdAt: { gt: new Date(Date.now() - lookbackMs) }
245
+ },
246
+ select: {
247
+ id: true,
248
+ userId: true,
249
+ filename: true,
250
+ contentType: true,
251
+ size: true,
252
+ storageKey: true,
253
+ isPublic: true,
254
+ status: true,
255
+ uploadId: true,
256
+ totalParts: true,
257
+ contentHash: true,
258
+ meta: true,
259
+ createdAt: true,
260
+ updatedAt: true,
261
+ deletedAt: true
262
+ },
263
+ take: opts.limit,
264
+ orderBy: { createdAt: "desc" }
265
+ });
266
+ return rows.map(toFileRecord);
267
+ }
268
+ };
269
+
270
+ // src/server/utils/repository/memory.ts
271
+ var InMemoryMultipartRepository = class {
272
+ store = /* @__PURE__ */ new Map();
273
+ idCounter = 0;
274
+ nextId() {
275
+ return `mp_${Date.now()}_${++this.idCounter}`;
276
+ }
277
+ async createInit(input) {
278
+ const id = this.nextId();
279
+ const now = /* @__PURE__ */ new Date();
280
+ const rec = {
281
+ id,
282
+ userId: input.userId,
283
+ filename: input.filename,
284
+ contentType: input.contentType,
285
+ totalSize: BigInt(input.totalSize),
286
+ partSize: input.partSize,
287
+ totalParts: input.totalParts,
288
+ isPublic: input.isPublic,
289
+ status: MultipartStatus.INIT,
290
+ receivedParts: [],
291
+ partHashes: {},
292
+ meta: input.meta ?? null,
293
+ expectedHash: input.expectedHash ?? null,
294
+ fileId: null,
295
+ createdAt: now,
296
+ updatedAt: now,
297
+ expiresAt: new Date(now.getTime() + input.ttlSeconds * 1e3),
298
+ completedAt: null,
299
+ abortedAt: null
300
+ };
301
+ this.store.set(id, rec);
302
+ return rec;
303
+ }
304
+ async findById(id) {
305
+ return this.store.get(id) ?? null;
306
+ }
307
+ async addPart(id, partNumber, partHash) {
308
+ const rec = this.store.get(id);
309
+ if (!rec) throw new Error("NOT_FOUND");
310
+ if (rec.status === MultipartStatus.COMPLETED || rec.status === MultipartStatus.ABORTED || rec.status === MultipartStatus.EXPIRED || rec.status === MultipartStatus.FAILED) {
311
+ throw new Error("STATUS_INVALID");
312
+ }
313
+ if (rec.receivedParts.includes(partNumber)) {
314
+ return {
315
+ updated: false,
316
+ duplicate: true,
317
+ existingHash: rec.partHashes[String(partNumber)] ?? null,
318
+ record: rec
319
+ };
320
+ }
321
+ const next = [...rec.receivedParts, partNumber].sort((a, b) => a - b);
322
+ const nextHashes = { ...rec.partHashes };
323
+ if (partHash) nextHashes[String(partNumber)] = partHash;
324
+ const updated = {
325
+ ...rec,
326
+ receivedParts: next,
327
+ partHashes: nextHashes,
328
+ status: rec.status === MultipartStatus.INIT ? MultipartStatus.UPLOADING : rec.status,
329
+ updatedAt: /* @__PURE__ */ new Date()
330
+ };
331
+ this.store.set(id, updated);
332
+ return { updated: true, record: updated };
333
+ }
334
+ async markCompleted(id, fileId) {
335
+ const rec = this.store.get(id);
336
+ if (!rec) throw new Error("NOT_FOUND");
337
+ if (rec.status !== MultipartStatus.INIT && rec.status !== MultipartStatus.UPLOADING) {
338
+ throw new Error("STATUS_INVALID");
339
+ }
340
+ const updated = {
341
+ ...rec,
342
+ status: MultipartStatus.COMPLETED,
343
+ fileId,
344
+ completedAt: /* @__PURE__ */ new Date(),
345
+ updatedAt: /* @__PURE__ */ new Date()
346
+ };
347
+ this.store.set(id, updated);
348
+ return updated;
349
+ }
350
+ async markAborted(id) {
351
+ const rec = this.store.get(id);
352
+ if (!rec) throw new Error("NOT_FOUND");
353
+ if (rec.status === MultipartStatus.COMPLETED) throw new Error("ALREADY_COMPLETED");
354
+ if (rec.status !== MultipartStatus.INIT && rec.status !== MultipartStatus.UPLOADING) {
355
+ throw new Error("STATUS_INVALID");
356
+ }
357
+ const updated = {
358
+ ...rec,
359
+ status: MultipartStatus.ABORTED,
360
+ abortedAt: /* @__PURE__ */ new Date(),
361
+ updatedAt: /* @__PURE__ */ new Date()
362
+ };
363
+ this.store.set(id, updated);
364
+ return updated;
365
+ }
366
+ async markExpired(id) {
367
+ const rec = this.store.get(id);
368
+ if (!rec) return;
369
+ if (rec.status !== MultipartStatus.INIT && rec.status !== MultipartStatus.UPLOADING) return;
370
+ this.store.set(id, {
371
+ ...rec,
372
+ status: MultipartStatus.EXPIRED,
373
+ abortedAt: /* @__PURE__ */ new Date(),
374
+ updatedAt: /* @__PURE__ */ new Date()
375
+ });
376
+ }
377
+ async listExpired(limit) {
378
+ const now = Date.now();
379
+ const list = [];
380
+ for (const rec of this.store.values()) {
381
+ if ((rec.status === MultipartStatus.INIT || rec.status === MultipartStatus.UPLOADING) && rec.expiresAt.getTime() < now) {
382
+ list.push(rec);
383
+ }
384
+ }
385
+ list.sort((a, b) => a.expiresAt.getTime() - b.expiresAt.getTime());
386
+ return list.slice(0, limit);
387
+ }
388
+ };
389
+ var InMemoryFileRepository = class {
390
+ store = /* @__PURE__ */ new Map();
391
+ idCounter = 0;
392
+ nextId() {
393
+ return `f_${Date.now()}_${++this.idCounter}`;
394
+ }
395
+ async findByHash(hash) {
396
+ for (const f of this.store.values()) {
397
+ if (f.contentHash === hash && !f.deletedAt) return f;
398
+ }
399
+ return null;
400
+ }
401
+ async findDedupCandidate(hash, isPublic, userId) {
402
+ for (const f of this.store.values()) {
403
+ if (f.deletedAt) continue;
404
+ if (f.contentHash !== hash) continue;
405
+ if (f.isPublic !== isPublic) continue;
406
+ if (!isPublic && f.userId !== userId) continue;
407
+ return f;
408
+ }
409
+ return null;
410
+ }
411
+ async findById(id) {
412
+ return this.store.get(id) ?? null;
413
+ }
414
+ async create(input) {
415
+ let id = input.id;
416
+ if (!id) id = this.nextId();
417
+ const now = /* @__PURE__ */ new Date();
418
+ const rec = {
419
+ id,
420
+ userId: input.userId,
421
+ filename: input.filename,
422
+ contentType: input.contentType,
423
+ size: BigInt(input.size),
424
+ storageKey: input.storageKey,
425
+ isPublic: input.isPublic,
426
+ status: FileStatus.COMPLETED,
427
+ uploadId: input.uploadId,
428
+ totalParts: input.totalParts,
429
+ contentHash: input.contentHash,
430
+ meta: input.meta ?? null,
431
+ createdAt: now,
432
+ updatedAt: now,
433
+ deletedAt: null
434
+ };
435
+ this.store.set(id, rec);
436
+ return rec;
437
+ }
438
+ async softDelete(id, metaPatch) {
439
+ const rec = this.store.get(id);
440
+ if (!rec) return;
441
+ const next = {
442
+ ...rec,
443
+ meta: metaPatch ? { ...rec.meta ?? {}, ...metaPatch } : rec.meta,
444
+ deletedAt: /* @__PURE__ */ new Date(),
445
+ updatedAt: /* @__PURE__ */ new Date()
446
+ };
447
+ this.store.set(id, next);
448
+ }
449
+ async listOrphans(opts) {
450
+ const sinceMs = opts.sinceDays * 24 * 3600 * 1e3;
451
+ const list = [];
452
+ for (const f of this.store.values()) {
453
+ if (f.deletedAt) continue;
454
+ if (f.createdAt.getTime() < Date.now() - sinceMs) continue;
455
+ list.push(f);
456
+ }
457
+ list.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
458
+ return list.slice(0, opts.limit);
459
+ }
460
+ };
461
+ export {
462
+ FileStatus,
463
+ InMemoryFileRepository,
464
+ InMemoryMultipartRepository,
465
+ MultipartStatus,
466
+ PrismaFileRepository,
467
+ PrismaMultipartRepository
468
+ };
469
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../src/server/utils/repository/types.ts","../../../../src/server/utils/repository/prisma.ts","../../../../src/server/utils/repository/memory.ts"],"sourcesContent":["/**\r\n * 数据层仓库接口\r\n *\r\n * SDK 不强制绑定 ORM,只依赖这 2 个接口:\r\n * - MultipartRepository: 分片上传上下文(init/part/complete/abort/expire)\r\n * - FileRepository: 文件元信息(秒传查询、orphan 扫描)\r\n *\r\n * 接入方式:\r\n * - kind: 'prisma' → SDK 用内置 PrismaMultipartRepository + PrismaFileRepository\r\n * - kind: 'custom' → 用户注入自己的实现(Drizzle / TypeORM / 内存 / Mongo ...)\r\n */\r\n\r\nimport type { MultipartStatusT, FileStatusT } from './types-extra';\r\n\r\n// ============================================================\r\n// MultipartRecord / FileRecord: 业务层最小字段集\r\n// ============================================================\r\n\r\nexport interface MultipartRecord {\r\n id: string;\r\n userId: number | null;\r\n filename: string;\r\n contentType: string;\r\n totalSize: bigint;\r\n partSize: number;\r\n totalParts: number;\r\n isPublic: boolean;\r\n status: MultipartStatusT;\r\n receivedParts: number[];\r\n partHashes: Record<string, string>;\r\n meta: Record<string, unknown> | null;\r\n expectedHash: string | null;\r\n fileId: string | null;\r\n createdAt: Date;\r\n updatedAt: Date;\r\n expiresAt: Date;\r\n completedAt: Date | null;\r\n abortedAt: Date | null;\r\n}\r\n\r\nexport interface FileRecord {\r\n id: string;\r\n userId: number | null;\r\n filename: string;\r\n contentType: string;\r\n size: bigint;\r\n storageKey: string;\r\n isPublic: boolean;\r\n status: FileStatusT;\r\n uploadId: string | null;\r\n totalParts: number | null;\r\n contentHash: string | null;\r\n meta: Record<string, unknown> | null;\r\n createdAt: Date;\r\n updatedAt: Date;\r\n deletedAt: Date | null;\r\n}\r\n\r\n// ============================================================\r\n// 输入\r\n// ============================================================\r\n\r\nexport interface CreateInitInput {\r\n userId: number | null;\r\n filename: string;\r\n contentType: string;\r\n totalSize: number;\r\n partSize: number;\r\n totalParts: number;\r\n isPublic: boolean;\r\n expectedHash?: string | null;\r\n meta?: Record<string, unknown> | null;\r\n ttlSeconds: number;\r\n}\r\n\r\nexport interface CreateFileInput {\r\n id: string;\r\n userId: number | null;\r\n filename: string;\r\n contentType: string;\r\n size: number;\r\n storageKey: string;\r\n isPublic: boolean;\r\n uploadId: string;\r\n totalParts: number;\r\n contentHash: string;\r\n meta?: Record<string, unknown> | null;\r\n}\r\n\r\nexport interface AddPartResult {\r\n updated: true;\r\n record: MultipartRecord;\r\n}\r\n\r\nexport interface AddPartDuplicate {\r\n updated: false;\r\n duplicate: true;\r\n existingHash: string | null;\r\n record: MultipartRecord;\r\n}\r\n\r\nexport type AddPartOutcome = AddPartResult | AddPartDuplicate;\r\n\r\n// ============================================================\r\n// 接口\r\n// ============================================================\r\n\r\nexport interface MultipartRepository {\r\n createInit(input: CreateInitInput): Promise<MultipartRecord>;\r\n findById(id: string): Promise<MultipartRecord | null>;\r\n addPart(id: string, partNumber: number, partHash: string | null): Promise<AddPartOutcome>;\r\n markCompleted(id: string, fileId: string): Promise<MultipartRecord>;\r\n markAborted(id: string): Promise<MultipartRecord>;\r\n markExpired(id: string): Promise<void>;\r\n listExpired(limit: number): Promise<MultipartRecord[]>;\r\n}\r\n\r\nexport interface FileRepository {\r\n findByHash(hash: string): Promise<FileRecord | null>;\r\n /**\r\n * v1.4.2: 严格秒传查询。\r\n * 必须同时满足: contentHash 相同、未删除、磁盘副本可见性与上传请求一致。\r\n * - isPublic=true → 命中 isPublic=true 的记录\r\n * - isPublic=false → 命中 isPublic=false 且 userId 与上传者一致的记录\r\n * 否则返回 null,调用方应走正常分片上传(新建独立 fileId)。\r\n */\r\n findDedupCandidate(\r\n hash: string,\r\n isPublic: boolean,\r\n userId: number | null\r\n ): Promise<FileRecord | null>;\r\n findById(id: string): Promise<FileRecord | null>;\r\n create(input: CreateFileInput): Promise<FileRecord>;\r\n softDelete(id: string, metaPatch?: Record<string, unknown>): Promise<void>;\r\n listOrphans(opts: { sinceDays: number; limit: number }): Promise<FileRecord[]>;\r\n}\r\n\r\n// ============================================================\r\n// 状态枚举(用户实现 repo 时可复用)\r\n// ============================================================\r\n\r\nexport const MultipartStatus = {\r\n INIT: 'INIT',\r\n UPLOADING: 'UPLOADING',\r\n COMPLETED: 'COMPLETED',\r\n ABORTED: 'ABORTED',\r\n EXPIRED: 'EXPIRED',\r\n FAILED: 'FAILED',\r\n} as const;\r\n\r\nexport const FileStatus = {\r\n PENDING: 'PENDING',\r\n UPLOADING: 'UPLOADING',\r\n COMPLETED: 'COMPLETED',\r\n FAILED: 'FAILED',\r\n} as const;\r\n","/**\r\n * Prisma 实现: MultipartRepository + FileRepository\r\n *\r\n * 假定用户项目里有以下两张表(见 src/prisma/schema.prisma):\r\n * - MultipartUpload(id, userId, filename, contentType, totalSize, partSize,\r\n * totalParts, isPublic, status, receivedParts(JSON),\r\n * partHashes(JSON), meta(JSON), expectedHash, fileId,\r\n * createdAt, updatedAt, expiresAt, completedAt, abortedAt)\r\n * - File(id, userId, filename, contentType, size, storageKey, isPublic,\r\n * status, uploadId, totalParts, contentHash, meta, createdAt,\r\n * updatedAt, deletedAt)\r\n *\r\n * 用户调用:\r\n * import { PrismaClient } from '@prisma/client';\r\n * import { PrismaMultipartRepository, PrismaFileRepository } from '...';\r\n * const prisma = new PrismaClient();\r\n * await app.register(secureUploadPlugin, {\r\n * repository: { kind: 'prisma', prisma },\r\n * ...\r\n * });\r\n */\r\n\r\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\r\ntype AnyClient = any;\r\n\r\nimport type {\r\n MultipartRepository,\r\n FileRepository,\r\n MultipartRecord,\r\n FileRecord,\r\n CreateInitInput,\r\n CreateFileInput,\r\n AddPartOutcome,\r\n} from './types';\r\nimport { MultipartStatus, FileStatus } from './types';\r\n\r\nfunction toMultipartRecord(row: any): MultipartRecord {\r\n return {\r\n id: row.id,\r\n userId: row.userId,\r\n filename: row.filename,\r\n contentType: row.contentType,\r\n totalSize: row.totalSize,\r\n partSize: row.partSize,\r\n totalParts: row.totalParts,\r\n isPublic: row.isPublic,\r\n status: row.status,\r\n receivedParts: Array.isArray(row.receivedParts) ? row.receivedParts : [],\r\n partHashes: row.partHashes && typeof row.partHashes === 'object' ? row.partHashes : {},\r\n meta: row.meta && typeof row.meta === 'object' ? row.meta : null,\r\n expectedHash: row.expectedHash,\r\n fileId: row.fileId,\r\n createdAt: row.createdAt,\r\n updatedAt: row.updatedAt,\r\n expiresAt: row.expiresAt,\r\n completedAt: row.completedAt,\r\n abortedAt: row.abortedAt,\r\n };\r\n}\r\n\r\nfunction toFileRecord(row: any): FileRecord {\r\n return {\r\n id: row.id,\r\n userId: row.userId,\r\n filename: row.filename,\r\n contentType: row.contentType,\r\n size: row.size,\r\n storageKey: row.storageKey,\r\n isPublic: row.isPublic,\r\n status: row.status,\r\n uploadId: row.uploadId,\r\n totalParts: row.totalParts,\r\n contentHash: row.contentHash,\r\n meta: row.meta && typeof row.meta === 'object' ? row.meta : null,\r\n createdAt: row.createdAt,\r\n updatedAt: row.updatedAt,\r\n deletedAt: row.deletedAt,\r\n };\r\n}\r\n\r\n// ============================================================\r\n// PrismaMultipartRepository\r\n// ============================================================\r\n\r\nexport class PrismaMultipartRepository implements MultipartRepository {\r\n constructor(private prisma: AnyClient) {}\r\n\r\n async createInit(input: CreateInitInput): Promise<MultipartRecord> {\r\n const expiresAt = new Date(Date.now() + input.ttlSeconds * 1000);\r\n const row = await this.prisma.multipartUpload.create({\r\n data: {\r\n userId: input.userId,\r\n filename: input.filename,\r\n contentType: input.contentType,\r\n totalSize: BigInt(input.totalSize),\r\n partSize: input.partSize,\r\n totalParts: input.totalParts,\r\n isPublic: input.isPublic,\r\n status: MultipartStatus.INIT,\r\n receivedParts: [],\r\n partHashes: {},\r\n meta: input.meta ?? undefined,\r\n expectedHash: input.expectedHash ?? null,\r\n expiresAt,\r\n },\r\n });\r\n return toMultipartRecord(row);\r\n }\r\n\r\n async findById(id: string): Promise<MultipartRecord | null> {\r\n const row = await this.prisma.multipartUpload.findUnique({ where: { id } });\r\n return row ? toMultipartRecord(row) : null;\r\n }\r\n\r\n async addPart(\r\n id: string,\r\n partNumber: number,\r\n partHash: string | null\r\n ): Promise<AddPartOutcome> {\r\n return await this.prisma.$transaction(async (tx: AnyClient) => {\r\n const row: any = await tx.multipartUpload.findUnique({ where: { id } });\r\n if (!row) throw new Error('NOT_FOUND');\r\n\r\n const received: number[] = Array.isArray(row.receivedParts) ? row.receivedParts : [];\r\n const hashes: Record<string, string> =\r\n row.partHashes && typeof row.partHashes === 'object' ? row.partHashes : {};\r\n\r\n if (received.includes(partNumber)) {\r\n return {\r\n updated: false as const,\r\n duplicate: true as const,\r\n existingHash: hashes[String(partNumber)] ?? null,\r\n record: toMultipartRecord(row),\r\n };\r\n }\r\n\r\n const next = [...received, partNumber].sort((a, b) => a - b);\r\n const nextHashes = { ...hashes };\r\n if (partHash) nextHashes[String(partNumber)] = partHash;\r\n\r\n const newStatus =\r\n row.status === MultipartStatus.INIT ? MultipartStatus.UPLOADING : row.status;\r\n\r\n if (\r\n row.status === MultipartStatus.COMPLETED ||\r\n row.status === MultipartStatus.ABORTED ||\r\n row.status === MultipartStatus.EXPIRED ||\r\n row.status === MultipartStatus.FAILED\r\n ) {\r\n throw new Error('STATUS_INVALID');\r\n }\r\n\r\n const updated = await tx.multipartUpload.update({\r\n where: { id },\r\n data: {\r\n receivedParts: next,\r\n partHashes: nextHashes,\r\n status: newStatus,\r\n },\r\n });\r\n return { updated: true as const, record: toMultipartRecord(updated) };\r\n });\r\n }\r\n\r\n async markCompleted(id: string, fileId: string): Promise<MultipartRecord> {\r\n const row = await this.prisma.multipartUpload.updateMany({\r\n where: { id, status: { in: [MultipartStatus.INIT, MultipartStatus.UPLOADING] } },\r\n data: {\r\n status: MultipartStatus.COMPLETED,\r\n fileId,\r\n completedAt: new Date(),\r\n },\r\n });\r\n if (row.count === 0) {\r\n const cur = await this.prisma.multipartUpload.findUnique({ where: { id } });\r\n if (!cur) throw new Error('NOT_FOUND');\r\n throw new Error('STATUS_INVALID');\r\n }\r\n const fresh = await this.prisma.multipartUpload.findUnique({ where: { id } });\r\n return toMultipartRecord(fresh);\r\n }\r\n\r\n async markAborted(id: string): Promise<MultipartRecord> {\r\n const row = await this.prisma.multipartUpload.updateMany({\r\n where: { id, status: { in: [MultipartStatus.INIT, MultipartStatus.UPLOADING] } },\r\n data: {\r\n status: MultipartStatus.ABORTED,\r\n abortedAt: new Date(),\r\n },\r\n });\r\n if (row.count === 0) {\r\n const cur = await this.prisma.multipartUpload.findUnique({ where: { id } });\r\n if (!cur) throw new Error('NOT_FOUND');\r\n if (cur.status === MultipartStatus.COMPLETED) throw new Error('ALREADY_COMPLETED');\r\n throw new Error('STATUS_INVALID');\r\n }\r\n const fresh = await this.prisma.multipartUpload.findUnique({ where: { id } });\r\n return toMultipartRecord(fresh);\r\n }\r\n\r\n async markExpired(id: string): Promise<void> {\r\n await this.prisma.multipartUpload.updateMany({\r\n where: { id, status: { in: [MultipartStatus.INIT, MultipartStatus.UPLOADING] } },\r\n data: {\r\n status: MultipartStatus.EXPIRED,\r\n abortedAt: new Date(),\r\n },\r\n });\r\n }\r\n\r\n async listExpired(limit: number): Promise<MultipartRecord[]> {\r\n const rows = await this.prisma.multipartUpload.findMany({\r\n where: {\r\n status: { in: [MultipartStatus.INIT, MultipartStatus.UPLOADING] },\r\n expiresAt: { lt: new Date() },\r\n },\r\n take: limit,\r\n orderBy: { expiresAt: 'asc' },\r\n });\r\n return rows.map(toMultipartRecord);\r\n }\r\n}\r\n\r\n// ============================================================\r\n// PrismaFileRepository\r\n// ============================================================\r\n\r\nexport class PrismaFileRepository implements FileRepository {\r\n constructor(private prisma: AnyClient) {}\r\n\r\n async findByHash(hash: string): Promise<FileRecord | null> {\r\n const row = await this.prisma.file.findFirst({\r\n where: { contentHash: hash, deletedAt: null },\r\n });\r\n return row ? toFileRecord(row) : null;\r\n }\r\n\r\n async findDedupCandidate(\r\n hash: string,\r\n isPublic: boolean,\r\n userId: number | null\r\n ): Promise<FileRecord | null> {\r\n const row = await this.prisma.file.findFirst({\r\n where: {\r\n contentHash: hash,\r\n deletedAt: null,\r\n isPublic,\r\n ...(isPublic ? {} : { userId }),\r\n },\r\n orderBy: { createdAt: 'asc' },\r\n });\r\n return row ? toFileRecord(row) : null;\r\n }\r\n\r\n async findById(id: string): Promise<FileRecord | null> {\r\n const row = await this.prisma.file.findUnique({ where: { id } });\r\n return row ? toFileRecord(row) : null;\r\n }\r\n\r\n async create(input: CreateFileInput): Promise<FileRecord> {\r\n const row = await this.prisma.file.create({\r\n data: {\r\n id: input.id,\r\n userId: input.userId,\r\n filename: input.filename,\r\n contentType: input.contentType,\r\n size: BigInt(input.size),\r\n storageKey: input.storageKey,\r\n isPublic: input.isPublic,\r\n status: FileStatus.COMPLETED,\r\n uploadId: input.uploadId,\r\n totalParts: input.totalParts,\r\n contentHash: input.contentHash,\r\n meta: input.meta ?? undefined,\r\n },\r\n });\r\n return toFileRecord(row);\r\n }\r\n\r\n async softDelete(id: string, metaPatch?: Record<string, unknown>): Promise<void> {\r\n if (metaPatch) {\r\n const cur = await this.prisma.file.findUnique({ where: { id } });\r\n const next = { ...((cur?.meta as any) ?? {}), ...metaPatch };\r\n await this.prisma.file.update({\r\n where: { id },\r\n data: { deletedAt: new Date(), meta: next },\r\n });\r\n } else {\r\n await this.prisma.file.update({\r\n where: { id },\r\n data: { deletedAt: new Date() },\r\n });\r\n }\r\n }\r\n\r\n async listOrphans(opts: { sinceDays: number; limit: number }): Promise<FileRecord[]> {\r\n const lookbackMs = opts.sinceDays * 24 * 3600 * 1000;\r\n const rows = await this.prisma.file.findMany({\r\n where: {\r\n deletedAt: null,\r\n storageKey: { not: '' },\r\n createdAt: { gt: new Date(Date.now() - lookbackMs) },\r\n },\r\n select: {\r\n id: true,\r\n userId: true,\r\n filename: true,\r\n contentType: true,\r\n size: true,\r\n storageKey: true,\r\n isPublic: true,\r\n status: true,\r\n uploadId: true,\r\n totalParts: true,\r\n contentHash: true,\r\n meta: true,\r\n createdAt: true,\r\n updatedAt: true,\r\n deletedAt: true,\r\n },\r\n take: opts.limit,\r\n orderBy: { createdAt: 'desc' },\r\n });\r\n return rows.map(toFileRecord);\r\n }\r\n}\r\n","/**\r\n * 内存仓库(测试 / POC 场景)\r\n *\r\n * 用 Map 模拟两张表,无并发安全保证,仅用于单进程测试。\r\n */\r\n\r\nimport type {\r\n MultipartRepository,\r\n FileRepository,\r\n MultipartRecord,\r\n FileRecord,\r\n CreateInitInput,\r\n CreateFileInput,\r\n AddPartOutcome,\r\n} from './types';\r\nimport { MultipartStatus, FileStatus } from './types';\r\n\r\n// ============================================================\r\n// InMemoryMultipartRepository\r\n// ============================================================\r\n\r\nexport class InMemoryMultipartRepository implements MultipartRepository {\r\n private store = new Map<string, MultipartRecord>();\r\n private idCounter = 0;\r\n\r\n private nextId(): string {\r\n return `mp_${Date.now()}_${++this.idCounter}`;\r\n }\r\n\r\n async createInit(input: CreateInitInput): Promise<MultipartRecord> {\r\n const id = this.nextId();\r\n const now = new Date();\r\n const rec: MultipartRecord = {\r\n id,\r\n userId: input.userId,\r\n filename: input.filename,\r\n contentType: input.contentType,\r\n totalSize: BigInt(input.totalSize),\r\n partSize: input.partSize,\r\n totalParts: input.totalParts,\r\n isPublic: input.isPublic,\r\n status: MultipartStatus.INIT,\r\n receivedParts: [],\r\n partHashes: {},\r\n meta: input.meta ?? null,\r\n expectedHash: input.expectedHash ?? null,\r\n fileId: null,\r\n createdAt: now,\r\n updatedAt: now,\r\n expiresAt: new Date(now.getTime() + input.ttlSeconds * 1000),\r\n completedAt: null,\r\n abortedAt: null,\r\n };\r\n this.store.set(id, rec);\r\n return rec;\r\n }\r\n\r\n async findById(id: string): Promise<MultipartRecord | null> {\r\n return this.store.get(id) ?? null;\r\n }\r\n\r\n async addPart(\r\n id: string,\r\n partNumber: number,\r\n partHash: string | null\r\n ): Promise<AddPartOutcome> {\r\n const rec = this.store.get(id);\r\n if (!rec) throw new Error('NOT_FOUND');\r\n\r\n if (\r\n rec.status === MultipartStatus.COMPLETED ||\r\n rec.status === MultipartStatus.ABORTED ||\r\n rec.status === MultipartStatus.EXPIRED ||\r\n rec.status === MultipartStatus.FAILED\r\n ) {\r\n throw new Error('STATUS_INVALID');\r\n }\r\n\r\n if (rec.receivedParts.includes(partNumber)) {\r\n return {\r\n updated: false,\r\n duplicate: true,\r\n existingHash: rec.partHashes[String(partNumber)] ?? null,\r\n record: rec,\r\n };\r\n }\r\n\r\n const next = [...rec.receivedParts, partNumber].sort((a, b) => a - b);\r\n const nextHashes = { ...rec.partHashes };\r\n if (partHash) nextHashes[String(partNumber)] = partHash;\r\n\r\n const updated: MultipartRecord = {\r\n ...rec,\r\n receivedParts: next,\r\n partHashes: nextHashes,\r\n status: rec.status === MultipartStatus.INIT ? MultipartStatus.UPLOADING : rec.status,\r\n updatedAt: new Date(),\r\n };\r\n this.store.set(id, updated);\r\n return { updated: true, record: updated };\r\n }\r\n\r\n async markCompleted(id: string, fileId: string): Promise<MultipartRecord> {\r\n const rec = this.store.get(id);\r\n if (!rec) throw new Error('NOT_FOUND');\r\n if (rec.status !== MultipartStatus.INIT && rec.status !== MultipartStatus.UPLOADING) {\r\n throw new Error('STATUS_INVALID');\r\n }\r\n const updated: MultipartRecord = {\r\n ...rec,\r\n status: MultipartStatus.COMPLETED,\r\n fileId,\r\n completedAt: new Date(),\r\n updatedAt: new Date(),\r\n };\r\n this.store.set(id, updated);\r\n return updated;\r\n }\r\n\r\n async markAborted(id: string): Promise<MultipartRecord> {\r\n const rec = this.store.get(id);\r\n if (!rec) throw new Error('NOT_FOUND');\r\n if (rec.status === MultipartStatus.COMPLETED) throw new Error('ALREADY_COMPLETED');\r\n if (rec.status !== MultipartStatus.INIT && rec.status !== MultipartStatus.UPLOADING) {\r\n throw new Error('STATUS_INVALID');\r\n }\r\n const updated: MultipartRecord = {\r\n ...rec,\r\n status: MultipartStatus.ABORTED,\r\n abortedAt: new Date(),\r\n updatedAt: new Date(),\r\n };\r\n this.store.set(id, updated);\r\n return updated;\r\n }\r\n\r\n async markExpired(id: string): Promise<void> {\r\n const rec = this.store.get(id);\r\n if (!rec) return;\r\n if (rec.status !== MultipartStatus.INIT && rec.status !== MultipartStatus.UPLOADING) return;\r\n this.store.set(id, {\r\n ...rec,\r\n status: MultipartStatus.EXPIRED,\r\n abortedAt: new Date(),\r\n updatedAt: new Date(),\r\n });\r\n }\r\n\r\n async listExpired(limit: number): Promise<MultipartRecord[]> {\r\n const now = Date.now();\r\n const list: MultipartRecord[] = [];\r\n for (const rec of this.store.values()) {\r\n if (\r\n (rec.status === MultipartStatus.INIT || rec.status === MultipartStatus.UPLOADING) &&\r\n rec.expiresAt.getTime() < now\r\n ) {\r\n list.push(rec);\r\n }\r\n }\r\n list.sort((a, b) => a.expiresAt.getTime() - b.expiresAt.getTime());\r\n return list.slice(0, limit);\r\n }\r\n}\r\n\r\n// ============================================================\r\n// InMemoryFileRepository\r\n// ============================================================\r\n\r\nexport class InMemoryFileRepository implements FileRepository {\r\n private store = new Map<string, FileRecord>();\r\n private idCounter = 0;\r\n\r\n private nextId(): string {\r\n return `f_${Date.now()}_${++this.idCounter}`;\r\n }\r\n\r\n async findByHash(hash: string): Promise<FileRecord | null> {\r\n for (const f of this.store.values()) {\r\n if (f.contentHash === hash && !f.deletedAt) return f;\r\n }\r\n return null;\r\n }\r\n\r\n async findDedupCandidate(\r\n hash: string,\r\n isPublic: boolean,\r\n userId: number | null\r\n ): Promise<FileRecord | null> {\r\n for (const f of this.store.values()) {\r\n if (f.deletedAt) continue;\r\n if (f.contentHash !== hash) continue;\r\n if (f.isPublic !== isPublic) continue;\r\n if (!isPublic && f.userId !== userId) continue;\r\n return f;\r\n }\r\n return null;\r\n }\r\n\r\n async findById(id: string): Promise<FileRecord | null> {\r\n return this.store.get(id) ?? null;\r\n }\r\n\r\n async create(input: CreateFileInput): Promise<FileRecord> {\r\n let id = input.id;\r\n if (!id) id = this.nextId();\r\n const now = new Date();\r\n const rec: FileRecord = {\r\n id,\r\n userId: input.userId,\r\n filename: input.filename,\r\n contentType: input.contentType,\r\n size: BigInt(input.size),\r\n storageKey: input.storageKey,\r\n isPublic: input.isPublic,\r\n status: FileStatus.COMPLETED,\r\n uploadId: input.uploadId,\r\n totalParts: input.totalParts,\r\n contentHash: input.contentHash,\r\n meta: input.meta ?? null,\r\n createdAt: now,\r\n updatedAt: now,\r\n deletedAt: null,\r\n };\r\n this.store.set(id, rec);\r\n return rec;\r\n }\r\n\r\n async softDelete(id: string, metaPatch?: Record<string, unknown>): Promise<void> {\r\n const rec = this.store.get(id);\r\n if (!rec) return;\r\n const next: FileRecord = {\r\n ...rec,\r\n meta: metaPatch ? { ...(rec.meta ?? {}), ...metaPatch } : rec.meta,\r\n deletedAt: new Date(),\r\n updatedAt: new Date(),\r\n };\r\n this.store.set(id, next);\r\n }\r\n\r\n async listOrphans(opts: { sinceDays: number; limit: number }): Promise<FileRecord[]> {\r\n const sinceMs = opts.sinceDays * 24 * 3600 * 1000;\r\n const list: FileRecord[] = [];\r\n for (const f of this.store.values()) {\r\n if (f.deletedAt) continue;\r\n if (f.createdAt.getTime() < Date.now() - sinceMs) continue;\r\n list.push(f);\r\n }\r\n list.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());\r\n return list.slice(0, opts.limit);\r\n }\r\n}\r\n"],"mappings":"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","names":[]}
@@ -0,0 +1,33 @@
1
+ /**
2
+ * 内存仓库(测试 / POC 场景)
3
+ *
4
+ * 用 Map 模拟两张表,无并发安全保证,仅用于单进程测试。
5
+ */
6
+ import type { MultipartRepository, FileRepository, MultipartRecord, FileRecord, CreateInitInput, CreateFileInput, AddPartOutcome } from './types';
7
+ export declare class InMemoryMultipartRepository implements MultipartRepository {
8
+ private store;
9
+ private idCounter;
10
+ private nextId;
11
+ createInit(input: CreateInitInput): Promise<MultipartRecord>;
12
+ findById(id: string): Promise<MultipartRecord | null>;
13
+ addPart(id: string, partNumber: number, partHash: string | null): Promise<AddPartOutcome>;
14
+ markCompleted(id: string, fileId: string): Promise<MultipartRecord>;
15
+ markAborted(id: string): Promise<MultipartRecord>;
16
+ markExpired(id: string): Promise<void>;
17
+ listExpired(limit: number): Promise<MultipartRecord[]>;
18
+ }
19
+ export declare class InMemoryFileRepository implements FileRepository {
20
+ private store;
21
+ private idCounter;
22
+ private nextId;
23
+ findByHash(hash: string): Promise<FileRecord | null>;
24
+ findDedupCandidate(hash: string, isPublic: boolean, userId: number | null): Promise<FileRecord | null>;
25
+ findById(id: string): Promise<FileRecord | null>;
26
+ create(input: CreateFileInput): Promise<FileRecord>;
27
+ softDelete(id: string, metaPatch?: Record<string, unknown>): Promise<void>;
28
+ listOrphans(opts: {
29
+ sinceDays: number;
30
+ limit: number;
31
+ }): Promise<FileRecord[]>;
32
+ }
33
+ //# sourceMappingURL=memory.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"memory.d.ts","sourceRoot":"","sources":["../../../../src/server/utils/repository/memory.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EACV,mBAAmB,EACnB,cAAc,EACd,eAAe,EACf,UAAU,EACV,eAAe,EACf,eAAe,EACf,cAAc,EACf,MAAM,SAAS,CAAC;AAOjB,qBAAa,2BAA4B,YAAW,mBAAmB;IACrE,OAAO,CAAC,KAAK,CAAsC;IACnD,OAAO,CAAC,SAAS,CAAK;IAEtB,OAAO,CAAC,MAAM;IAIR,UAAU,CAAC,KAAK,EAAE,eAAe,GAAG,OAAO,CAAC,eAAe,CAAC;IA4B5D,QAAQ,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,GAAG,IAAI,CAAC;IAIrD,OAAO,CACX,EAAE,EAAE,MAAM,EACV,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,MAAM,GAAG,IAAI,GACtB,OAAO,CAAC,cAAc,CAAC;IAqCpB,aAAa,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,CAAC;IAiBnE,WAAW,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,CAAC;IAiBjD,WAAW,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAYtC,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,EAAE,CAAC;CAc7D;AAMD,qBAAa,sBAAuB,YAAW,cAAc;IAC3D,OAAO,CAAC,KAAK,CAAiC;IAC9C,OAAO,CAAC,SAAS,CAAK;IAEtB,OAAO,CAAC,MAAM;IAIR,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC;IAOpD,kBAAkB,CACtB,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,OAAO,EACjB,MAAM,EAAE,MAAM,GAAG,IAAI,GACpB,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC;IAWvB,QAAQ,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC;IAIhD,MAAM,CAAC,KAAK,EAAE,eAAe,GAAG,OAAO,CAAC,UAAU,CAAC;IAyBnD,UAAU,CAAC,EAAE,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC;IAY1E,WAAW,CAAC,IAAI,EAAE;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,UAAU,EAAE,CAAC;CAWrF"}
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Prisma 实现: MultipartRepository + FileRepository
3
+ *
4
+ * 假定用户项目里有以下两张表(见 src/prisma/schema.prisma):
5
+ * - MultipartUpload(id, userId, filename, contentType, totalSize, partSize,
6
+ * totalParts, isPublic, status, receivedParts(JSON),
7
+ * partHashes(JSON), meta(JSON), expectedHash, fileId,
8
+ * createdAt, updatedAt, expiresAt, completedAt, abortedAt)
9
+ * - File(id, userId, filename, contentType, size, storageKey, isPublic,
10
+ * status, uploadId, totalParts, contentHash, meta, createdAt,
11
+ * updatedAt, deletedAt)
12
+ *
13
+ * 用户调用:
14
+ * import { PrismaClient } from '@prisma/client';
15
+ * import { PrismaMultipartRepository, PrismaFileRepository } from '...';
16
+ * const prisma = new PrismaClient();
17
+ * await app.register(secureUploadPlugin, {
18
+ * repository: { kind: 'prisma', prisma },
19
+ * ...
20
+ * });
21
+ */
22
+ type AnyClient = any;
23
+ import type { MultipartRepository, FileRepository, MultipartRecord, FileRecord, CreateInitInput, CreateFileInput, AddPartOutcome } from './types';
24
+ export declare class PrismaMultipartRepository implements MultipartRepository {
25
+ private prisma;
26
+ constructor(prisma: AnyClient);
27
+ createInit(input: CreateInitInput): Promise<MultipartRecord>;
28
+ findById(id: string): Promise<MultipartRecord | null>;
29
+ addPart(id: string, partNumber: number, partHash: string | null): Promise<AddPartOutcome>;
30
+ markCompleted(id: string, fileId: string): Promise<MultipartRecord>;
31
+ markAborted(id: string): Promise<MultipartRecord>;
32
+ markExpired(id: string): Promise<void>;
33
+ listExpired(limit: number): Promise<MultipartRecord[]>;
34
+ }
35
+ export declare class PrismaFileRepository implements FileRepository {
36
+ private prisma;
37
+ constructor(prisma: AnyClient);
38
+ findByHash(hash: string): Promise<FileRecord | null>;
39
+ findDedupCandidate(hash: string, isPublic: boolean, userId: number | null): Promise<FileRecord | null>;
40
+ findById(id: string): Promise<FileRecord | null>;
41
+ create(input: CreateFileInput): Promise<FileRecord>;
42
+ softDelete(id: string, metaPatch?: Record<string, unknown>): Promise<void>;
43
+ listOrphans(opts: {
44
+ sinceDays: number;
45
+ limit: number;
46
+ }): Promise<FileRecord[]>;
47
+ }
48
+ export {};
49
+ //# sourceMappingURL=prisma.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"prisma.d.ts","sourceRoot":"","sources":["../../../../src/server/utils/repository/prisma.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAGH,KAAK,SAAS,GAAG,GAAG,CAAC;AAErB,OAAO,KAAK,EACV,mBAAmB,EACnB,cAAc,EACd,eAAe,EACf,UAAU,EACV,eAAe,EACf,eAAe,EACf,cAAc,EACf,MAAM,SAAS,CAAC;AAmDjB,qBAAa,yBAA0B,YAAW,mBAAmB;IACvD,OAAO,CAAC,MAAM;gBAAN,MAAM,EAAE,SAAS;IAE/B,UAAU,CAAC,KAAK,EAAE,eAAe,GAAG,OAAO,CAAC,eAAe,CAAC;IAsB5D,QAAQ,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,GAAG,IAAI,CAAC;IAKrD,OAAO,CACX,EAAE,EAAE,MAAM,EACV,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,MAAM,GAAG,IAAI,GACtB,OAAO,CAAC,cAAc,CAAC;IA8CpB,aAAa,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,CAAC;IAkBnE,WAAW,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,CAAC;IAkBjD,WAAW,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAUtC,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,EAAE,CAAC;CAW7D;AAMD,qBAAa,oBAAqB,YAAW,cAAc;IAC7C,OAAO,CAAC,MAAM;gBAAN,MAAM,EAAE,SAAS;IAE/B,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC;IAOpD,kBAAkB,CACtB,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,OAAO,EACjB,MAAM,EAAE,MAAM,GAAG,IAAI,GACpB,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC;IAavB,QAAQ,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC;IAKhD,MAAM,CAAC,KAAK,EAAE,eAAe,GAAG,OAAO,CAAC,UAAU,CAAC;IAoBnD,UAAU,CAAC,EAAE,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC;IAgB1E,WAAW,CAAC,IAAI,EAAE;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,UAAU,EAAE,CAAC;CA8BrF"}
@@ -0,0 +1,9 @@
1
+ /**
2
+ * 状态枚举的 type alias
3
+ *
4
+ * 放在独立文件避免 types.ts 出现"const + type"的循环引用
5
+ */
6
+ import type { MultipartStatus, FileStatus } from './types';
7
+ export type MultipartStatusT = (typeof MultipartStatus)[keyof typeof MultipartStatus];
8
+ export type FileStatusT = (typeof FileStatus)[keyof typeof FileStatus];
9
+ //# sourceMappingURL=types-extra.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types-extra.d.ts","sourceRoot":"","sources":["../../../../src/server/utils/repository/types-extra.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAE3D,MAAM,MAAM,gBAAgB,GAAG,CAAC,OAAO,eAAe,CAAC,CAAC,MAAM,OAAO,eAAe,CAAC,CAAC;AACtF,MAAM,MAAM,WAAW,GAAG,CAAC,OAAO,UAAU,CAAC,CAAC,MAAM,OAAO,UAAU,CAAC,CAAC"}