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
|
@@ -0,0 +1,501 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/server/utils/repository/index.ts
|
|
21
|
+
var repository_exports = {};
|
|
22
|
+
__export(repository_exports, {
|
|
23
|
+
FileStatus: () => FileStatus,
|
|
24
|
+
InMemoryFileRepository: () => InMemoryFileRepository,
|
|
25
|
+
InMemoryMultipartRepository: () => InMemoryMultipartRepository,
|
|
26
|
+
MultipartStatus: () => MultipartStatus,
|
|
27
|
+
PrismaFileRepository: () => PrismaFileRepository,
|
|
28
|
+
PrismaMultipartRepository: () => PrismaMultipartRepository
|
|
29
|
+
});
|
|
30
|
+
module.exports = __toCommonJS(repository_exports);
|
|
31
|
+
|
|
32
|
+
// src/server/utils/repository/types.ts
|
|
33
|
+
var MultipartStatus = {
|
|
34
|
+
INIT: "INIT",
|
|
35
|
+
UPLOADING: "UPLOADING",
|
|
36
|
+
COMPLETED: "COMPLETED",
|
|
37
|
+
ABORTED: "ABORTED",
|
|
38
|
+
EXPIRED: "EXPIRED",
|
|
39
|
+
FAILED: "FAILED"
|
|
40
|
+
};
|
|
41
|
+
var FileStatus = {
|
|
42
|
+
PENDING: "PENDING",
|
|
43
|
+
UPLOADING: "UPLOADING",
|
|
44
|
+
COMPLETED: "COMPLETED",
|
|
45
|
+
FAILED: "FAILED"
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
// src/server/utils/repository/prisma.ts
|
|
49
|
+
function toMultipartRecord(row) {
|
|
50
|
+
return {
|
|
51
|
+
id: row.id,
|
|
52
|
+
userId: row.userId,
|
|
53
|
+
filename: row.filename,
|
|
54
|
+
contentType: row.contentType,
|
|
55
|
+
totalSize: row.totalSize,
|
|
56
|
+
partSize: row.partSize,
|
|
57
|
+
totalParts: row.totalParts,
|
|
58
|
+
isPublic: row.isPublic,
|
|
59
|
+
status: row.status,
|
|
60
|
+
receivedParts: Array.isArray(row.receivedParts) ? row.receivedParts : [],
|
|
61
|
+
partHashes: row.partHashes && typeof row.partHashes === "object" ? row.partHashes : {},
|
|
62
|
+
meta: row.meta && typeof row.meta === "object" ? row.meta : null,
|
|
63
|
+
expectedHash: row.expectedHash,
|
|
64
|
+
fileId: row.fileId,
|
|
65
|
+
createdAt: row.createdAt,
|
|
66
|
+
updatedAt: row.updatedAt,
|
|
67
|
+
expiresAt: row.expiresAt,
|
|
68
|
+
completedAt: row.completedAt,
|
|
69
|
+
abortedAt: row.abortedAt
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
function toFileRecord(row) {
|
|
73
|
+
return {
|
|
74
|
+
id: row.id,
|
|
75
|
+
userId: row.userId,
|
|
76
|
+
filename: row.filename,
|
|
77
|
+
contentType: row.contentType,
|
|
78
|
+
size: row.size,
|
|
79
|
+
storageKey: row.storageKey,
|
|
80
|
+
isPublic: row.isPublic,
|
|
81
|
+
status: row.status,
|
|
82
|
+
uploadId: row.uploadId,
|
|
83
|
+
totalParts: row.totalParts,
|
|
84
|
+
contentHash: row.contentHash,
|
|
85
|
+
meta: row.meta && typeof row.meta === "object" ? row.meta : null,
|
|
86
|
+
createdAt: row.createdAt,
|
|
87
|
+
updatedAt: row.updatedAt,
|
|
88
|
+
deletedAt: row.deletedAt
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
var PrismaMultipartRepository = class {
|
|
92
|
+
constructor(prisma) {
|
|
93
|
+
this.prisma = prisma;
|
|
94
|
+
}
|
|
95
|
+
prisma;
|
|
96
|
+
async createInit(input) {
|
|
97
|
+
const expiresAt = new Date(Date.now() + input.ttlSeconds * 1e3);
|
|
98
|
+
const row = await this.prisma.multipartUpload.create({
|
|
99
|
+
data: {
|
|
100
|
+
userId: input.userId,
|
|
101
|
+
filename: input.filename,
|
|
102
|
+
contentType: input.contentType,
|
|
103
|
+
totalSize: BigInt(input.totalSize),
|
|
104
|
+
partSize: input.partSize,
|
|
105
|
+
totalParts: input.totalParts,
|
|
106
|
+
isPublic: input.isPublic,
|
|
107
|
+
status: MultipartStatus.INIT,
|
|
108
|
+
receivedParts: [],
|
|
109
|
+
partHashes: {},
|
|
110
|
+
meta: input.meta ?? void 0,
|
|
111
|
+
expectedHash: input.expectedHash ?? null,
|
|
112
|
+
expiresAt
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
return toMultipartRecord(row);
|
|
116
|
+
}
|
|
117
|
+
async findById(id) {
|
|
118
|
+
const row = await this.prisma.multipartUpload.findUnique({ where: { id } });
|
|
119
|
+
return row ? toMultipartRecord(row) : null;
|
|
120
|
+
}
|
|
121
|
+
async addPart(id, partNumber, partHash) {
|
|
122
|
+
return await this.prisma.$transaction(async (tx) => {
|
|
123
|
+
const row = await tx.multipartUpload.findUnique({ where: { id } });
|
|
124
|
+
if (!row) throw new Error("NOT_FOUND");
|
|
125
|
+
const received = Array.isArray(row.receivedParts) ? row.receivedParts : [];
|
|
126
|
+
const hashes = row.partHashes && typeof row.partHashes === "object" ? row.partHashes : {};
|
|
127
|
+
if (received.includes(partNumber)) {
|
|
128
|
+
return {
|
|
129
|
+
updated: false,
|
|
130
|
+
duplicate: true,
|
|
131
|
+
existingHash: hashes[String(partNumber)] ?? null,
|
|
132
|
+
record: toMultipartRecord(row)
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
const next = [...received, partNumber].sort((a, b) => a - b);
|
|
136
|
+
const nextHashes = { ...hashes };
|
|
137
|
+
if (partHash) nextHashes[String(partNumber)] = partHash;
|
|
138
|
+
const newStatus = row.status === MultipartStatus.INIT ? MultipartStatus.UPLOADING : row.status;
|
|
139
|
+
if (row.status === MultipartStatus.COMPLETED || row.status === MultipartStatus.ABORTED || row.status === MultipartStatus.EXPIRED || row.status === MultipartStatus.FAILED) {
|
|
140
|
+
throw new Error("STATUS_INVALID");
|
|
141
|
+
}
|
|
142
|
+
const updated = await tx.multipartUpload.update({
|
|
143
|
+
where: { id },
|
|
144
|
+
data: {
|
|
145
|
+
receivedParts: next,
|
|
146
|
+
partHashes: nextHashes,
|
|
147
|
+
status: newStatus
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
return { updated: true, record: toMultipartRecord(updated) };
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
async markCompleted(id, fileId) {
|
|
154
|
+
const row = await this.prisma.multipartUpload.updateMany({
|
|
155
|
+
where: { id, status: { in: [MultipartStatus.INIT, MultipartStatus.UPLOADING] } },
|
|
156
|
+
data: {
|
|
157
|
+
status: MultipartStatus.COMPLETED,
|
|
158
|
+
fileId,
|
|
159
|
+
completedAt: /* @__PURE__ */ new Date()
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
if (row.count === 0) {
|
|
163
|
+
const cur = await this.prisma.multipartUpload.findUnique({ where: { id } });
|
|
164
|
+
if (!cur) throw new Error("NOT_FOUND");
|
|
165
|
+
throw new Error("STATUS_INVALID");
|
|
166
|
+
}
|
|
167
|
+
const fresh = await this.prisma.multipartUpload.findUnique({ where: { id } });
|
|
168
|
+
return toMultipartRecord(fresh);
|
|
169
|
+
}
|
|
170
|
+
async markAborted(id) {
|
|
171
|
+
const row = await this.prisma.multipartUpload.updateMany({
|
|
172
|
+
where: { id, status: { in: [MultipartStatus.INIT, MultipartStatus.UPLOADING] } },
|
|
173
|
+
data: {
|
|
174
|
+
status: MultipartStatus.ABORTED,
|
|
175
|
+
abortedAt: /* @__PURE__ */ new Date()
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
if (row.count === 0) {
|
|
179
|
+
const cur = await this.prisma.multipartUpload.findUnique({ where: { id } });
|
|
180
|
+
if (!cur) throw new Error("NOT_FOUND");
|
|
181
|
+
if (cur.status === MultipartStatus.COMPLETED) throw new Error("ALREADY_COMPLETED");
|
|
182
|
+
throw new Error("STATUS_INVALID");
|
|
183
|
+
}
|
|
184
|
+
const fresh = await this.prisma.multipartUpload.findUnique({ where: { id } });
|
|
185
|
+
return toMultipartRecord(fresh);
|
|
186
|
+
}
|
|
187
|
+
async markExpired(id) {
|
|
188
|
+
await this.prisma.multipartUpload.updateMany({
|
|
189
|
+
where: { id, status: { in: [MultipartStatus.INIT, MultipartStatus.UPLOADING] } },
|
|
190
|
+
data: {
|
|
191
|
+
status: MultipartStatus.EXPIRED,
|
|
192
|
+
abortedAt: /* @__PURE__ */ new Date()
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
async listExpired(limit) {
|
|
197
|
+
const rows = await this.prisma.multipartUpload.findMany({
|
|
198
|
+
where: {
|
|
199
|
+
status: { in: [MultipartStatus.INIT, MultipartStatus.UPLOADING] },
|
|
200
|
+
expiresAt: { lt: /* @__PURE__ */ new Date() }
|
|
201
|
+
},
|
|
202
|
+
take: limit,
|
|
203
|
+
orderBy: { expiresAt: "asc" }
|
|
204
|
+
});
|
|
205
|
+
return rows.map(toMultipartRecord);
|
|
206
|
+
}
|
|
207
|
+
};
|
|
208
|
+
var PrismaFileRepository = class {
|
|
209
|
+
constructor(prisma) {
|
|
210
|
+
this.prisma = prisma;
|
|
211
|
+
}
|
|
212
|
+
prisma;
|
|
213
|
+
async findByHash(hash) {
|
|
214
|
+
const row = await this.prisma.file.findFirst({
|
|
215
|
+
where: { contentHash: hash, deletedAt: null }
|
|
216
|
+
});
|
|
217
|
+
return row ? toFileRecord(row) : null;
|
|
218
|
+
}
|
|
219
|
+
async findDedupCandidate(hash, isPublic, userId) {
|
|
220
|
+
const row = await this.prisma.file.findFirst({
|
|
221
|
+
where: {
|
|
222
|
+
contentHash: hash,
|
|
223
|
+
deletedAt: null,
|
|
224
|
+
isPublic,
|
|
225
|
+
...isPublic ? {} : { userId }
|
|
226
|
+
},
|
|
227
|
+
orderBy: { createdAt: "asc" }
|
|
228
|
+
});
|
|
229
|
+
return row ? toFileRecord(row) : null;
|
|
230
|
+
}
|
|
231
|
+
async findById(id) {
|
|
232
|
+
const row = await this.prisma.file.findUnique({ where: { id } });
|
|
233
|
+
return row ? toFileRecord(row) : null;
|
|
234
|
+
}
|
|
235
|
+
async create(input) {
|
|
236
|
+
const row = await this.prisma.file.create({
|
|
237
|
+
data: {
|
|
238
|
+
id: input.id,
|
|
239
|
+
userId: input.userId,
|
|
240
|
+
filename: input.filename,
|
|
241
|
+
contentType: input.contentType,
|
|
242
|
+
size: BigInt(input.size),
|
|
243
|
+
storageKey: input.storageKey,
|
|
244
|
+
isPublic: input.isPublic,
|
|
245
|
+
status: FileStatus.COMPLETED,
|
|
246
|
+
uploadId: input.uploadId,
|
|
247
|
+
totalParts: input.totalParts,
|
|
248
|
+
contentHash: input.contentHash,
|
|
249
|
+
meta: input.meta ?? void 0
|
|
250
|
+
}
|
|
251
|
+
});
|
|
252
|
+
return toFileRecord(row);
|
|
253
|
+
}
|
|
254
|
+
async softDelete(id, metaPatch) {
|
|
255
|
+
if (metaPatch) {
|
|
256
|
+
const cur = await this.prisma.file.findUnique({ where: { id } });
|
|
257
|
+
const next = { ...cur?.meta ?? {}, ...metaPatch };
|
|
258
|
+
await this.prisma.file.update({
|
|
259
|
+
where: { id },
|
|
260
|
+
data: { deletedAt: /* @__PURE__ */ new Date(), meta: next }
|
|
261
|
+
});
|
|
262
|
+
} else {
|
|
263
|
+
await this.prisma.file.update({
|
|
264
|
+
where: { id },
|
|
265
|
+
data: { deletedAt: /* @__PURE__ */ new Date() }
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
async listOrphans(opts) {
|
|
270
|
+
const lookbackMs = opts.sinceDays * 24 * 3600 * 1e3;
|
|
271
|
+
const rows = await this.prisma.file.findMany({
|
|
272
|
+
where: {
|
|
273
|
+
deletedAt: null,
|
|
274
|
+
storageKey: { not: "" },
|
|
275
|
+
createdAt: { gt: new Date(Date.now() - lookbackMs) }
|
|
276
|
+
},
|
|
277
|
+
select: {
|
|
278
|
+
id: true,
|
|
279
|
+
userId: true,
|
|
280
|
+
filename: true,
|
|
281
|
+
contentType: true,
|
|
282
|
+
size: true,
|
|
283
|
+
storageKey: true,
|
|
284
|
+
isPublic: true,
|
|
285
|
+
status: true,
|
|
286
|
+
uploadId: true,
|
|
287
|
+
totalParts: true,
|
|
288
|
+
contentHash: true,
|
|
289
|
+
meta: true,
|
|
290
|
+
createdAt: true,
|
|
291
|
+
updatedAt: true,
|
|
292
|
+
deletedAt: true
|
|
293
|
+
},
|
|
294
|
+
take: opts.limit,
|
|
295
|
+
orderBy: { createdAt: "desc" }
|
|
296
|
+
});
|
|
297
|
+
return rows.map(toFileRecord);
|
|
298
|
+
}
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
// src/server/utils/repository/memory.ts
|
|
302
|
+
var InMemoryMultipartRepository = class {
|
|
303
|
+
store = /* @__PURE__ */ new Map();
|
|
304
|
+
idCounter = 0;
|
|
305
|
+
nextId() {
|
|
306
|
+
return `mp_${Date.now()}_${++this.idCounter}`;
|
|
307
|
+
}
|
|
308
|
+
async createInit(input) {
|
|
309
|
+
const id = this.nextId();
|
|
310
|
+
const now = /* @__PURE__ */ new Date();
|
|
311
|
+
const rec = {
|
|
312
|
+
id,
|
|
313
|
+
userId: input.userId,
|
|
314
|
+
filename: input.filename,
|
|
315
|
+
contentType: input.contentType,
|
|
316
|
+
totalSize: BigInt(input.totalSize),
|
|
317
|
+
partSize: input.partSize,
|
|
318
|
+
totalParts: input.totalParts,
|
|
319
|
+
isPublic: input.isPublic,
|
|
320
|
+
status: MultipartStatus.INIT,
|
|
321
|
+
receivedParts: [],
|
|
322
|
+
partHashes: {},
|
|
323
|
+
meta: input.meta ?? null,
|
|
324
|
+
expectedHash: input.expectedHash ?? null,
|
|
325
|
+
fileId: null,
|
|
326
|
+
createdAt: now,
|
|
327
|
+
updatedAt: now,
|
|
328
|
+
expiresAt: new Date(now.getTime() + input.ttlSeconds * 1e3),
|
|
329
|
+
completedAt: null,
|
|
330
|
+
abortedAt: null
|
|
331
|
+
};
|
|
332
|
+
this.store.set(id, rec);
|
|
333
|
+
return rec;
|
|
334
|
+
}
|
|
335
|
+
async findById(id) {
|
|
336
|
+
return this.store.get(id) ?? null;
|
|
337
|
+
}
|
|
338
|
+
async addPart(id, partNumber, partHash) {
|
|
339
|
+
const rec = this.store.get(id);
|
|
340
|
+
if (!rec) throw new Error("NOT_FOUND");
|
|
341
|
+
if (rec.status === MultipartStatus.COMPLETED || rec.status === MultipartStatus.ABORTED || rec.status === MultipartStatus.EXPIRED || rec.status === MultipartStatus.FAILED) {
|
|
342
|
+
throw new Error("STATUS_INVALID");
|
|
343
|
+
}
|
|
344
|
+
if (rec.receivedParts.includes(partNumber)) {
|
|
345
|
+
return {
|
|
346
|
+
updated: false,
|
|
347
|
+
duplicate: true,
|
|
348
|
+
existingHash: rec.partHashes[String(partNumber)] ?? null,
|
|
349
|
+
record: rec
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
const next = [...rec.receivedParts, partNumber].sort((a, b) => a - b);
|
|
353
|
+
const nextHashes = { ...rec.partHashes };
|
|
354
|
+
if (partHash) nextHashes[String(partNumber)] = partHash;
|
|
355
|
+
const updated = {
|
|
356
|
+
...rec,
|
|
357
|
+
receivedParts: next,
|
|
358
|
+
partHashes: nextHashes,
|
|
359
|
+
status: rec.status === MultipartStatus.INIT ? MultipartStatus.UPLOADING : rec.status,
|
|
360
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
361
|
+
};
|
|
362
|
+
this.store.set(id, updated);
|
|
363
|
+
return { updated: true, record: updated };
|
|
364
|
+
}
|
|
365
|
+
async markCompleted(id, fileId) {
|
|
366
|
+
const rec = this.store.get(id);
|
|
367
|
+
if (!rec) throw new Error("NOT_FOUND");
|
|
368
|
+
if (rec.status !== MultipartStatus.INIT && rec.status !== MultipartStatus.UPLOADING) {
|
|
369
|
+
throw new Error("STATUS_INVALID");
|
|
370
|
+
}
|
|
371
|
+
const updated = {
|
|
372
|
+
...rec,
|
|
373
|
+
status: MultipartStatus.COMPLETED,
|
|
374
|
+
fileId,
|
|
375
|
+
completedAt: /* @__PURE__ */ new Date(),
|
|
376
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
377
|
+
};
|
|
378
|
+
this.store.set(id, updated);
|
|
379
|
+
return updated;
|
|
380
|
+
}
|
|
381
|
+
async markAborted(id) {
|
|
382
|
+
const rec = this.store.get(id);
|
|
383
|
+
if (!rec) throw new Error("NOT_FOUND");
|
|
384
|
+
if (rec.status === MultipartStatus.COMPLETED) throw new Error("ALREADY_COMPLETED");
|
|
385
|
+
if (rec.status !== MultipartStatus.INIT && rec.status !== MultipartStatus.UPLOADING) {
|
|
386
|
+
throw new Error("STATUS_INVALID");
|
|
387
|
+
}
|
|
388
|
+
const updated = {
|
|
389
|
+
...rec,
|
|
390
|
+
status: MultipartStatus.ABORTED,
|
|
391
|
+
abortedAt: /* @__PURE__ */ new Date(),
|
|
392
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
393
|
+
};
|
|
394
|
+
this.store.set(id, updated);
|
|
395
|
+
return updated;
|
|
396
|
+
}
|
|
397
|
+
async markExpired(id) {
|
|
398
|
+
const rec = this.store.get(id);
|
|
399
|
+
if (!rec) return;
|
|
400
|
+
if (rec.status !== MultipartStatus.INIT && rec.status !== MultipartStatus.UPLOADING) return;
|
|
401
|
+
this.store.set(id, {
|
|
402
|
+
...rec,
|
|
403
|
+
status: MultipartStatus.EXPIRED,
|
|
404
|
+
abortedAt: /* @__PURE__ */ new Date(),
|
|
405
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
async listExpired(limit) {
|
|
409
|
+
const now = Date.now();
|
|
410
|
+
const list = [];
|
|
411
|
+
for (const rec of this.store.values()) {
|
|
412
|
+
if ((rec.status === MultipartStatus.INIT || rec.status === MultipartStatus.UPLOADING) && rec.expiresAt.getTime() < now) {
|
|
413
|
+
list.push(rec);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
list.sort((a, b) => a.expiresAt.getTime() - b.expiresAt.getTime());
|
|
417
|
+
return list.slice(0, limit);
|
|
418
|
+
}
|
|
419
|
+
};
|
|
420
|
+
var InMemoryFileRepository = class {
|
|
421
|
+
store = /* @__PURE__ */ new Map();
|
|
422
|
+
idCounter = 0;
|
|
423
|
+
nextId() {
|
|
424
|
+
return `f_${Date.now()}_${++this.idCounter}`;
|
|
425
|
+
}
|
|
426
|
+
async findByHash(hash) {
|
|
427
|
+
for (const f of this.store.values()) {
|
|
428
|
+
if (f.contentHash === hash && !f.deletedAt) return f;
|
|
429
|
+
}
|
|
430
|
+
return null;
|
|
431
|
+
}
|
|
432
|
+
async findDedupCandidate(hash, isPublic, userId) {
|
|
433
|
+
for (const f of this.store.values()) {
|
|
434
|
+
if (f.deletedAt) continue;
|
|
435
|
+
if (f.contentHash !== hash) continue;
|
|
436
|
+
if (f.isPublic !== isPublic) continue;
|
|
437
|
+
if (!isPublic && f.userId !== userId) continue;
|
|
438
|
+
return f;
|
|
439
|
+
}
|
|
440
|
+
return null;
|
|
441
|
+
}
|
|
442
|
+
async findById(id) {
|
|
443
|
+
return this.store.get(id) ?? null;
|
|
444
|
+
}
|
|
445
|
+
async create(input) {
|
|
446
|
+
let id = input.id;
|
|
447
|
+
if (!id) id = this.nextId();
|
|
448
|
+
const now = /* @__PURE__ */ new Date();
|
|
449
|
+
const rec = {
|
|
450
|
+
id,
|
|
451
|
+
userId: input.userId,
|
|
452
|
+
filename: input.filename,
|
|
453
|
+
contentType: input.contentType,
|
|
454
|
+
size: BigInt(input.size),
|
|
455
|
+
storageKey: input.storageKey,
|
|
456
|
+
isPublic: input.isPublic,
|
|
457
|
+
status: FileStatus.COMPLETED,
|
|
458
|
+
uploadId: input.uploadId,
|
|
459
|
+
totalParts: input.totalParts,
|
|
460
|
+
contentHash: input.contentHash,
|
|
461
|
+
meta: input.meta ?? null,
|
|
462
|
+
createdAt: now,
|
|
463
|
+
updatedAt: now,
|
|
464
|
+
deletedAt: null
|
|
465
|
+
};
|
|
466
|
+
this.store.set(id, rec);
|
|
467
|
+
return rec;
|
|
468
|
+
}
|
|
469
|
+
async softDelete(id, metaPatch) {
|
|
470
|
+
const rec = this.store.get(id);
|
|
471
|
+
if (!rec) return;
|
|
472
|
+
const next = {
|
|
473
|
+
...rec,
|
|
474
|
+
meta: metaPatch ? { ...rec.meta ?? {}, ...metaPatch } : rec.meta,
|
|
475
|
+
deletedAt: /* @__PURE__ */ new Date(),
|
|
476
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
477
|
+
};
|
|
478
|
+
this.store.set(id, next);
|
|
479
|
+
}
|
|
480
|
+
async listOrphans(opts) {
|
|
481
|
+
const sinceMs = opts.sinceDays * 24 * 3600 * 1e3;
|
|
482
|
+
const list = [];
|
|
483
|
+
for (const f of this.store.values()) {
|
|
484
|
+
if (f.deletedAt) continue;
|
|
485
|
+
if (f.createdAt.getTime() < Date.now() - sinceMs) continue;
|
|
486
|
+
list.push(f);
|
|
487
|
+
}
|
|
488
|
+
list.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
|
|
489
|
+
return list.slice(0, opts.limit);
|
|
490
|
+
}
|
|
491
|
+
};
|
|
492
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
493
|
+
0 && (module.exports = {
|
|
494
|
+
FileStatus,
|
|
495
|
+
InMemoryFileRepository,
|
|
496
|
+
InMemoryMultipartRepository,
|
|
497
|
+
MultipartStatus,
|
|
498
|
+
PrismaFileRepository,
|
|
499
|
+
PrismaMultipartRepository
|
|
500
|
+
});
|
|
501
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../src/server/utils/repository/index.ts","../../../../src/server/utils/repository/types.ts","../../../../src/server/utils/repository/prisma.ts","../../../../src/server/utils/repository/memory.ts"],"sourcesContent":["/**\r\n * Repository 统一导出\r\n *\r\n * 用法 1:用 Prisma(SDK 内置实现)\r\n * import { PrismaMultipartRepository, PrismaFileRepository } from '.../repository';\r\n * const multipart = new PrismaMultipartRepository(prisma);\r\n * const file = new PrismaFileRepository(prisma);\r\n * await app.register(secureUploadPlugin, {\r\n * repository: { kind: 'custom', multipart, file },\r\n * ...\r\n * });\r\n *\r\n * 用法 2:用内存实现(测试)\r\n * import { InMemoryMultipartRepository, InMemoryFileRepository } from '.../repository';\r\n *\r\n * 用法 3:完全自实现(Drizzle / TypeORM / Mongo / ...)\r\n * class MyMultipartRepo implements MultipartRepository { ... }\r\n * await app.register(secureUploadPlugin, {\r\n * repository: { kind: 'custom', multipart: new MyMultipartRepo(), file: new MyFileRepo() },\r\n * ...\r\n * });\r\n */\r\n\r\nexport {\r\n PrismaMultipartRepository,\r\n PrismaFileRepository,\r\n} from './prisma';\r\nexport {\r\n InMemoryMultipartRepository,\r\n InMemoryFileRepository,\r\n} from './memory';\r\nexport {\r\n MultipartStatus,\r\n FileStatus,\r\n type MultipartRecord,\r\n type FileRecord,\r\n type CreateInitInput,\r\n type CreateFileInput,\r\n type AddPartOutcome,\r\n type MultipartRepository,\r\n type FileRepository,\r\n} from './types';\r\nexport type { MultipartStatusT, FileStatusT } from './types-extra';\r\n","/**\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":[]}
|