@rpcbase/server 0.601.0 → 0.602.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/dist/fileReferences-Ca0tivFd.js +101 -0
- package/dist/fileReferences-Ca0tivFd.js.map +1 -0
- package/dist/{handler-Fk-5fOSm.js → handler-BUTqP40B.js} +2 -2
- package/dist/{handler-Fk-5fOSm.js.map → handler-BUTqP40B.js.map} +1 -1
- package/dist/{handler-C92_gJkX.js → handler-BgcIWWMc.js} +2 -2
- package/dist/{handler-C92_gJkX.js.map → handler-BgcIWWMc.js.map} +1 -1
- package/dist/{handler-CapJTGzc.js → handler-CqSlSCdd.js} +215 -57
- package/dist/handler-CqSlSCdd.js.map +1 -0
- package/dist/{handler-BqlcQ9sE.js → handler-LRLmxrsg.js} +69 -24
- package/dist/handler-LRLmxrsg.js.map +1 -0
- package/dist/notifications.js +1 -1
- package/dist/postProcessors-iffHvje4.js.map +1 -1
- package/dist/rts/index.js +1 -1
- package/dist/{schemas-B2fUvgYo.js → schemas-POKnVn0c.js} +58 -2
- package/dist/schemas-POKnVn0c.js.map +1 -0
- package/dist/{shared-DfrVDSp0.js → shared-Zt17vuAT.js} +54 -2
- package/dist/shared-Zt17vuAT.js.map +1 -0
- package/dist/uploads/api/file-uploads/fileReferences.d.ts +42 -0
- package/dist/uploads/api/file-uploads/fileReferences.d.ts.map +1 -0
- package/dist/uploads/api/file-uploads/handlers/completeUpload.d.ts.map +1 -1
- package/dist/uploads/api/file-uploads/handlers/initUpload.d.ts.map +1 -1
- package/dist/uploads/api/file-uploads/imageVariantsPostProcessor.d.ts.map +1 -1
- package/dist/uploads/api/file-uploads/index.d.ts +23 -0
- package/dist/uploads/api/file-uploads/index.d.ts.map +1 -1
- package/dist/uploads/api/file-uploads/postProcessors.d.ts +2 -0
- package/dist/uploads/api/file-uploads/postProcessors.d.ts.map +1 -1
- package/dist/uploads/api/file-uploads/shared.d.ts +21 -0
- package/dist/uploads/api/file-uploads/shared.d.ts.map +1 -1
- package/dist/uploads/api/files/handlers/deleteFile.d.ts.map +1 -1
- package/dist/uploads/api/files/handlers/getFile.d.ts.map +1 -1
- package/dist/uploads/worker.js +3 -2
- package/dist/uploads/worker.js.map +1 -1
- package/dist/uploads.js +2 -2
- package/package.json +1 -1
- package/dist/handler-BqlcQ9sE.js.map +0 -1
- package/dist/handler-CapJTGzc.js.map +0 -1
- package/dist/schemas-B2fUvgYo.js.map +0 -1
- package/dist/shared-DfrVDSp0.js.map +0 -1
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { r as enqueueUploadPostProcessors } from "./postProcessors-iffHvje4.js";
|
|
2
|
-
import { a as
|
|
2
|
+
import { _ as getUserId, a as ensureFilesystemUploadIndexes, b as resolveUploadAccessScope, c as getChunkSizeBytes, d as getModelCtx, f as getRawBodyLimitBytes, g as getUploadSessionAccessQuery, i as computeSha256Hex, m as getTenantId, n as buildUploadDeduplication, o as ensureUploadIndexes, p as getSessionTtlMs, r as buildUploadsAbility, s as getBucketName, t as buildUploadAccessMetadata, u as getMaxClientUploadBytesPerSecond, v as normalizeSha256Hex, x as toBufferPayload } from "./shared-Zt17vuAT.js";
|
|
3
3
|
import { t as convertHeifToWebpProcessor } from "./convertHeifToWebp-BT1n72DQ.js";
|
|
4
|
-
import { a as
|
|
4
|
+
import { a as number, c as string, i as literal, n as array, o as object, r as boolean, t as _enum } from "./schemas-POKnVn0c.js";
|
|
5
|
+
import { n as createUploadFileReference, t as claimUploadFileReference } from "./fileReferences-Ca0tivFd.js";
|
|
5
6
|
import { GridFSBucket, ObjectId } from "mongodb";
|
|
6
7
|
import { getTenantFilesystemDb, models } from "@rpcbase/db";
|
|
7
|
-
import { randomBytes } from "node:crypto";
|
|
8
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
8
9
|
import { JSDOM } from "jsdom";
|
|
9
10
|
import createDOMPurify from "dompurify";
|
|
10
11
|
//#region src/uploads/api/file-uploads/processors/sanitizeSvg.ts
|
|
@@ -109,6 +110,7 @@ var abortUploadStream = async (stream) => {
|
|
|
109
110
|
stream.destroy?.();
|
|
110
111
|
} catch {}
|
|
111
112
|
};
|
|
113
|
+
var isDuplicateKeyError = (error) => error !== null && typeof error === "object" && "code" in error && error.code === 11e3;
|
|
112
114
|
var completeUpload = async (_payload, ctx) => {
|
|
113
115
|
const tenantId = getTenantId(ctx);
|
|
114
116
|
if (!tenantId) {
|
|
@@ -146,7 +148,8 @@ var completeUpload = async (_payload, ctx) => {
|
|
|
146
148
|
}
|
|
147
149
|
if (existing.status === "done" && existing.fileId) return {
|
|
148
150
|
ok: true,
|
|
149
|
-
fileId: existing.fileId
|
|
151
|
+
fileId: existing.fileId,
|
|
152
|
+
...existing.deduplicated ? { deduplicated: true } : {}
|
|
150
153
|
};
|
|
151
154
|
const locked = await UploadSession.findOneAndUpdate({ $and: [
|
|
152
155
|
{ _id: uploadId },
|
|
@@ -176,8 +179,26 @@ var completeUpload = async (_payload, ctx) => {
|
|
|
176
179
|
error: "assembly_failed"
|
|
177
180
|
};
|
|
178
181
|
}
|
|
179
|
-
const
|
|
182
|
+
const bucketName = getBucketName();
|
|
183
|
+
const bucket = new GridFSBucket(nativeDb, { bucketName });
|
|
184
|
+
if (locked.deduplication) await ensureFilesystemUploadIndexes(nativeDb, bucketName);
|
|
180
185
|
const lockedUserId = typeof locked.userId === "string" ? locked.userId : void 0;
|
|
186
|
+
const accessMetadata = buildUploadAccessMetadata(locked);
|
|
187
|
+
const unclaimedDeduplication = locked.deduplication ? { deduplication: {
|
|
188
|
+
namespace: locked.deduplication.namespace,
|
|
189
|
+
algorithm: locked.deduplication.algorithm,
|
|
190
|
+
hash: locked.deduplication.hash,
|
|
191
|
+
pipelineVersion: locked.deduplication.pipelineVersion
|
|
192
|
+
} } : {};
|
|
193
|
+
const createFileMetadata = (storedMimeType, storedSize, additional = {}) => ({
|
|
194
|
+
uploadId,
|
|
195
|
+
tenantId,
|
|
196
|
+
mimeType: storedMimeType,
|
|
197
|
+
totalSize: storedSize,
|
|
198
|
+
...additional,
|
|
199
|
+
...accessMetadata,
|
|
200
|
+
...unclaimedDeduplication
|
|
201
|
+
});
|
|
181
202
|
const maxProcessorBytes = getMaxUploadProcessorBytes();
|
|
182
203
|
const shouldBufferForProcessing = locked.totalSize <= maxProcessorBytes;
|
|
183
204
|
const declaredSvg = locked.mimeType.trim().toLowerCase() === "image/svg+xml" || locked.filename.trim().toLowerCase().endsWith(".svg");
|
|
@@ -186,19 +207,13 @@ var completeUpload = async (_payload, ctx) => {
|
|
|
186
207
|
let finalMimeType = locked.mimeType;
|
|
187
208
|
let finalSize = locked.totalSize;
|
|
188
209
|
let inlineProcessors = [];
|
|
189
|
-
let finalMetadata =
|
|
190
|
-
uploadId,
|
|
191
|
-
tenantId,
|
|
192
|
-
mimeType: locked.mimeType,
|
|
193
|
-
totalSize: locked.totalSize,
|
|
194
|
-
...typeof locked.isPublic === "boolean" ? { isPublic: locked.isPublic } : {},
|
|
195
|
-
...typeof locked.ownerKeyHash === "string" ? { ownerKeyHash: locked.ownerKeyHash } : {},
|
|
196
|
-
...lockedUserId ? { userId: lockedUserId } : {}
|
|
197
|
-
};
|
|
210
|
+
let finalMetadata = createFileMetadata(locked.mimeType, locked.totalSize);
|
|
198
211
|
try {
|
|
199
212
|
if (!shouldBufferForProcessing && declaredSvg) throw new Error("svg_too_large");
|
|
200
213
|
const cursor = UploadChunk.find({ uploadId }).sort({ index: 1 }).cursor();
|
|
201
214
|
let expectedIndex = 0;
|
|
215
|
+
let sourceBytes = 0;
|
|
216
|
+
const sourceHasher = locked.deduplication ? createHash("sha256") : null;
|
|
202
217
|
const chunks = [];
|
|
203
218
|
let bufferedBytes = 0;
|
|
204
219
|
const pendingChunks = [];
|
|
@@ -208,6 +223,8 @@ var completeUpload = async (_payload, ctx) => {
|
|
|
208
223
|
for await (const chunkDoc of cursor) {
|
|
209
224
|
if (chunkDoc.index !== expectedIndex) throw new Error("missing_chunks");
|
|
210
225
|
const chunk = chunkDoc.data;
|
|
226
|
+
sourceBytes += chunk.length;
|
|
227
|
+
sourceHasher?.update(chunk);
|
|
211
228
|
if (shouldBufferForProcessing) {
|
|
212
229
|
chunks.push(chunk);
|
|
213
230
|
bufferedBytes += chunk.length;
|
|
@@ -229,15 +246,7 @@ var completeUpload = async (_payload, ctx) => {
|
|
|
229
246
|
sniff
|
|
230
247
|
});
|
|
231
248
|
if (processors.length) throw new Error(processors.some((processor) => processor.id === "sanitize-svg") ? "svg_too_large" : "processor_input_too_large");
|
|
232
|
-
finalMetadata =
|
|
233
|
-
uploadId,
|
|
234
|
-
tenantId,
|
|
235
|
-
mimeType: locked.mimeType,
|
|
236
|
-
totalSize: locked.totalSize,
|
|
237
|
-
...typeof locked.isPublic === "boolean" ? { isPublic: locked.isPublic } : {},
|
|
238
|
-
...typeof locked.ownerKeyHash === "string" ? { ownerKeyHash: locked.ownerKeyHash } : {},
|
|
239
|
-
...lockedUserId ? { userId: lockedUserId } : {}
|
|
240
|
-
};
|
|
249
|
+
finalMetadata = createFileMetadata(locked.mimeType, locked.totalSize);
|
|
241
250
|
uploadStream = bucket.openUploadStream(locked.filename, { metadata: finalMetadata });
|
|
242
251
|
for (const pending of pendingChunks) await writeToStream(uploadStream, pending);
|
|
243
252
|
pendingChunks.length = 0;
|
|
@@ -251,6 +260,8 @@ var completeUpload = async (_payload, ctx) => {
|
|
|
251
260
|
} catch {}
|
|
252
261
|
}
|
|
253
262
|
if (expectedIndex !== locked.chunksTotal) throw new Error("missing_chunks");
|
|
263
|
+
if (sourceBytes !== locked.totalSize) throw new Error("invalid_source_size");
|
|
264
|
+
if (sourceHasher && sourceHasher.digest("hex") !== locked.deduplication?.hash) throw new Error("content_hash_mismatch");
|
|
254
265
|
if (shouldBufferForProcessing) {
|
|
255
266
|
const { data: processed, filename: processedFilename, mimeType: processedMimeType, applied, metadata: processedMetadata } = await applyUploadProcessors(Buffer.concat(chunks, bufferedBytes), {
|
|
256
267
|
filename: locked.filename,
|
|
@@ -260,20 +271,13 @@ var completeUpload = async (_payload, ctx) => {
|
|
|
260
271
|
finalMimeType = processedMimeType;
|
|
261
272
|
finalSize = processed.length;
|
|
262
273
|
inlineProcessors = applied;
|
|
263
|
-
finalMetadata = {
|
|
264
|
-
uploadId,
|
|
265
|
-
tenantId,
|
|
266
|
-
mimeType: processedMimeType,
|
|
267
|
-
totalSize: processed.length,
|
|
274
|
+
finalMetadata = createFileMetadata(processedMimeType, processed.length, {
|
|
268
275
|
...applied.length ? {
|
|
269
276
|
processors: applied,
|
|
270
277
|
sourceTotalSize: locked.totalSize
|
|
271
278
|
} : {},
|
|
272
|
-
...processedMetadata
|
|
273
|
-
|
|
274
|
-
...typeof locked.ownerKeyHash === "string" ? { ownerKeyHash: locked.ownerKeyHash } : {},
|
|
275
|
-
...lockedUserId ? { userId: lockedUserId } : {}
|
|
276
|
-
};
|
|
279
|
+
...processedMetadata
|
|
280
|
+
});
|
|
277
281
|
uploadStream = bucket.openUploadStream(finalFilename, { metadata: finalMetadata });
|
|
278
282
|
const finished = waitForStreamFinished(uploadStream);
|
|
279
283
|
uploadStream.end(processed);
|
|
@@ -288,15 +292,7 @@ var completeUpload = async (_payload, ctx) => {
|
|
|
288
292
|
sniff
|
|
289
293
|
});
|
|
290
294
|
if (processors.length) throw new Error(processors.some((processor) => processor.id === "sanitize-svg") ? "svg_too_large" : "processor_input_too_large");
|
|
291
|
-
finalMetadata =
|
|
292
|
-
uploadId,
|
|
293
|
-
tenantId,
|
|
294
|
-
mimeType: locked.mimeType,
|
|
295
|
-
totalSize: locked.totalSize,
|
|
296
|
-
...typeof locked.isPublic === "boolean" ? { isPublic: locked.isPublic } : {},
|
|
297
|
-
...typeof locked.ownerKeyHash === "string" ? { ownerKeyHash: locked.ownerKeyHash } : {},
|
|
298
|
-
...lockedUserId ? { userId: lockedUserId } : {}
|
|
299
|
-
};
|
|
295
|
+
finalMetadata = createFileMetadata(locked.mimeType, locked.totalSize);
|
|
300
296
|
uploadStream = bucket.openUploadStream(locked.filename, { metadata: finalMetadata });
|
|
301
297
|
for (const pending of pendingChunks) await writeToStream(uploadStream, pending);
|
|
302
298
|
pendingChunks.length = 0;
|
|
@@ -305,27 +301,85 @@ var completeUpload = async (_payload, ctx) => {
|
|
|
305
301
|
uploadStream.end();
|
|
306
302
|
await finished;
|
|
307
303
|
}
|
|
308
|
-
const
|
|
309
|
-
if (!
|
|
304
|
+
const uploadedBlobId = String(uploadStream.id ?? "");
|
|
305
|
+
if (!uploadedBlobId) throw new Error("missing_file_id");
|
|
306
|
+
let resolvedBlobId = uploadedBlobId;
|
|
307
|
+
let deduplicated = false;
|
|
308
|
+
let returnedFileId = locked.deduplication ? null : uploadedBlobId;
|
|
309
|
+
if (locked.deduplication) {
|
|
310
|
+
const filesCollection = nativeDb.collection(`${bucketName}.files`);
|
|
311
|
+
const uploadedBlobObjectId = new ObjectId(uploadedBlobId);
|
|
312
|
+
const referenceOptions = {
|
|
313
|
+
tenantId,
|
|
314
|
+
bucketName,
|
|
315
|
+
filename: finalFilename,
|
|
316
|
+
mimeType: finalMimeType,
|
|
317
|
+
totalSize: finalSize,
|
|
318
|
+
accessScope: resolveUploadAccessScope({
|
|
319
|
+
accessScope: locked.accessScope,
|
|
320
|
+
isPublic: locked.isPublic
|
|
321
|
+
}),
|
|
322
|
+
...lockedUserId ? { userId: lockedUserId } : {},
|
|
323
|
+
...typeof locked.ownerKeyHash === "string" ? { ownerKeyHash: locked.ownerKeyHash } : {}
|
|
324
|
+
};
|
|
325
|
+
let lastDuplicateKeyError = null;
|
|
326
|
+
for (let attempt = 0; attempt < 3 && !returnedFileId; attempt += 1) {
|
|
327
|
+
try {
|
|
328
|
+
returnedFileId = await claimUploadFileReference({
|
|
329
|
+
...referenceOptions,
|
|
330
|
+
blobId: uploadedBlobObjectId,
|
|
331
|
+
deduplication: locked.deduplication
|
|
332
|
+
});
|
|
333
|
+
if (!returnedFileId) throw new Error("missing_file_id");
|
|
334
|
+
finalMetadata = {
|
|
335
|
+
...finalMetadata,
|
|
336
|
+
deduplication: locked.deduplication
|
|
337
|
+
};
|
|
338
|
+
break;
|
|
339
|
+
} catch (error) {
|
|
340
|
+
if (!isDuplicateKeyError(error)) throw error;
|
|
341
|
+
lastDuplicateKeyError = error;
|
|
342
|
+
}
|
|
343
|
+
const canonicalFile = await filesCollection.findOne({ "metadata.deduplication.key": locked.deduplication.key }, { projection: { _id: 1 } });
|
|
344
|
+
if (!(canonicalFile?._id instanceof ObjectId)) continue;
|
|
345
|
+
const canonicalReferenceId = await createUploadFileReference({
|
|
346
|
+
...referenceOptions,
|
|
347
|
+
blobId: canonicalFile._id,
|
|
348
|
+
deduplicationKey: locked.deduplication.key
|
|
349
|
+
});
|
|
350
|
+
if (!canonicalReferenceId) continue;
|
|
351
|
+
await bucket.delete(uploadedBlobObjectId);
|
|
352
|
+
resolvedBlobId = canonicalFile._id.toHexString();
|
|
353
|
+
returnedFileId = canonicalReferenceId;
|
|
354
|
+
deduplicated = true;
|
|
355
|
+
}
|
|
356
|
+
if (!returnedFileId) {
|
|
357
|
+
await bucket.delete(uploadedBlobObjectId);
|
|
358
|
+
if (lastDuplicateKeyError) throw lastDuplicateKeyError;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
if (!returnedFileId) throw new Error("canonical_file_missing");
|
|
310
362
|
await UploadSession.updateOne({ $and: [{ _id: uploadId }, getUploadSessionAccessQuery(ability, "update")] }, {
|
|
311
363
|
$set: {
|
|
312
364
|
status: "done",
|
|
313
|
-
fileId,
|
|
365
|
+
fileId: returnedFileId,
|
|
314
366
|
filename: finalFilename,
|
|
315
367
|
mimeType: finalMimeType,
|
|
316
|
-
totalSize: finalSize
|
|
368
|
+
totalSize: finalSize,
|
|
369
|
+
...deduplicated ? { deduplicated: true } : {}
|
|
317
370
|
},
|
|
318
371
|
$unset: { error: "" }
|
|
319
372
|
});
|
|
320
|
-
await enqueueUploadPostProcessors({
|
|
373
|
+
if (!deduplicated) await enqueueUploadPostProcessors({
|
|
321
374
|
tenantId,
|
|
322
375
|
uploadId,
|
|
323
|
-
fileId,
|
|
376
|
+
fileId: resolvedBlobId,
|
|
324
377
|
filename: finalFilename,
|
|
325
378
|
mimeType: finalMimeType,
|
|
326
379
|
clientMimeType: locked.mimeType,
|
|
327
380
|
totalSize: finalSize,
|
|
328
381
|
...typeof locked.isPublic === "boolean" ? { isPublic: locked.isPublic } : {},
|
|
382
|
+
...locked.accessScope ? { accessScope: locked.accessScope } : {},
|
|
329
383
|
...typeof locked.ownerKeyHash === "string" ? { ownerKeyHash: locked.ownerKeyHash } : {},
|
|
330
384
|
...lockedUserId ? { userId: lockedUserId } : {},
|
|
331
385
|
inlineProcessors,
|
|
@@ -334,7 +388,7 @@ var completeUpload = async (_payload, ctx) => {
|
|
|
334
388
|
console.error("Upload post processor enqueue failed", {
|
|
335
389
|
tenantId,
|
|
336
390
|
uploadId,
|
|
337
|
-
fileId,
|
|
391
|
+
fileId: resolvedBlobId,
|
|
338
392
|
error
|
|
339
393
|
});
|
|
340
394
|
});
|
|
@@ -343,7 +397,8 @@ var completeUpload = async (_payload, ctx) => {
|
|
|
343
397
|
} catch {}
|
|
344
398
|
return {
|
|
345
399
|
ok: true,
|
|
346
|
-
fileId
|
|
400
|
+
fileId: returnedFileId,
|
|
401
|
+
...deduplicated ? { deduplicated: true } : {}
|
|
347
402
|
};
|
|
348
403
|
} catch (error) {
|
|
349
404
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -356,6 +411,17 @@ var completeUpload = async (_payload, ctx) => {
|
|
|
356
411
|
error: "missing_chunks"
|
|
357
412
|
};
|
|
358
413
|
}
|
|
414
|
+
if (message === "content_hash_mismatch") {
|
|
415
|
+
await UploadSession.updateOne({ $and: [{ _id: uploadId }, getUploadSessionAccessQuery(ability, "update")] }, { $set: {
|
|
416
|
+
status: "error",
|
|
417
|
+
error: message
|
|
418
|
+
} });
|
|
419
|
+
ctx.res.status(400);
|
|
420
|
+
return {
|
|
421
|
+
ok: false,
|
|
422
|
+
error: message
|
|
423
|
+
};
|
|
424
|
+
}
|
|
359
425
|
if (message === "svg_too_large") {
|
|
360
426
|
await UploadSession.updateOne({ $and: [{ _id: uploadId }, getUploadSessionAccessQuery(ability, "update")] }, { $set: {
|
|
361
427
|
status: "error",
|
|
@@ -467,11 +533,29 @@ var InitRoute = "/api/rb/file-uploads";
|
|
|
467
533
|
var ChunkRoute = "/api/rb/file-uploads/:uploadId/chunks/:index";
|
|
468
534
|
var StatusRoute = "/api/rb/file-uploads/:uploadId/status";
|
|
469
535
|
var CompleteRoute = "/api/rb/file-uploads/:uploadId/complete";
|
|
536
|
+
var uploadAccessScopeSchema = _enum([
|
|
537
|
+
"owner",
|
|
538
|
+
"tenant",
|
|
539
|
+
"public"
|
|
540
|
+
]);
|
|
541
|
+
var uploadDeduplicationSchema = object({
|
|
542
|
+
namespace: string().trim().min(1).max(128).regex(/^[a-z0-9][a-z0-9._:-]*$/i),
|
|
543
|
+
algorithm: literal("sha256"),
|
|
544
|
+
hash: string().trim().regex(/^[a-f0-9]{64}$/i)
|
|
545
|
+
});
|
|
470
546
|
var initRequestSchema = object({
|
|
471
547
|
filename: string().min(1),
|
|
472
548
|
mimeType: string().min(1),
|
|
473
549
|
isPublic: boolean().optional(),
|
|
550
|
+
accessScope: uploadAccessScopeSchema.optional(),
|
|
551
|
+
deduplication: uploadDeduplicationSchema.optional(),
|
|
474
552
|
totalSize: number().int().min(1)
|
|
553
|
+
}).superRefine((value, ctx) => {
|
|
554
|
+
if (value.accessScope !== void 0 && value.isPublic !== void 0) ctx.addIssue({
|
|
555
|
+
code: "custom",
|
|
556
|
+
message: "accessScope and isPublic are mutually exclusive",
|
|
557
|
+
path: ["accessScope"]
|
|
558
|
+
});
|
|
475
559
|
});
|
|
476
560
|
object({
|
|
477
561
|
ok: boolean(),
|
|
@@ -479,7 +563,9 @@ object({
|
|
|
479
563
|
uploadId: string().optional(),
|
|
480
564
|
uploadKey: string().optional(),
|
|
481
565
|
chunkSize: number().int().optional(),
|
|
482
|
-
chunksTotal: number().int().optional()
|
|
566
|
+
chunksTotal: number().int().optional(),
|
|
567
|
+
fileId: string().optional(),
|
|
568
|
+
deduplicated: boolean().optional()
|
|
483
569
|
});
|
|
484
570
|
object({
|
|
485
571
|
ok: boolean(),
|
|
@@ -498,7 +584,8 @@ object({
|
|
|
498
584
|
object({
|
|
499
585
|
ok: boolean(),
|
|
500
586
|
error: string().optional(),
|
|
501
|
-
fileId: string().optional()
|
|
587
|
+
fileId: string().optional(),
|
|
588
|
+
deduplicated: boolean().optional()
|
|
502
589
|
});
|
|
503
590
|
//#endregion
|
|
504
591
|
//#region src/uploads/api/file-uploads/handlers/initUpload.ts
|
|
@@ -520,8 +607,79 @@ var initUpload = async (payload, ctx) => {
|
|
|
520
607
|
error: "invalid_payload"
|
|
521
608
|
};
|
|
522
609
|
}
|
|
610
|
+
const { filename, mimeType, totalSize, isPublic, deduplication: requestedDeduplication } = parsed.data;
|
|
611
|
+
const accessScope = resolveUploadAccessScope({
|
|
612
|
+
accessScope: parsed.data.accessScope,
|
|
613
|
+
isPublic
|
|
614
|
+
});
|
|
615
|
+
if (accessScope === "tenant" && !userId) {
|
|
616
|
+
ctx.res.status(401);
|
|
617
|
+
return {
|
|
618
|
+
ok: false,
|
|
619
|
+
error: "authentication_required"
|
|
620
|
+
};
|
|
621
|
+
}
|
|
622
|
+
const uploadKey = userId ? null : randomBytes(32).toString("base64url");
|
|
623
|
+
const ownerKeyHash = uploadKey ? computeSha256Hex(Buffer.from(uploadKey)) : void 0;
|
|
624
|
+
const deduplication = requestedDeduplication ? buildUploadDeduplication({
|
|
625
|
+
namespace: requestedDeduplication.namespace,
|
|
626
|
+
hash: requestedDeduplication.hash,
|
|
627
|
+
filename,
|
|
628
|
+
mimeType,
|
|
629
|
+
totalSize,
|
|
630
|
+
accessScope,
|
|
631
|
+
...userId ? { userId } : {},
|
|
632
|
+
...ownerKeyHash ? { ownerKeyHash } : {}
|
|
633
|
+
}) : null;
|
|
634
|
+
if (requestedDeduplication && !deduplication) {
|
|
635
|
+
ctx.res.status(400);
|
|
636
|
+
return {
|
|
637
|
+
ok: false,
|
|
638
|
+
error: "deduplication_unavailable"
|
|
639
|
+
};
|
|
640
|
+
}
|
|
641
|
+
if (deduplication) {
|
|
642
|
+
const nativeDb = (await getTenantFilesystemDb(tenantId)).db;
|
|
643
|
+
if (!nativeDb) {
|
|
644
|
+
ctx.res.status(500);
|
|
645
|
+
return {
|
|
646
|
+
ok: false,
|
|
647
|
+
error: "filesystem_db_unavailable"
|
|
648
|
+
};
|
|
649
|
+
}
|
|
650
|
+
const bucketName = getBucketName();
|
|
651
|
+
await ensureFilesystemUploadIndexes(nativeDb, bucketName);
|
|
652
|
+
const existingFile = await nativeDb.collection(`${bucketName}.files`).findOne({ "metadata.deduplication.key": deduplication.key }, { projection: {
|
|
653
|
+
_id: 1,
|
|
654
|
+
filename: 1,
|
|
655
|
+
length: 1,
|
|
656
|
+
metadata: 1
|
|
657
|
+
} });
|
|
658
|
+
if (existingFile?._id instanceof ObjectId) {
|
|
659
|
+
const existingMetadata = existingFile.metadata && typeof existingFile.metadata === "object" ? existingFile.metadata : {};
|
|
660
|
+
const existingMimeType = typeof existingMetadata.mimeType === "string" ? existingMetadata.mimeType : mimeType;
|
|
661
|
+
const existingSize = typeof existingFile.length === "number" ? existingFile.length : totalSize;
|
|
662
|
+
const fileId = await createUploadFileReference({
|
|
663
|
+
tenantId,
|
|
664
|
+
bucketName,
|
|
665
|
+
blobId: existingFile._id,
|
|
666
|
+
filename,
|
|
667
|
+
mimeType: existingMimeType,
|
|
668
|
+
totalSize: existingSize,
|
|
669
|
+
accessScope,
|
|
670
|
+
...userId ? { userId } : {},
|
|
671
|
+
...ownerKeyHash ? { ownerKeyHash } : {},
|
|
672
|
+
deduplicationKey: deduplication.key
|
|
673
|
+
});
|
|
674
|
+
if (fileId) return {
|
|
675
|
+
ok: true,
|
|
676
|
+
fileId,
|
|
677
|
+
deduplicated: true,
|
|
678
|
+
...uploadKey ? { uploadKey } : {}
|
|
679
|
+
};
|
|
680
|
+
}
|
|
681
|
+
}
|
|
523
682
|
const chunkSize = getChunkSizeBytes();
|
|
524
|
-
const { filename, mimeType, totalSize, isPublic } = parsed.data;
|
|
525
683
|
const chunksTotal = Math.ceil(totalSize / chunkSize);
|
|
526
684
|
const modelCtx = getModelCtx(ctx, tenantId, buildUploadsAbility(ctx, tenantId));
|
|
527
685
|
const [UploadSession, UploadChunk] = await Promise.all([models.get("RBUploadSession", modelCtx), models.get("RBUploadChunk", modelCtx)]);
|
|
@@ -529,15 +687,15 @@ var initUpload = async (payload, ctx) => {
|
|
|
529
687
|
const uploadId = new ObjectId().toString();
|
|
530
688
|
const now = Date.now();
|
|
531
689
|
const expiresAt = new Date(now + getSessionTtlMs());
|
|
532
|
-
const uploadKey = userId ? null : randomBytes(32).toString("base64url");
|
|
533
|
-
const ownerKeyHash = uploadKey ? computeSha256Hex(Buffer.from(uploadKey)) : void 0;
|
|
534
690
|
await UploadSession.create({
|
|
535
691
|
_id: uploadId,
|
|
536
692
|
...userId ? { userId } : {},
|
|
537
693
|
...ownerKeyHash ? { ownerKeyHash } : {},
|
|
538
694
|
filename,
|
|
539
695
|
mimeType,
|
|
540
|
-
|
|
696
|
+
isPublic: accessScope === "public",
|
|
697
|
+
accessScope,
|
|
698
|
+
...deduplication ? { deduplication } : {},
|
|
541
699
|
totalSize,
|
|
542
700
|
chunkSize,
|
|
543
701
|
chunksTotal,
|
|
@@ -792,4 +950,4 @@ var handler_default = (api) => {
|
|
|
792
950
|
//#endregion
|
|
793
951
|
export { handler_default as default };
|
|
794
952
|
|
|
795
|
-
//# sourceMappingURL=handler-
|
|
953
|
+
//# sourceMappingURL=handler-CqSlSCdd.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"handler-CqSlSCdd.js","names":["JSDOM","createDOMPurify","UploadFileProcessor","MAX_SVG_BYTES","window","DOMPurify","normalizeForSniff","raw","replace","trimStart","looksLikeSvgText","text","normalized","startsWith","test","looksLikeSvg","sniff","Buffer","toString","sanitizeSvg","svg","sanitize","USE_PROFILES","svgFilters","sanitizeSvgProcessor","id","maxBytes","match","process","data","mimeType","length","Error","svgText","sanitized","trim","sanitizedBuffer","from","convertHeifToWebpProcessor","sanitizeSvgProcessor","UploadFileProcessorContext","filename","clientMimeType","totalSize","sniff","Buffer","UploadFileProcessorResult","data","mimeType","metadata","Record","UploadFileProcessor","id","maxBytes","match","ctx","process","Promise","uploadProcessors","Object","freeze","getMaxUploadProcessorBytes","reduce","max","processor","Math","selectUploadProcessors","filter","applyUploadProcessors","Omit","applied","currentData","currentFilename","currentMimeType","processorCtx","length","Error","result","trim","assign","push","createHash","ApiHandler","getTenantFilesystemDb","models","GridFSBucket","ObjectId","Model","Uploads","claimUploadFileReference","createUploadFileReference","enqueueUploadPostProcessors","applyUploadProcessors","getMaxUploadProcessorBytes","selectUploadProcessors","SessionUser","UploadChunkDoc","UploadSessionDoc","buildUploadAccessMetadata","buildUploadsAbility","ensureFilesystemUploadIndexes","ensureUploadIndexes","getBucketName","getModelCtx","getUploadSessionAccessQuery","getTenantId","resolveUploadAccessScope","waitForStreamFinished","stream","NodeJS","WritableStream","Promise","resolve","reject","once","writeToStream","chunk","Buffer","ok","write","onDrain","cleanup","onError","error","off","on","abortUploadStream","abort","destroy","isDuplicateKeyError","code","completeUpload","Record","CompleteResponsePayload","_payload","ctx","tenantId","res","status","uploadId","String","req","params","trim","ability","modelCtx","UploadSession","UploadChunk","all","get","can","existing","findOne","$and","_id","lean","fileId","deduplicated","locked","findOneAndUpdate","$set","$unset","returnDocument","fsDb","nativeDb","db","updateOne","bucketName","bucket","deduplication","lockedUserId","userId","undefined","accessMetadata","unclaimedDeduplication","namespace","algorithm","hash","pipelineVersion","createFileMetadata","storedMimeType","storedSize","additional","mimeType","totalSize","maxProcessorBytes","shouldBufferForProcessing","declaredMimeType","toLowerCase","declaredSvg","filename","endsWith","uploadStream","finalFilename","finalMimeType","finalSize","inlineProcessors","finalMetadata","Error","cursor","find","sort","index","AsyncIterable","close","expectedIndex","sourceBytes","sourceHasher","chunks","bufferedBytes","pendingChunks","sniffParts","sniffBytes","chunkDoc","data","length","update","push","slice","subarray","Math","min","sniff","concat","processors","clientMimeType","some","processor","id","openUploadStream","metadata","pending","chunksTotal","digest","assembled","processed","processedFilename","processedMimeType","applied","processedMetadata","sourceTotalSize","finished","end","uploadedBlobId","resolvedBlobId","returnedFileId","filesCollection","collection","uploadedBlobObjectId","referenceOptions","accessScope","isPublic","ownerKeyHash","lastDuplicateKeyError","attempt","blobId","canonicalFile","key","projection","canonicalReferenceId","deduplicationKey","delete","toHexString","catch","console","deleteMany","message","startsWith","ApiHandler","models","Model","Uploads","SessionUser","UploadChunkDoc","UploadSessionDoc","buildUploadsAbility","getModelCtx","getUploadSessionAccessQuery","getTenantId","getStatus","Record","StatusResponsePayload","_payload","ctx","Promise","tenantId","res","status","ok","error","uploadId","String","req","params","trim","ability","modelCtx","UploadSession","UploadChunk","all","get","can","session","findOne","$and","_id","lean","receivedDocs","find","index","sort","received","Array","map","doc","filter","n","Number","isInteger","chunkSize","chunksTotal","fileId","z","InitRoute","ChunkRoute","StatusRoute","CompleteRoute","uploadAccessScopeSchema","enum","uploadDeduplicationSchema","object","namespace","string","trim","min","max","regex","algorithm","literal","hash","initRequestSchema","filename","mimeType","isPublic","boolean","optional","accessScope","deduplication","totalSize","number","int","superRefine","value","ctx","undefined","addIssue","code","message","path","InitRequestPayload","infer","initResponseSchema","ok","error","uploadId","uploadKey","chunkSize","chunksTotal","fileId","deduplicated","InitResponsePayload","statusResponseSchema","status","received","array","StatusResponsePayload","completeResponseSchema","CompleteResponsePayload","randomBytes","ApiHandler","getTenantFilesystemDb","models","ObjectId","Model","Uploads","createUploadFileReference","SessionUser","UploadChunkDoc","UploadSessionDoc","buildUploadDeduplication","buildUploadsAbility","computeSha256Hex","ensureFilesystemUploadIndexes","ensureUploadIndexes","getBucketName","getChunkSizeBytes","getModelCtx","getSessionTtlMs","getTenantId","getUserId","resolveUploadAccessScope","initUpload","InitRequestPayload","InitResponsePayload","payload","ctx","Promise","tenantId","res","status","ok","error","userId","parsed","initRequestSchema","safeParse","success","filename","mimeType","totalSize","isPublic","deduplication","requestedDeduplication","data","accessScope","uploadKey","toString","ownerKeyHash","Buffer","from","undefined","namespace","hash","fsDb","nativeDb","db","bucketName","existingFile","collection","findOne","key","projection","_id","length","metadata","existingMetadata","Record","existingMimeType","existingSize","fileId","blobId","deduplicationKey","deduplicated","chunkSize","chunksTotal","Math","ceil","ability","modelCtx","UploadSession","UploadChunk","all","get","uploadId","now","Date","expiresAt","create","createdAt","ApiHandler","models","Model","SessionUser","UploadChunkDoc","UploadSessionDoc","buildUploadsAbility","computeSha256Hex","ensureUploadIndexes","getModelCtx","getUploadSessionAccessQuery","getTenantId","normalizeSha256Hex","toBufferPayload","ChunkResponsePayload","ok","error","uploadChunk","Buffer","payload","ctx","Promise","tenantId","res","status","uploadId","String","req","params","trim","indexRaw","index","Number","isInteger","ability","modelCtx","UploadSession","UploadChunk","all","get","can","session","findOne","$and","_id","lean","chunksTotal","data","expectedSize","totalSize","chunkSize","length","checksumHeader","sha256","undefined","expectedSha256","updateOne","$set","size","expiresAt","$setOnInsert","createdAt","Date","upsert","rawBodyParser","limitBytes","maxClientBytesPerSecond","req","res","next","contentType","headers","String","includes","total","chunks","Buffer","done","paused","throttleTimeout","ReturnType","setTimeout","rateBytesPerSecond","cleanup","off","onData","onEnd","onError","onAborted","clearTimeout","finish","error","body","concat","chunk","buffer","isBuffer","from","length","destroy","status","json","ok","push","now","Date","clientKey","getClientKey","state","getClientRateState","waitMs","consumeRateBudget","pause","resume","err","Error","on","ClientRateState","tokens","lastRefillMs","lastSeenMs","MAX_BURST_SECONDS","STALE_CLIENT_MS","clientRateStates","Map","lastCleanupMs","rawClientIp","clientIp","trim","rawIp","ip","maybeCleanupStates","size","key","delete","capacity","existing","get","Math","min","set","bytes","elapsedMs","max","ceil","Api","completeUpload","getStatus","initUpload","uploadChunk","rawBodyParser","getChunkSizeBytes","getMaxClientUploadBytesPerSecond","getRawBodyLimitBytes","SessionUser","Uploads","api","chunkSizeBytes","use","InitRoute","limitBytes","maxClientBytesPerSecond","post","put","ChunkRoute","get","StatusRoute","CompleteRoute"],"sources":["../src/uploads/api/file-uploads/processors/sanitizeSvg.ts","../src/uploads/api/file-uploads/processors/index.ts","../src/uploads/api/file-uploads/handlers/completeUpload.ts","../src/uploads/api/file-uploads/handlers/getStatus.ts","../src/uploads/api/file-uploads/index.ts","../src/uploads/api/file-uploads/handlers/initUpload.ts","../src/uploads/api/file-uploads/handlers/uploadChunk.ts","../src/uploads/api/file-uploads/middleware/rawBodyParser.ts","../src/uploads/api/file-uploads/handler.ts"],"sourcesContent":["import { JSDOM } from \"jsdom\"\nimport createDOMPurify from \"dompurify\"\n\nimport type { UploadFileProcessor } from \"./index\"\n\n\nconst MAX_SVG_BYTES = 128 * 1024\n\nconst window = new JSDOM(\"\").window\nconst DOMPurify = createDOMPurify(window)\n\nconst normalizeForSniff = (raw: string): string => raw.replace(/^\\uFEFF/, \"\").trimStart()\n\nconst looksLikeSvgText = (text: string): boolean => {\n const normalized = normalizeForSniff(text)\n if (!normalized.startsWith(\"<\")) return false\n return /<svg(?:\\s|>)/i.test(normalized)\n}\n\nexport const looksLikeSvg = (sniff: Buffer): boolean => looksLikeSvgText(sniff.toString(\"utf8\"))\n\nexport const sanitizeSvg = (svg: string): string =>\n DOMPurify.sanitize(svg, {\n USE_PROFILES: { svg: true, svgFilters: true },\n })\n\nexport const sanitizeSvgProcessor: UploadFileProcessor = {\n id: \"sanitize-svg\",\n maxBytes: MAX_SVG_BYTES,\n match: ({ sniff }) => looksLikeSvg(sniff),\n process: (data): { data: Buffer; mimeType: string } => {\n if (data.length > MAX_SVG_BYTES) {\n throw new Error(\"svg_too_large\")\n }\n\n const svgText = data.toString(\"utf8\")\n if (!looksLikeSvgText(svgText)) {\n throw new Error(\"svg_invalid\")\n }\n\n const sanitized = sanitizeSvg(svgText)\n if (!sanitized.trim() || !looksLikeSvgText(sanitized)) {\n throw new Error(\"svg_sanitize_failed\")\n }\n\n const sanitizedBuffer = Buffer.from(sanitized, \"utf8\")\n if (sanitizedBuffer.length > MAX_SVG_BYTES) {\n throw new Error(\"svg_too_large\")\n }\n\n return { data: sanitizedBuffer, mimeType: \"image/svg+xml\" }\n },\n}\n\n","import { convertHeifToWebpProcessor } from \"./convertHeifToWebp\"\nimport { sanitizeSvgProcessor } from \"./sanitizeSvg\"\n\n\nexport type UploadFileProcessorContext = {\n filename: string\n clientMimeType: string\n totalSize: number\n sniff: Buffer\n}\n\nexport type UploadFileProcessorResult = {\n data: Buffer\n filename?: string\n mimeType?: string\n metadata?: Record<string, unknown>\n}\n\nexport type UploadFileProcessor = {\n id: string\n maxBytes: number\n match: (ctx: UploadFileProcessorContext) => boolean\n process: (data: Buffer, ctx: UploadFileProcessorContext) => Promise<UploadFileProcessorResult> | UploadFileProcessorResult\n}\n\nexport const uploadProcessors = Object.freeze([sanitizeSvgProcessor, convertHeifToWebpProcessor] satisfies UploadFileProcessor[])\n\nexport const getMaxUploadProcessorBytes = (): number =>\n uploadProcessors.reduce((max, processor) => Math.max(max, processor.maxBytes), 0)\n\nexport const selectUploadProcessors = (ctx: UploadFileProcessorContext): UploadFileProcessor[] =>\n uploadProcessors.filter((processor) => processor.match(ctx))\n\nexport const applyUploadProcessors = async (\n data: Buffer,\n ctx: Omit<UploadFileProcessorContext, \"sniff\" | \"totalSize\">,\n): Promise<{ data: Buffer; filename: string; mimeType: string; applied: string[]; metadata: Record<string, unknown> }> => {\n let currentData = data\n let currentFilename = ctx.filename\n let currentMimeType = ctx.clientMimeType\n const applied: string[] = []\n const metadata: Record<string, unknown> = {}\n\n for (const processor of uploadProcessors) {\n const processorCtx: UploadFileProcessorContext = {\n filename: currentFilename,\n clientMimeType: currentMimeType,\n totalSize: currentData.length,\n sniff: currentData,\n }\n\n if (!processor.match(processorCtx)) continue\n\n if (currentData.length > processor.maxBytes) {\n throw new Error(\"processor_input_too_large\")\n }\n\n const result = await processor.process(currentData, processorCtx)\n currentData = result.data\n if (typeof result.filename === \"string\" && result.filename.trim()) {\n currentFilename = result.filename.trim()\n }\n if (typeof result.mimeType === \"string\" && result.mimeType.trim()) {\n currentMimeType = result.mimeType.trim()\n }\n if (result.metadata && typeof result.metadata === \"object\") {\n Object.assign(metadata, result.metadata)\n }\n applied.push(processor.id)\n }\n\n return {\n data: currentData,\n filename: currentFilename,\n mimeType: currentMimeType,\n applied,\n metadata,\n }\n}\n","import { createHash } from \"node:crypto\"\n\nimport { ApiHandler } from \"@rpcbase/api\"\nimport { getTenantFilesystemDb, models } from \"@rpcbase/db\"\nimport { GridFSBucket, ObjectId } from \"mongodb\"\nimport type { Model } from \"mongoose\"\n\nimport * as Uploads from \"../index\"\nimport {\n claimUploadFileReference,\n createUploadFileReference,\n} from \"../fileReferences\"\nimport { enqueueUploadPostProcessors } from \"../postProcessors\"\nimport { applyUploadProcessors, getMaxUploadProcessorBytes, selectUploadProcessors } from \"../processors\"\nimport {\n type SessionUser,\n type UploadChunkDoc,\n type UploadSessionDoc,\n buildUploadAccessMetadata,\n buildUploadsAbility,\n ensureFilesystemUploadIndexes,\n ensureUploadIndexes,\n getBucketName,\n getModelCtx,\n getUploadSessionAccessQuery,\n getTenantId,\n resolveUploadAccessScope,\n} from \"../shared\"\n\n\nconst waitForStreamFinished = async (stream: NodeJS.WritableStream): Promise<void> => new Promise((resolve, reject) => {\n stream.once(\"finish\", resolve)\n stream.once(\"error\", reject)\n})\n\nconst writeToStream = async (stream: NodeJS.WritableStream, chunk: Buffer): Promise<void> => {\n const ok = stream.write(chunk)\n if (ok) return\n await new Promise<void>((resolve, reject) => {\n const onDrain = () => {\n cleanup()\n resolve()\n }\n\n const onError = (error: unknown) => {\n cleanup()\n reject(error)\n }\n\n const cleanup = () => {\n stream.off(\"drain\", onDrain)\n stream.off(\"error\", onError)\n }\n\n stream.on(\"drain\", onDrain)\n stream.on(\"error\", onError)\n })\n}\n\nconst abortUploadStream = async (stream: unknown): Promise<void> => {\n if (!stream) return\n if (typeof (stream as { abort?: unknown }).abort === \"function\") {\n try {\n await (stream as { abort: () => Promise<void> | void }).abort()\n return\n } catch {\n //\n }\n }\n try {\n ;(stream as { destroy?: () => void }).destroy?.()\n } catch {\n //\n }\n}\n\nconst isDuplicateKeyError = (error: unknown): boolean => (\n error !== null &&\n typeof error === \"object\" &&\n \"code\" in error &&\n (error as { code?: unknown }).code === 11000\n)\n\nexport const completeUpload: ApiHandler<Record<string, never>, Uploads.CompleteResponsePayload, SessionUser> = async (\n _payload,\n ctx,\n): Promise<Uploads.CompleteResponsePayload> => {\n const tenantId = getTenantId(ctx)\n if (!tenantId) {\n ctx.res.status(400)\n return { ok: false, error: \"tenant_missing\" }\n }\n\n const uploadId = String(ctx.req.params?.uploadId ?? \"\").trim()\n if (!uploadId) {\n ctx.res.status(400)\n return { ok: false, error: \"invalid_upload_id\" }\n }\n\n const ability = buildUploadsAbility(ctx, tenantId)\n const modelCtx = getModelCtx(ctx, tenantId, ability)\n\n const [UploadSession, UploadChunk] = await Promise.all([\n models.get(\"RBUploadSession\", modelCtx) as Promise<Model<UploadSessionDoc>>,\n models.get(\"RBUploadChunk\", modelCtx) as Promise<Model<UploadChunkDoc>>,\n ])\n\n if (!ability.can(\"update\", \"RBUploadSession\")) {\n ctx.res.status(401)\n return { ok: false, error: \"unauthorized\" }\n }\n\n const existing = await UploadSession.findOne({ $and: [{ _id: uploadId }, getUploadSessionAccessQuery(ability, \"read\")] }).lean()\n if (!existing) {\n ctx.res.status(404)\n return { ok: false, error: \"not_found\" }\n }\n\n if (existing.status === \"done\" && existing.fileId) {\n return {\n ok: true,\n fileId: existing.fileId,\n ...(existing.deduplicated ? { deduplicated: true } : {}),\n }\n }\n\n const locked = await UploadSession.findOneAndUpdate(\n { $and: [{ _id: uploadId }, { status: \"uploading\" }, getUploadSessionAccessQuery(ability, \"update\")] },\n { $set: { status: \"assembling\" }, $unset: { error: \"\" } },\n { returnDocument: \"after\" },\n ).lean()\n\n if (!locked) {\n ctx.res.status(409)\n return { ok: false, error: \"not_uploading\" }\n }\n\n await ensureUploadIndexes(UploadSession, UploadChunk)\n\n const fsDb = await getTenantFilesystemDb(tenantId)\n const nativeDb = fsDb.db\n if (!nativeDb) {\n await UploadSession.updateOne(\n { $and: [{ _id: uploadId }, getUploadSessionAccessQuery(ability, \"update\")] },\n { $set: { status: \"error\", error: \"filesystem_db_unavailable\" } },\n )\n ctx.res.status(500)\n return { ok: false, error: \"assembly_failed\" }\n }\n const bucketName = getBucketName()\n const bucket = new GridFSBucket(nativeDb, { bucketName })\n if (locked.deduplication) {\n await ensureFilesystemUploadIndexes(nativeDb, bucketName)\n }\n\n const lockedUserId = typeof locked.userId === \"string\" ? locked.userId : undefined\n const accessMetadata = buildUploadAccessMetadata(locked)\n const unclaimedDeduplication = locked.deduplication\n ? {\n deduplication: {\n namespace: locked.deduplication.namespace,\n algorithm: locked.deduplication.algorithm,\n hash: locked.deduplication.hash,\n pipelineVersion: locked.deduplication.pipelineVersion,\n },\n }\n : {}\n const createFileMetadata = (\n storedMimeType: string,\n storedSize: number,\n additional: Record<string, unknown> = {},\n ): Record<string, unknown> => ({\n uploadId,\n tenantId,\n mimeType: storedMimeType,\n totalSize: storedSize,\n ...additional,\n ...accessMetadata,\n ...unclaimedDeduplication,\n })\n const maxProcessorBytes = getMaxUploadProcessorBytes()\n const shouldBufferForProcessing = locked.totalSize <= maxProcessorBytes\n const declaredMimeType = locked.mimeType.trim().toLowerCase()\n const declaredSvg = declaredMimeType === \"image/svg+xml\" || locked.filename.trim().toLowerCase().endsWith(\".svg\")\n\n let uploadStream: NodeJS.WritableStream | null = null\n let finalFilename = locked.filename\n let finalMimeType = locked.mimeType\n let finalSize = locked.totalSize\n let inlineProcessors: string[] = []\n let finalMetadata = createFileMetadata(locked.mimeType, locked.totalSize)\n\n try {\n if (!shouldBufferForProcessing && declaredSvg) {\n throw new Error(\"svg_too_large\")\n }\n\n const cursor = UploadChunk.find({ uploadId }).sort({ index: 1 }).cursor() as unknown as AsyncIterable<UploadChunkDoc> & {\n close: () => Promise<void>\n }\n\n let expectedIndex = 0\n let sourceBytes = 0\n const sourceHasher = locked.deduplication ? createHash(\"sha256\") : null\n const chunks: Buffer[] = []\n let bufferedBytes = 0\n\n const pendingChunks: Buffer[] = []\n const sniffParts: Buffer[] = []\n let sniffBytes = 0\n\n try {\n for await (const chunkDoc of cursor) {\n if (chunkDoc.index !== expectedIndex) {\n throw new Error(\"missing_chunks\")\n }\n\n const chunk = chunkDoc.data\n sourceBytes += chunk.length\n sourceHasher?.update(chunk)\n\n if (shouldBufferForProcessing) {\n chunks.push(chunk)\n bufferedBytes += chunk.length\n } else if (!uploadStream) {\n pendingChunks.push(chunk)\n\n if (sniffBytes < maxProcessorBytes) {\n const slice = chunk.subarray(0, Math.min(chunk.length, maxProcessorBytes - sniffBytes))\n if (slice.length) {\n sniffParts.push(slice)\n sniffBytes += slice.length\n }\n }\n\n if (sniffBytes >= maxProcessorBytes) {\n const sniff = Buffer.concat(sniffParts, sniffBytes)\n const processors = selectUploadProcessors({\n filename: locked.filename,\n clientMimeType: locked.mimeType,\n totalSize: locked.totalSize,\n sniff,\n })\n\n if (processors.length) {\n throw new Error(processors.some((processor) => processor.id === \"sanitize-svg\") ? \"svg_too_large\" : \"processor_input_too_large\")\n }\n\n finalMetadata = createFileMetadata(locked.mimeType, locked.totalSize)\n uploadStream = bucket.openUploadStream(locked.filename, {\n metadata: finalMetadata,\n })\n\n for (const pending of pendingChunks) {\n await writeToStream(uploadStream, pending)\n }\n pendingChunks.length = 0\n }\n } else {\n await writeToStream(uploadStream, chunk)\n }\n\n expectedIndex += 1\n }\n } finally {\n try {\n await cursor.close()\n } catch {\n //\n }\n }\n\n if (expectedIndex !== locked.chunksTotal) {\n throw new Error(\"missing_chunks\")\n }\n\n if (sourceBytes !== locked.totalSize) {\n throw new Error(\"invalid_source_size\")\n }\n\n if (sourceHasher && sourceHasher.digest(\"hex\") !== locked.deduplication?.hash) {\n throw new Error(\"content_hash_mismatch\")\n }\n\n if (shouldBufferForProcessing) {\n const assembled = Buffer.concat(chunks, bufferedBytes)\n const {\n data: processed,\n filename: processedFilename,\n mimeType: processedMimeType,\n applied,\n metadata: processedMetadata,\n } = await applyUploadProcessors(assembled, {\n filename: locked.filename,\n clientMimeType: locked.mimeType,\n })\n\n finalFilename = processedFilename\n finalMimeType = processedMimeType\n finalSize = processed.length\n inlineProcessors = applied\n finalMetadata = createFileMetadata(processedMimeType, processed.length, {\n ...(applied.length ? { processors: applied, sourceTotalSize: locked.totalSize } : {}),\n ...processedMetadata,\n })\n uploadStream = bucket.openUploadStream(finalFilename, {\n metadata: finalMetadata,\n })\n\n const finished = waitForStreamFinished(uploadStream)\n uploadStream.end(processed)\n await finished\n } else {\n if (!uploadStream) {\n const sniff = Buffer.concat(sniffParts, sniffBytes)\n const processors = selectUploadProcessors({\n filename: locked.filename,\n clientMimeType: locked.mimeType,\n totalSize: locked.totalSize,\n sniff,\n })\n\n if (processors.length) {\n throw new Error(processors.some((processor) => processor.id === \"sanitize-svg\") ? \"svg_too_large\" : \"processor_input_too_large\")\n }\n\n finalMetadata = createFileMetadata(locked.mimeType, locked.totalSize)\n uploadStream = bucket.openUploadStream(locked.filename, {\n metadata: finalMetadata,\n })\n\n for (const pending of pendingChunks) {\n await writeToStream(uploadStream, pending)\n }\n pendingChunks.length = 0\n }\n\n const finished = waitForStreamFinished(uploadStream)\n uploadStream.end()\n await finished\n }\n\n const uploadedBlobId = String((uploadStream as unknown as { id?: unknown }).id ?? \"\")\n if (!uploadedBlobId) {\n throw new Error(\"missing_file_id\")\n }\n\n let resolvedBlobId = uploadedBlobId\n let deduplicated = false\n let returnedFileId: string | null = locked.deduplication ? null : uploadedBlobId\n\n if (locked.deduplication) {\n const filesCollection = nativeDb.collection(`${bucketName}.files`)\n const uploadedBlobObjectId = new ObjectId(uploadedBlobId)\n const referenceOptions = {\n tenantId,\n bucketName,\n filename: finalFilename,\n mimeType: finalMimeType,\n totalSize: finalSize,\n accessScope: resolveUploadAccessScope({\n accessScope: locked.accessScope,\n isPublic: locked.isPublic,\n }),\n ...(lockedUserId ? { userId: lockedUserId } : {}),\n ...(typeof locked.ownerKeyHash === \"string\" ? { ownerKeyHash: locked.ownerKeyHash } : {}),\n }\n let lastDuplicateKeyError: unknown = null\n\n for (let attempt = 0; attempt < 3 && !returnedFileId; attempt += 1) {\n try {\n returnedFileId = await claimUploadFileReference({\n ...referenceOptions,\n blobId: uploadedBlobObjectId,\n deduplication: locked.deduplication,\n })\n if (!returnedFileId) throw new Error(\"missing_file_id\")\n finalMetadata = {\n ...finalMetadata,\n deduplication: locked.deduplication,\n }\n break\n } catch (error) {\n if (!isDuplicateKeyError(error)) throw error\n lastDuplicateKeyError = error\n }\n\n const canonicalFile = await filesCollection.findOne(\n { \"metadata.deduplication.key\": locked.deduplication.key },\n { projection: { _id: 1 } },\n )\n if (!(canonicalFile?._id instanceof ObjectId)) continue\n\n const canonicalReferenceId = await createUploadFileReference({\n ...referenceOptions,\n blobId: canonicalFile._id,\n deduplicationKey: locked.deduplication.key,\n })\n if (!canonicalReferenceId) continue\n\n await bucket.delete(uploadedBlobObjectId)\n resolvedBlobId = canonicalFile._id.toHexString()\n returnedFileId = canonicalReferenceId\n deduplicated = true\n }\n\n if (!returnedFileId) {\n await bucket.delete(uploadedBlobObjectId)\n if (lastDuplicateKeyError) throw lastDuplicateKeyError\n }\n }\n if (!returnedFileId) {\n throw new Error(\"canonical_file_missing\")\n }\n\n await UploadSession.updateOne(\n { $and: [{ _id: uploadId }, getUploadSessionAccessQuery(ability, \"update\")] },\n {\n $set: {\n status: \"done\",\n fileId: returnedFileId,\n filename: finalFilename,\n mimeType: finalMimeType,\n totalSize: finalSize,\n ...(deduplicated ? { deduplicated: true } : {}),\n },\n $unset: { error: \"\" },\n },\n )\n\n if (!deduplicated) {\n await enqueueUploadPostProcessors({\n tenantId,\n uploadId,\n fileId: resolvedBlobId,\n filename: finalFilename,\n mimeType: finalMimeType,\n clientMimeType: locked.mimeType,\n totalSize: finalSize,\n ...(typeof locked.isPublic === \"boolean\" ? { isPublic: locked.isPublic } : {}),\n ...(locked.accessScope ? { accessScope: locked.accessScope } : {}),\n ...(typeof locked.ownerKeyHash === \"string\" ? { ownerKeyHash: locked.ownerKeyHash } : {}),\n ...(lockedUserId ? { userId: lockedUserId } : {}),\n inlineProcessors,\n metadata: finalMetadata,\n }).catch((error) => {\n console.error(\"Upload post processor enqueue failed\", {\n tenantId,\n uploadId,\n fileId: resolvedBlobId,\n error,\n })\n })\n }\n\n try {\n await UploadChunk.deleteMany({ uploadId })\n } catch {\n //\n }\n\n return {\n ok: true,\n fileId: returnedFileId,\n ...(deduplicated ? { deduplicated: true } : {}),\n }\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error)\n\n await abortUploadStream(uploadStream)\n\n if (message === \"missing_chunks\") {\n await UploadSession.updateOne(\n { $and: [{ _id: uploadId }, getUploadSessionAccessQuery(ability, \"update\")] },\n { $set: { status: \"uploading\" } },\n )\n ctx.res.status(409)\n return { ok: false, error: \"missing_chunks\" }\n }\n\n if (message === \"content_hash_mismatch\") {\n await UploadSession.updateOne(\n { $and: [{ _id: uploadId }, getUploadSessionAccessQuery(ability, \"update\")] },\n { $set: { status: \"error\", error: message } },\n )\n ctx.res.status(400)\n return { ok: false, error: message }\n }\n\n if (message === \"svg_too_large\") {\n await UploadSession.updateOne(\n { $and: [{ _id: uploadId }, getUploadSessionAccessQuery(ability, \"update\")] },\n { $set: { status: \"error\", error: message } },\n )\n ctx.res.status(413)\n return { ok: false, error: message }\n }\n\n if (message === \"heif_too_large\" || message === \"processor_input_too_large\") {\n await UploadSession.updateOne(\n { $and: [{ _id: uploadId }, getUploadSessionAccessQuery(ability, \"update\")] },\n { $set: { status: \"error\", error: message } },\n )\n ctx.res.status(413)\n return { ok: false, error: message }\n }\n\n if (message.startsWith(\"heif_decode_unsupported\")) {\n await UploadSession.updateOne(\n { $and: [{ _id: uploadId }, getUploadSessionAccessQuery(ability, \"update\")] },\n { $set: { status: \"error\", error: message } },\n )\n ctx.res.status(415)\n return { ok: false, error: message }\n }\n\n if (message === \"svg_invalid\" || message === \"svg_sanitize_failed\") {\n await UploadSession.updateOne(\n { $and: [{ _id: uploadId }, getUploadSessionAccessQuery(ability, \"update\")] },\n { $set: { status: \"error\", error: message } },\n )\n ctx.res.status(400)\n return { ok: false, error: message }\n }\n\n await UploadSession.updateOne(\n { $and: [{ _id: uploadId }, getUploadSessionAccessQuery(ability, \"update\")] },\n { $set: { status: \"error\", error: message } },\n )\n\n ctx.res.status(500)\n return { ok: false, error: \"assembly_failed\" }\n }\n}\n","import { ApiHandler } from \"@rpcbase/api\"\nimport { models } from \"@rpcbase/db\"\nimport type { Model } from \"mongoose\"\n\nimport * as Uploads from \"../index\"\nimport {\n type SessionUser,\n type UploadChunkDoc,\n type UploadSessionDoc,\n buildUploadsAbility,\n getModelCtx,\n getUploadSessionAccessQuery,\n getTenantId,\n} from \"../shared\"\n\n\nexport const getStatus: ApiHandler<Record<string, never>, Uploads.StatusResponsePayload, SessionUser> = async (\n _payload,\n ctx,\n): Promise<Uploads.StatusResponsePayload> => {\n const tenantId = getTenantId(ctx)\n if (!tenantId) {\n ctx.res.status(400)\n return { ok: false, error: \"tenant_missing\" }\n }\n\n const uploadId = String(ctx.req.params?.uploadId ?? \"\").trim()\n if (!uploadId) {\n ctx.res.status(400)\n return { ok: false, error: \"invalid_upload_id\" }\n }\n\n const ability = buildUploadsAbility(ctx, tenantId)\n const modelCtx = getModelCtx(ctx, tenantId, ability)\n\n const [UploadSession, UploadChunk] = await Promise.all([\n models.get(\"RBUploadSession\", modelCtx) as Promise<Model<UploadSessionDoc>>,\n models.get(\"RBUploadChunk\", modelCtx) as Promise<Model<UploadChunkDoc>>,\n ])\n\n if (!ability.can(\"read\", \"RBUploadSession\")) {\n ctx.res.status(401)\n return { ok: false, error: \"unauthorized\" }\n }\n\n const session = await UploadSession.findOne({ $and: [{ _id: uploadId }, getUploadSessionAccessQuery(ability, \"read\")] }).lean()\n if (!session) {\n ctx.res.status(404)\n return { ok: false, error: \"not_found\" }\n }\n\n const receivedDocs = await UploadChunk.find(\n { uploadId },\n { index: 1, _id: 0 },\n ).sort({ index: 1 }).lean()\n\n const received = (receivedDocs as unknown as Array<{ index?: unknown }>)\n .map((doc) => (typeof doc.index === \"number\" ? doc.index : -1))\n .filter((n) => Number.isInteger(n) && n >= 0)\n\n return {\n ok: true,\n status: session.status,\n chunkSize: session.chunkSize,\n chunksTotal: session.chunksTotal,\n received,\n ...(session.fileId ? { fileId: session.fileId } : {}),\n }\n}\n","import { z } from \"zod\"\n\n\nexport const InitRoute = \"/api/rb/file-uploads\"\nexport const ChunkRoute = \"/api/rb/file-uploads/:uploadId/chunks/:index\"\nexport const StatusRoute = \"/api/rb/file-uploads/:uploadId/status\"\nexport const CompleteRoute = \"/api/rb/file-uploads/:uploadId/complete\"\n\nexport const uploadAccessScopeSchema = z.enum([\"owner\", \"tenant\", \"public\"])\n\nexport const uploadDeduplicationSchema = z.object({\n namespace: z.string().trim().min(1).max(128).regex(/^[a-z0-9][a-z0-9._:-]*$/i),\n algorithm: z.literal(\"sha256\"),\n hash: z.string().trim().regex(/^[a-f0-9]{64}$/i),\n})\n\nexport const initRequestSchema = z.object({\n filename: z.string().min(1),\n mimeType: z.string().min(1),\n isPublic: z.boolean().optional(),\n accessScope: uploadAccessScopeSchema.optional(),\n deduplication: uploadDeduplicationSchema.optional(),\n totalSize: z.number().int().min(1),\n}).superRefine((value, ctx) => {\n if (value.accessScope !== undefined && value.isPublic !== undefined) {\n ctx.addIssue({\n code: \"custom\",\n message: \"accessScope and isPublic are mutually exclusive\",\n path: [\"accessScope\"],\n })\n }\n})\n\nexport type InitRequestPayload = z.infer<typeof initRequestSchema>\n\nexport const initResponseSchema = z.object({\n ok: z.boolean(),\n error: z.string().optional(),\n uploadId: z.string().optional(),\n uploadKey: z.string().optional(),\n chunkSize: z.number().int().optional(),\n chunksTotal: z.number().int().optional(),\n fileId: z.string().optional(),\n deduplicated: z.boolean().optional(),\n})\n\nexport type InitResponsePayload = z.infer<typeof initResponseSchema>\n\nexport const statusResponseSchema = z.object({\n ok: z.boolean(),\n error: z.string().optional(),\n status: z.enum([\"uploading\", \"assembling\", \"done\", \"error\"]).optional(),\n chunkSize: z.number().int().optional(),\n chunksTotal: z.number().int().optional(),\n received: z.array(z.number().int().min(0)).optional(),\n fileId: z.string().optional(),\n})\n\nexport type StatusResponsePayload = z.infer<typeof statusResponseSchema>\n\nexport const completeResponseSchema = z.object({\n ok: z.boolean(),\n error: z.string().optional(),\n fileId: z.string().optional(),\n deduplicated: z.boolean().optional(),\n})\n\nexport type CompleteResponsePayload = z.infer<typeof completeResponseSchema>\n","import { randomBytes } from \"node:crypto\"\n\nimport { ApiHandler } from \"@rpcbase/api\"\nimport { getTenantFilesystemDb, models } from \"@rpcbase/db\"\nimport { ObjectId } from \"mongodb\"\nimport type { Model } from \"mongoose\"\n\nimport * as Uploads from \"../index\"\nimport { createUploadFileReference } from \"../fileReferences\"\nimport {\n type SessionUser,\n type UploadChunkDoc,\n type UploadSessionDoc,\n buildUploadDeduplication,\n buildUploadsAbility,\n computeSha256Hex,\n ensureFilesystemUploadIndexes,\n ensureUploadIndexes,\n getBucketName,\n getChunkSizeBytes,\n getModelCtx,\n getSessionTtlMs,\n getTenantId,\n getUserId,\n resolveUploadAccessScope,\n} from \"../shared\"\n\n\nexport const initUpload: ApiHandler<Uploads.InitRequestPayload, Uploads.InitResponsePayload, SessionUser> = async (\n payload,\n ctx,\n): Promise<Uploads.InitResponsePayload> => {\n const tenantId = getTenantId(ctx)\n if (!tenantId) {\n ctx.res.status(400)\n return { ok: false, error: \"tenant_missing\" }\n }\n\n const userId = getUserId(ctx)\n\n const parsed = Uploads.initRequestSchema.safeParse(payload ?? {})\n if (!parsed.success) {\n ctx.res.status(400)\n return { ok: false, error: \"invalid_payload\" }\n }\n\n const { filename, mimeType, totalSize, isPublic, deduplication: requestedDeduplication } = parsed.data\n const accessScope = resolveUploadAccessScope({ accessScope: parsed.data.accessScope, isPublic })\n if (accessScope === \"tenant\" && !userId) {\n ctx.res.status(401)\n return { ok: false, error: \"authentication_required\" }\n }\n\n const uploadKey = userId ? null : randomBytes(32).toString(\"base64url\")\n const ownerKeyHash = uploadKey ? computeSha256Hex(Buffer.from(uploadKey)) : undefined\n const deduplication = requestedDeduplication\n ? buildUploadDeduplication({\n namespace: requestedDeduplication.namespace,\n hash: requestedDeduplication.hash,\n filename,\n mimeType,\n totalSize,\n accessScope,\n ...(userId ? { userId } : {}),\n ...(ownerKeyHash ? { ownerKeyHash } : {}),\n })\n : null\n\n if (requestedDeduplication && !deduplication) {\n ctx.res.status(400)\n return { ok: false, error: \"deduplication_unavailable\" }\n }\n\n if (deduplication) {\n const fsDb = await getTenantFilesystemDb(tenantId)\n const nativeDb = fsDb.db\n if (!nativeDb) {\n ctx.res.status(500)\n return { ok: false, error: \"filesystem_db_unavailable\" }\n }\n\n const bucketName = getBucketName()\n await ensureFilesystemUploadIndexes(nativeDb, bucketName)\n const existingFile = await nativeDb.collection(`${bucketName}.files`).findOne(\n { \"metadata.deduplication.key\": deduplication.key },\n { projection: { _id: 1, filename: 1, length: 1, metadata: 1 } },\n )\n if (existingFile?._id instanceof ObjectId) {\n const existingMetadata = existingFile.metadata && typeof existingFile.metadata === \"object\"\n ? existingFile.metadata as Record<string, unknown>\n : {}\n const existingMimeType = typeof existingMetadata.mimeType === \"string\"\n ? existingMetadata.mimeType\n : mimeType\n const existingSize = typeof existingFile.length === \"number\"\n ? existingFile.length\n : totalSize\n const fileId = await createUploadFileReference({\n tenantId,\n bucketName,\n blobId: existingFile._id,\n filename,\n mimeType: existingMimeType,\n totalSize: existingSize,\n accessScope,\n ...(userId ? { userId } : {}),\n ...(ownerKeyHash ? { ownerKeyHash } : {}),\n deduplicationKey: deduplication.key,\n })\n if (fileId) {\n return {\n ok: true,\n fileId,\n deduplicated: true,\n ...(uploadKey ? { uploadKey } : {}),\n }\n }\n }\n }\n\n const chunkSize = getChunkSizeBytes()\n const chunksTotal = Math.ceil(totalSize / chunkSize)\n\n const ability = buildUploadsAbility(ctx, tenantId)\n const modelCtx = getModelCtx(ctx, tenantId, ability)\n\n const [UploadSession, UploadChunk] = await Promise.all([\n models.get(\"RBUploadSession\", modelCtx) as Promise<Model<UploadSessionDoc>>,\n models.get(\"RBUploadChunk\", modelCtx) as Promise<Model<UploadChunkDoc>>,\n ])\n\n await ensureUploadIndexes(UploadSession, UploadChunk)\n\n const uploadId = new ObjectId().toString()\n const now = Date.now()\n const expiresAt = new Date(now + getSessionTtlMs())\n\n await UploadSession.create({\n _id: uploadId,\n ...(userId ? { userId } : {}),\n ...(ownerKeyHash ? { ownerKeyHash } : {}),\n filename,\n mimeType,\n isPublic: accessScope === \"public\",\n accessScope,\n ...(deduplication ? { deduplication } : {}),\n totalSize,\n chunkSize,\n chunksTotal,\n status: \"uploading\",\n createdAt: new Date(now),\n expiresAt,\n })\n\n return {\n ok: true,\n uploadId,\n chunkSize,\n chunksTotal,\n ...(uploadKey ? { uploadKey } : {}),\n }\n}\n","import { ApiHandler } from \"@rpcbase/api\"\nimport { models } from \"@rpcbase/db\"\nimport type { Model } from \"mongoose\"\n\nimport {\n type SessionUser,\n type UploadChunkDoc,\n type UploadSessionDoc,\n buildUploadsAbility,\n computeSha256Hex,\n ensureUploadIndexes,\n getModelCtx,\n getUploadSessionAccessQuery,\n getTenantId,\n normalizeSha256Hex,\n toBufferPayload,\n} from \"../shared\"\n\n\ntype ChunkResponsePayload = {\n ok: boolean\n error?: string\n}\n\nexport const uploadChunk: ApiHandler<Buffer, ChunkResponsePayload, SessionUser> = async (\n payload,\n ctx,\n): Promise<ChunkResponsePayload> => {\n const tenantId = getTenantId(ctx)\n if (!tenantId) {\n ctx.res.status(400)\n return { ok: false, error: \"tenant_missing\" }\n }\n\n const uploadId = String(ctx.req.params?.uploadId ?? \"\").trim()\n const indexRaw = String(ctx.req.params?.index ?? \"\").trim()\n const index = Number(indexRaw)\n\n if (!uploadId || !Number.isInteger(index) || index < 0) {\n ctx.res.status(400)\n return { ok: false, error: \"invalid_chunk_ref\" }\n }\n\n const ability = buildUploadsAbility(ctx, tenantId)\n const modelCtx = getModelCtx(ctx, tenantId, ability)\n\n const [UploadSession, UploadChunk] = await Promise.all([\n models.get(\"RBUploadSession\", modelCtx) as Promise<Model<UploadSessionDoc>>,\n models.get(\"RBUploadChunk\", modelCtx) as Promise<Model<UploadChunkDoc>>,\n ])\n\n if (!ability.can(\"update\", \"RBUploadSession\")) {\n ctx.res.status(401)\n return { ok: false, error: \"unauthorized\" }\n }\n\n const session = await UploadSession.findOne({ $and: [{ _id: uploadId }, getUploadSessionAccessQuery(ability, \"update\")] }).lean()\n if (!session) {\n ctx.res.status(404)\n return { ok: false, error: \"not_found\" }\n }\n\n if (session.status !== \"uploading\") {\n ctx.res.status(409)\n return { ok: false, error: \"not_uploading\" }\n }\n\n if (index >= session.chunksTotal) {\n ctx.res.status(400)\n return { ok: false, error: \"index_out_of_range\" }\n }\n\n const data = toBufferPayload(payload)\n if (!data) {\n ctx.res.status(400)\n return { ok: false, error: \"invalid_body\" }\n }\n\n const expectedSize = index === session.chunksTotal - 1\n ? session.totalSize - session.chunkSize * (session.chunksTotal - 1)\n : session.chunkSize\n\n if (data.length > expectedSize) {\n ctx.res.status(413)\n return { ok: false, error: \"chunk_too_large\" }\n }\n\n if (data.length !== expectedSize) {\n ctx.res.status(400)\n return { ok: false, error: \"invalid_chunk_size\" }\n }\n\n const checksumHeader = ctx.req.get(\"X-Chunk-SHA256\")\n const sha256 = checksumHeader ? computeSha256Hex(data) : undefined\n\n if (checksumHeader) {\n const expectedSha256 = normalizeSha256Hex(checksumHeader)\n if (sha256 !== expectedSha256) {\n ctx.res.status(400)\n return { ok: false, error: \"checksum_mismatch\" }\n }\n }\n\n await ensureUploadIndexes(UploadSession, UploadChunk)\n\n await UploadChunk.updateOne(\n { uploadId, index },\n {\n $set: {\n uploadId,\n index,\n data,\n size: data.length,\n sha256,\n expiresAt: session.expiresAt,\n },\n $setOnInsert: {\n createdAt: new Date(),\n },\n },\n { upsert: true },\n )\n\n ctx.res.status(204)\n return { ok: true }\n}\n","export const rawBodyParser = ({\n limitBytes,\n maxClientBytesPerSecond,\n}: {\n limitBytes: number\n maxClientBytesPerSecond?: number | null\n}) => {\n return (req: any, res: any, next: any) => {\n const contentType = typeof req?.headers?.[\"content-type\"] === \"string\"\n ? String(req.headers[\"content-type\"])\n : \"\"\n\n if (!contentType.includes(\"application/octet-stream\")) {\n next()\n return\n }\n\n let total = 0\n const chunks: Buffer[] = []\n let done = false\n let paused = false\n let throttleTimeout: ReturnType<typeof setTimeout> | null = null\n\n const rateBytesPerSecond = typeof maxClientBytesPerSecond === \"number\" && maxClientBytesPerSecond > 0\n ? maxClientBytesPerSecond\n : null\n\n const cleanup = () => {\n req.off(\"data\", onData)\n req.off(\"end\", onEnd)\n req.off(\"error\", onError)\n req.off(\"aborted\", onAborted)\n if (throttleTimeout) {\n clearTimeout(throttleTimeout)\n throttleTimeout = null\n }\n }\n\n const finish = (error?: unknown) => {\n if (done) return\n done = true\n\n cleanup()\n\n if (error) {\n next(error)\n return\n }\n\n req.body = Buffer.concat(chunks, total)\n next()\n }\n\n const onData = (chunk: any) => {\n if (done) return\n const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)\n total += buffer.length\n\n if (total > limitBytes) {\n done = true\n cleanup()\n req.destroy()\n res.status(413).json({ ok: false, error: \"chunk_too_large\" })\n return\n }\n\n chunks.push(buffer)\n\n if (!rateBytesPerSecond) return\n\n const now = Date.now()\n const clientKey = getClientKey(req)\n const state = getClientRateState(clientKey, rateBytesPerSecond, now)\n const waitMs = consumeRateBudget(state, buffer.length, rateBytesPerSecond, now)\n\n if (waitMs > 0 && !paused) {\n paused = true\n req.pause()\n throttleTimeout = setTimeout(() => {\n throttleTimeout = null\n paused = false\n if (done) return\n try {\n req.resume()\n } catch {\n //\n }\n }, waitMs)\n }\n }\n\n const onEnd = () => finish()\n const onError = (err: unknown) => finish(err)\n const onAborted = () => finish(new Error(\"request_aborted\"))\n\n req.on(\"data\", onData)\n req.on(\"end\", onEnd)\n req.on(\"error\", onError)\n req.on(\"aborted\", onAborted)\n }\n}\n\ntype ClientRateState = {\n tokens: number\n lastRefillMs: number\n lastSeenMs: number\n}\n\nconst MAX_BURST_SECONDS = 1\nconst STALE_CLIENT_MS = 15 * 60 * 1000\n\nconst clientRateStates = new Map<string, ClientRateState>()\nlet lastCleanupMs = 0\n\nconst getClientKey = (req: any): string => {\n const rawClientIp = typeof req?.clientIp === \"string\" ? req.clientIp : \"\"\n if (rawClientIp.trim()) return rawClientIp.trim()\n const rawIp = typeof req?.ip === \"string\" ? req.ip : \"\"\n return rawIp.trim() || \"unknown\"\n}\n\nconst maybeCleanupStates = (now: number) => {\n if (now - lastCleanupMs < 60_000) return\n lastCleanupMs = now\n\n if (clientRateStates.size < 2000) return\n\n for (const [key, state] of clientRateStates) {\n if (now - state.lastSeenMs > STALE_CLIENT_MS) {\n clientRateStates.delete(key)\n }\n }\n}\n\nconst getClientRateState = (key: string, rateBytesPerSecond: number, now: number): ClientRateState => {\n maybeCleanupStates(now)\n\n const capacity = rateBytesPerSecond * MAX_BURST_SECONDS\n const existing = clientRateStates.get(key)\n if (existing) {\n existing.lastSeenMs = now\n existing.tokens = Math.min(capacity, existing.tokens)\n return existing\n }\n\n const next: ClientRateState = {\n tokens: capacity,\n lastRefillMs: now,\n lastSeenMs: now,\n }\n clientRateStates.set(key, next)\n return next\n}\n\nconst consumeRateBudget = (\n state: ClientRateState,\n bytes: number,\n rateBytesPerSecond: number,\n now: number,\n): number => {\n const capacity = rateBytesPerSecond * MAX_BURST_SECONDS\n const elapsedMs = Math.max(0, now - state.lastRefillMs)\n\n if (elapsedMs > 0) {\n state.tokens = Math.min(capacity, state.tokens + (elapsedMs * rateBytesPerSecond) / 1000)\n state.lastRefillMs = now\n }\n\n state.tokens -= bytes\n\n if (state.tokens >= 0) return 0\n return Math.ceil((-state.tokens / rateBytesPerSecond) * 1000)\n}\n","import { Api } from \"@rpcbase/api\"\n\nimport { completeUpload } from \"./handlers/completeUpload\"\nimport { getStatus } from \"./handlers/getStatus\"\nimport { initUpload } from \"./handlers/initUpload\"\nimport { uploadChunk } from \"./handlers/uploadChunk\"\nimport { rawBodyParser } from \"./middleware/rawBodyParser\"\nimport { getChunkSizeBytes, getMaxClientUploadBytesPerSecond, getRawBodyLimitBytes, type SessionUser } from \"./shared\"\n\nimport * as Uploads from \"./index\"\n\n\nexport default (api: Api<SessionUser>) => {\n const chunkSizeBytes = getChunkSizeBytes()\n api.use(\n Uploads.InitRoute,\n rawBodyParser({\n limitBytes: getRawBodyLimitBytes(chunkSizeBytes),\n maxClientBytesPerSecond: getMaxClientUploadBytesPerSecond(),\n }),\n )\n\n api.post(Uploads.InitRoute, initUpload)\n api.put(Uploads.ChunkRoute, uploadChunk)\n api.get(Uploads.StatusRoute, getStatus)\n api.post(Uploads.CompleteRoute, completeUpload)\n}\n"],"mappings":"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"}
|