@cedarjs/uploads 7.0.1-next.314 → 7.0.1-next.466

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/README.md +11 -0
  2. package/dist/authenticator.d.ts +2 -2
  3. package/dist/authenticator.d.ts.map +1 -1
  4. package/dist/cleanupStaleUploads.d.ts +4 -3
  5. package/dist/cleanupStaleUploads.d.ts.map +1 -1
  6. package/dist/cleanupStaleUploads.js +8 -2
  7. package/dist/contentTypes.d.ts +74 -0
  8. package/dist/contentTypes.d.ts.map +1 -0
  9. package/dist/contentTypes.js +78 -0
  10. package/dist/deleteFile.d.ts +19 -4
  11. package/dist/deleteFile.d.ts.map +1 -1
  12. package/dist/deleteFile.js +49 -8
  13. package/dist/detectContentType.d.ts +168 -0
  14. package/dist/detectContentType.d.ts.map +1 -0
  15. package/dist/detectContentType.js +443 -0
  16. package/dist/errors.d.ts +1 -1
  17. package/dist/errors.d.ts.map +1 -1
  18. package/dist/errors.js +2 -1
  19. package/dist/fastify/plugin.d.ts +30 -2
  20. package/dist/fastify/plugin.d.ts.map +1 -1
  21. package/dist/fastify/plugin.js +38 -10
  22. package/dist/index.d.ts +6 -1
  23. package/dist/index.d.ts.map +1 -1
  24. package/dist/index.js +35 -0
  25. package/dist/keys.d.ts +3 -1
  26. package/dist/keys.d.ts.map +1 -1
  27. package/dist/keys.js +2 -1
  28. package/dist/profiles.d.ts +8 -2
  29. package/dist/profiles.d.ts.map +1 -1
  30. package/dist/profiles.js +5 -4
  31. package/dist/providers/s3.d.ts.map +1 -1
  32. package/dist/providers/s3.js +16 -0
  33. package/dist/services.d.ts +24 -6
  34. package/dist/services.d.ts.map +1 -1
  35. package/dist/services.js +45 -10
  36. package/dist/types.d.ts +116 -0
  37. package/dist/types.d.ts.map +1 -1
  38. package/dist/variants.d.ts +207 -0
  39. package/dist/variants.d.ts.map +1 -0
  40. package/dist/variants.js +498 -0
  41. package/dist/verifyStoredContentType.d.ts +19 -0
  42. package/dist/verifyStoredContentType.d.ts.map +1 -0
  43. package/dist/verifyStoredContentType.js +41 -0
  44. package/dist/web/createUppy.d.ts +19 -2
  45. package/dist/web/createUppy.d.ts.map +1 -1
  46. package/dist/web/createUppy.js +37 -15
  47. package/dist/web/hooks/useFsUpload.d.ts.map +1 -1
  48. package/dist/web/hooks/useFsUpload.js +21 -9
  49. package/dist/web/hooks/useS3Upload.d.ts.map +1 -1
  50. package/dist/web/hooks/useS3Upload.js +12 -5
  51. package/dist/web/hooks/useUploadToken.d.ts +27 -1
  52. package/dist/web/hooks/useUploadToken.d.ts.map +1 -1
  53. package/dist/web/hooks/useUploadToken.js +72 -3
  54. package/dist/web/hooks/useUppyUpload.d.ts.map +1 -1
  55. package/dist/web/hooks/useUppyUpload.js +2 -8
  56. package/dist/webhooks/s3.d.ts +17 -8
  57. package/dist/webhooks/s3.d.ts.map +1 -1
  58. package/dist/webhooks/s3.js +22 -3
  59. package/package.json +18 -18
@@ -0,0 +1,498 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { setTimeout as sleep } from "node:timers/promises";
3
+ import mime from "mime-types";
4
+ import { UploadError } from "./errors.js";
5
+ import { generateStorageKey } from "./keys.js";
6
+ import { DB_MAX_FILE_SIZE } from "./profiles.js";
7
+ import { toDataUri } from "./serialize.js";
8
+ import { resolveTarget } from "./targets.js";
9
+ const ONE_DAY = 24 * 60 * 60 * 1e3;
10
+ const TEN_MINUTES = 10 * 60 * 1e3;
11
+ const MIME_TYPE = /^[a-z0-9][a-z0-9!#$&^_.+-]*\/[a-z0-9][a-z0-9!#$&^_.+-]*$/i;
12
+ const VARIANT_NAME = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
13
+ function idOf(source) {
14
+ return typeof source === "string" ? source : source.id;
15
+ }
16
+ function isPrismaError(e, code) {
17
+ return typeof e === "object" && e !== null && "code" in e && e.code === code;
18
+ }
19
+ function defaultFilename(sourceFilename, name, mimeType) {
20
+ const base = sourceFilename.replace(/\.[^./\\]*$/, "") || "file";
21
+ const ext = mimeType ? mime.extension(mimeType.split(";")[0].trim()) : false;
22
+ return ext ? `${base}-${name}.${ext}` : `${base}-${name}`;
23
+ }
24
+ async function readBytes(db, targets, upload) {
25
+ const target = resolveTarget(targets, upload.target);
26
+ if (target.providerType === "db") {
27
+ const row = await db.upload.findUnique({ where: { id: upload.id } });
28
+ if (!row?.data) {
29
+ throw new UploadError(
30
+ "NOT_FOUND",
31
+ `Upload '${upload.id}' has no inline data.`
32
+ );
33
+ }
34
+ return Buffer.from(row.data);
35
+ }
36
+ if (!upload.storageKey) {
37
+ throw new UploadError(
38
+ "NOT_FOUND",
39
+ `Upload '${upload.id}' has no stored file.`
40
+ );
41
+ }
42
+ return Buffer.from(await target.read(upload.storageKey));
43
+ }
44
+ async function deleteVariantRow(db, targets, row) {
45
+ let current = row;
46
+ while (current) {
47
+ if (current.storageKey) {
48
+ await resolveTarget(targets, current.target).delete(current.storageKey);
49
+ }
50
+ const { count } = await db.upload.deleteMany({
51
+ where: {
52
+ id: current.id,
53
+ status: current.status,
54
+ tokenId: current.tokenId,
55
+ storageKey: current.storageKey
56
+ }
57
+ });
58
+ if (count === 1) {
59
+ return;
60
+ }
61
+ current = await db.upload.findUnique({
62
+ where: { id: current.id },
63
+ omit: { data: true }
64
+ });
65
+ }
66
+ }
67
+ async function deleteVariants(source, { db, targets }) {
68
+ const rows = await db.upload.findMany({
69
+ where: { sourceId: idOf(source) },
70
+ omit: { data: true }
71
+ });
72
+ for (const row of rows) {
73
+ await deleteVariantRow(db, targets, row);
74
+ }
75
+ return rows.length;
76
+ }
77
+ function defineUploadVariants(options) {
78
+ const {
79
+ db,
80
+ targets,
81
+ target: defaultTarget,
82
+ retryFailedAfter = ONE_DAY,
83
+ claimTimeout = TEN_MINUTES,
84
+ pollInterval = 250,
85
+ onError = (error, { sourceId, variant }) => console.error(
86
+ `[cedar uploads] could not make variant '${variant}' of ${sourceId}:`,
87
+ error
88
+ )
89
+ } = options;
90
+ const definitions = /* @__PURE__ */ new Map();
91
+ for (const [name, value] of Object.entries(options.variants)) {
92
+ if (!VARIANT_NAME.test(name)) {
93
+ throw new UploadError(
94
+ "CONFIGURATION",
95
+ `Invalid variant name '${name}'. Use letters, digits, '.', '_' and '-', starting with a letter or digit.`
96
+ );
97
+ }
98
+ const definition = typeof value === "function" ? { generate: value } : value;
99
+ const target = definition.target ?? defaultTarget;
100
+ if (!Object.hasOwn(targets, target.name) || targets[target.name] !== target) {
101
+ throw new UploadError(
102
+ "CONFIGURATION",
103
+ `Variant '${name}' uses a storage target that isn't in \`targets\`.`
104
+ );
105
+ }
106
+ definitions.set(name, { generate: definition.generate, target });
107
+ }
108
+ const names = [...definitions.keys()];
109
+ const inFlight = /* @__PURE__ */ new Map();
110
+ const waitingFor = /* @__PURE__ */ new Map();
111
+ const keyOf = (sourceId, name) => `${sourceId}\0${name}`;
112
+ const isWaitingFor = (from, to) => {
113
+ const seen = /* @__PURE__ */ new Set();
114
+ const stack = [from];
115
+ for (let key = stack.pop(); key !== void 0; key = stack.pop()) {
116
+ if (key === to) {
117
+ return true;
118
+ }
119
+ if (!seen.has(key)) {
120
+ seen.add(key);
121
+ stack.push(...waitingFor.get(key) ?? []);
122
+ }
123
+ }
124
+ return false;
125
+ };
126
+ const obtainFor = async (waiter, sourceId, name, chain) => {
127
+ const key = keyOf(sourceId, name);
128
+ if (!chain.includes(name) && inFlight.has(key) && isWaitingFor(key, waiter)) {
129
+ throw new UploadError(
130
+ "CONFIGURATION",
131
+ `Variant '${name}' and the variant reading it depend on each other.`
132
+ );
133
+ }
134
+ const waits = waitingFor.get(waiter) ?? [];
135
+ waitingFor.set(waiter, [...waits, key]);
136
+ try {
137
+ return await obtain(sourceId, name, chain);
138
+ } finally {
139
+ const rest = waitingFor.get(waiter) ?? [];
140
+ rest.splice(rest.indexOf(key), 1);
141
+ if (rest.length === 0) {
142
+ waitingFor.delete(waiter);
143
+ }
144
+ }
145
+ };
146
+ const definitionOf = (name) => {
147
+ const definition = definitions.get(name);
148
+ if (!definition) {
149
+ throw new UploadError(
150
+ "CONFIGURATION",
151
+ `Unknown upload variant '${name}'. Configured variants: ${names.join(", ") || "(none)"}.`
152
+ );
153
+ }
154
+ return definition;
155
+ };
156
+ const loadSource = async (sourceId) => {
157
+ const source = await db.upload.findUnique({
158
+ where: { id: sourceId },
159
+ omit: { data: true }
160
+ });
161
+ if (source?.status !== "completed") {
162
+ throw new UploadError(
163
+ "NOT_FOUND",
164
+ `Upload '${sourceId}' does not exist or is not completed, so it cannot have variants.`
165
+ );
166
+ }
167
+ if (source.sourceId) {
168
+ throw new UploadError(
169
+ "NOT_SUPPORTED",
170
+ `Upload '${sourceId}' is itself a variant. Variants are made from original uploads only; use \`readVariant()\` in a generator to make one variant from another.`
171
+ );
172
+ }
173
+ return source;
174
+ };
175
+ const findRow = (sourceId, name) => db.upload.findFirst({
176
+ where: { sourceId, variant: name },
177
+ omit: { data: true }
178
+ });
179
+ const make = async (source, rowId, claim, name, chain) => {
180
+ const definition = definitionOf(name);
181
+ const target = definition.target;
182
+ const ours = { id: rowId, status: "pending", tokenId: claim };
183
+ const fail = async (error) => {
184
+ if (error !== void 0) {
185
+ onError(error, { sourceId: source.id, variant: name });
186
+ }
187
+ await db.upload.updateMany({ where: ours, data: { status: "failed" } });
188
+ return null;
189
+ };
190
+ let file;
191
+ try {
192
+ file = await definition.generate({
193
+ source,
194
+ variant: name,
195
+ readSource: () => readBytes(db, targets, source),
196
+ readVariant: async (other) => {
197
+ const made = await obtainFor(
198
+ keyOf(source.id, name),
199
+ source.id,
200
+ other,
201
+ [...chain, name]
202
+ );
203
+ return made ? readBytes(db, targets, made) : null;
204
+ }
205
+ });
206
+ } catch (e) {
207
+ return fail(e);
208
+ }
209
+ if (!file) {
210
+ return fail();
211
+ }
212
+ if (!MIME_TYPE.test(file.mimeType)) {
213
+ return fail(
214
+ new UploadError(
215
+ "MIME_TYPE_NOT_ALLOWED",
216
+ `Variant '${name}' returned '${file.mimeType}', which is not a valid MIME type.`
217
+ )
218
+ );
219
+ }
220
+ const isDbTarget = target.providerType === "db";
221
+ if (isDbTarget && file.data.byteLength > DB_MAX_FILE_SIZE) {
222
+ return fail(
223
+ new UploadError(
224
+ "FILE_TOO_LARGE",
225
+ `Variant '${name}' is ${file.data.byteLength} bytes, which exceeds the ${DB_MAX_FILE_SIZE} byte limit for target '${target.name}'.`
226
+ )
227
+ );
228
+ }
229
+ const storageKey = isDbTarget ? null : generateStorageKey(file.mimeType);
230
+ if (storageKey) {
231
+ try {
232
+ await target.write(storageKey, file.data, {
233
+ contentType: file.mimeType
234
+ });
235
+ } catch (e) {
236
+ await target.delete(storageKey).catch(() => void 0);
237
+ await db.upload.deleteMany({ where: ours });
238
+ throw e;
239
+ }
240
+ }
241
+ const { count } = await db.upload.updateMany({
242
+ where: ours,
243
+ data: {
244
+ status: "completed",
245
+ // A retry or a takeover writes to the variant's current target,
246
+ // which is not necessarily the one the row was created for
247
+ target: target.name,
248
+ storageKey,
249
+ size: BigInt(file.data.byteLength),
250
+ mimeType: file.mimeType,
251
+ filename: file.filename ?? defaultFilename(source.filename, name, file.mimeType),
252
+ // Copying gives Prisma the plain-ArrayBuffer-backed array its
253
+ // `Bytes` input requires
254
+ data: isDbTarget ? new Uint8Array(file.data) : null,
255
+ tokenId: null
256
+ }
257
+ }).catch(async (e) => {
258
+ if (storageKey) {
259
+ await target.delete(storageKey).catch(() => void 0);
260
+ }
261
+ await db.upload.deleteMany({ where: ours }).catch(() => void 0);
262
+ throw e;
263
+ });
264
+ if (count !== 1) {
265
+ if (storageKey) {
266
+ await target.delete(storageKey).catch(() => void 0);
267
+ }
268
+ const current = await findRow(source.id, name);
269
+ return current?.status === "completed" ? current : null;
270
+ }
271
+ return db.upload.findUnique({ where: { id: rowId }, omit: { data: true } });
272
+ };
273
+ const obtainUncached = async (sourceId, name, chain) => {
274
+ const definition = definitionOf(name);
275
+ const source = await loadSource(sourceId);
276
+ for (; ; ) {
277
+ const row = await findRow(sourceId, name);
278
+ const now = Date.now();
279
+ const claim = randomUUID();
280
+ if (!row) {
281
+ let created;
282
+ try {
283
+ created = await db.upload.create({
284
+ data: {
285
+ sourceId,
286
+ variant: name,
287
+ target: definition.target.name,
288
+ status: "pending",
289
+ filename: defaultFilename(source.filename, name),
290
+ mimeType: "application/octet-stream",
291
+ size: 0n,
292
+ tokenId: claim,
293
+ userId: source.userId,
294
+ organizationId: source.organizationId
295
+ }
296
+ });
297
+ } catch (e) {
298
+ if (isPrismaError(e, "P2002")) {
299
+ continue;
300
+ }
301
+ if (isPrismaError(e, "P2003")) {
302
+ throw new UploadError(
303
+ "NOT_FOUND",
304
+ `Upload '${sourceId}' was deleted while its variant '${name}' was being made.`,
305
+ e
306
+ );
307
+ }
308
+ throw e;
309
+ }
310
+ return make(source, created.id, claim, name, chain);
311
+ }
312
+ if (row.status === "completed") {
313
+ return row;
314
+ }
315
+ if (row.status === "failed") {
316
+ const cutoff2 = new Date(now - retryFailedAfter);
317
+ if (row.updatedAt >= cutoff2) {
318
+ return null;
319
+ }
320
+ const { count } = await db.upload.updateMany({
321
+ where: { id: row.id, status: "failed", updatedAt: { lt: cutoff2 } },
322
+ data: { status: "pending", tokenId: claim }
323
+ });
324
+ if (count === 1) {
325
+ return make(source, row.id, claim, name, chain);
326
+ }
327
+ continue;
328
+ }
329
+ const cutoff = new Date(now - claimTimeout);
330
+ if (row.updatedAt < cutoff) {
331
+ const { count } = await db.upload.updateMany({
332
+ where: {
333
+ id: row.id,
334
+ status: "pending",
335
+ tokenId: row.tokenId,
336
+ updatedAt: { lt: cutoff }
337
+ },
338
+ data: { tokenId: claim }
339
+ });
340
+ if (count === 1) {
341
+ return make(source, row.id, claim, name, chain);
342
+ }
343
+ continue;
344
+ }
345
+ await sleep(pollInterval);
346
+ }
347
+ };
348
+ const obtain = (sourceId, name, chain) => {
349
+ if (chain.includes(name)) {
350
+ return Promise.reject(
351
+ new UploadError(
352
+ "CONFIGURATION",
353
+ `Variant '${name}' depends on itself: ${[...chain, name].join(" -> ")}.`
354
+ )
355
+ );
356
+ }
357
+ const key = keyOf(sourceId, name);
358
+ const running = inFlight.get(key);
359
+ if (running) {
360
+ return running;
361
+ }
362
+ const attempt = obtainUncached(sourceId, name, chain);
363
+ const forget = () => {
364
+ if (inFlight.get(key) === attempt) {
365
+ inFlight.delete(key);
366
+ }
367
+ };
368
+ inFlight.set(key, attempt);
369
+ attempt.then(forget, forget);
370
+ return attempt;
371
+ };
372
+ const getVariant = (source, name) => obtain(idOf(source), name, []);
373
+ const findVariant = async (source, name) => {
374
+ definitionOf(name);
375
+ return db.upload.findFirst({
376
+ where: { sourceId: idOf(source), variant: name, status: "completed" },
377
+ omit: { data: true }
378
+ });
379
+ };
380
+ const findVariants = async (sourceIds, name) => {
381
+ definitionOf(name);
382
+ const rows = sourceIds.length ? await db.upload.findMany({
383
+ where: {
384
+ sourceId: { in: sourceIds },
385
+ variant: name,
386
+ status: "completed"
387
+ },
388
+ omit: { data: true }
389
+ }) : [];
390
+ const bySource = /* @__PURE__ */ new Map();
391
+ for (const row of rows) {
392
+ if (row.sourceId) {
393
+ bySource.set(row.sourceId, row);
394
+ }
395
+ }
396
+ return bySource;
397
+ };
398
+ const readVariant = async (source, name) => {
399
+ const upload = await getVariant(source, name);
400
+ if (!upload) {
401
+ return null;
402
+ }
403
+ return { upload, data: await readBytes(db, targets, upload) };
404
+ };
405
+ const getVariantUrl = async (source, name, urlOptions = {}) => {
406
+ const upload = await getVariant(source, name);
407
+ if (!upload) {
408
+ return null;
409
+ }
410
+ const target = resolveTarget(targets, upload.target);
411
+ if (target.providerType === "db") {
412
+ return toDataUri(upload.mimeType, await readBytes(db, targets, upload));
413
+ }
414
+ if (!upload.storageKey) {
415
+ return null;
416
+ }
417
+ return target.getSignedReadUrl(upload.storageKey, urlOptions);
418
+ };
419
+ const regenerateVariants = async (name, {
420
+ remake = true,
421
+ batchSize = 100
422
+ } = {}) => {
423
+ definitionOf(name);
424
+ const result = {
425
+ deleted: 0,
426
+ made: 0,
427
+ failed: 0,
428
+ errors: 0
429
+ };
430
+ const newest = await db.upload.findFirst({
431
+ where: { variant: name },
432
+ omit: { data: true },
433
+ orderBy: { createdAt: "desc" }
434
+ });
435
+ if (!newest) {
436
+ return result;
437
+ }
438
+ const cutoff = newest.createdAt;
439
+ const exclude = [];
440
+ for (; ; ) {
441
+ const rows = await db.upload.findMany({
442
+ where: {
443
+ variant: name,
444
+ createdAt: { lte: cutoff },
445
+ ...exclude.length > 0 && { id: { notIn: exclude } }
446
+ },
447
+ omit: { data: true },
448
+ orderBy: { createdAt: "asc" },
449
+ take: batchSize
450
+ });
451
+ if (rows.length === 0) {
452
+ return result;
453
+ }
454
+ for (const row of rows) {
455
+ const sourceId = row.sourceId ?? "";
456
+ try {
457
+ await deleteVariantRow(db, targets, row);
458
+ result.deleted += 1;
459
+ } catch (e) {
460
+ result.errors += 1;
461
+ exclude.push(row.id);
462
+ onError(e, { sourceId, variant: name });
463
+ continue;
464
+ }
465
+ if (remake) {
466
+ await inFlight.get(keyOf(sourceId, name))?.catch(() => null);
467
+ const made = await getVariant(sourceId, name).catch((e) => {
468
+ onError(e, { sourceId, variant: name });
469
+ return null;
470
+ });
471
+ if (made) {
472
+ result.made += 1;
473
+ } else {
474
+ result.failed += 1;
475
+ }
476
+ const current = await findRow(sourceId, name);
477
+ if (current && current.createdAt <= cutoff) {
478
+ exclude.push(current.id);
479
+ }
480
+ }
481
+ }
482
+ }
483
+ };
484
+ return {
485
+ names,
486
+ getVariant,
487
+ findVariant,
488
+ findVariants,
489
+ readVariant,
490
+ getVariantUrl,
491
+ regenerateVariants,
492
+ deleteVariants: (source) => deleteVariants(source, { db, targets })
493
+ };
494
+ }
495
+ export {
496
+ defineUploadVariants,
497
+ deleteVariants
498
+ };
@@ -0,0 +1,19 @@
1
+ import type { StorageProvider } from './types.js';
2
+ /**
3
+ * Checks an object that was uploaded straight to storage against the type
4
+ * the client declared for it when it asked for the upload URL. The declared
5
+ * type was checked against the upload profile then, and is the
6
+ * `Content-Type` the object was stored with, so the bytes must be that type
7
+ * (or an alias of it). Reads the first `DETECT_CONTENT_TYPE_BYTES`, plus the
8
+ * last `DETECT_CONTENT_TYPE_TAIL_BYTES` when the head is a ZIP, plus the
9
+ * start of the ZIP's central directory when that begins before the tail
10
+ * (at most `MAX_ZIP_DIRECTORY_BYTES`).
11
+ *
12
+ * Returns the canonical type to record, or `null` when the bytes aren't the
13
+ * declared type.
14
+ */
15
+ export declare function verifyStoredContentType(target: StorageProvider, key: string, { size, declaredType }: {
16
+ size: number;
17
+ declaredType: string;
18
+ }): Promise<string | null>;
19
+ //# sourceMappingURL=verifyStoredContentType.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"verifyStoredContentType.d.ts","sourceRoot":"","sources":["../src/verifyStoredContentType.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAA;AAmBjD;;;;;;;;;;;;GAYG;AACH,wBAAsB,uBAAuB,CAC3C,MAAM,EAAE,eAAe,EACvB,GAAG,EAAE,MAAM,EACX,EAAE,IAAI,EAAE,YAAY,EAAE,EAAE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,MAAM,CAAA;CAAE,GAC7D,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAsCxB"}
@@ -0,0 +1,41 @@
1
+ import {
2
+ acceptedContentType,
3
+ DETECT_CONTENT_TYPE_BYTES,
4
+ DETECT_CONTENT_TYPE_TAIL_BYTES,
5
+ findZipCentralDirectory
6
+ } from "./detectContentType.js";
7
+ const ZIP_LOCAL_HEADER = [80, 75, 3, 4];
8
+ async function readRange(target, key, offset, length) {
9
+ if (target.readRange) {
10
+ return target.readRange(key, { offset, length });
11
+ }
12
+ return (await target.read(key)).subarray(offset, offset + length);
13
+ }
14
+ async function verifyStoredContentType(target, key, { size, declaredType }) {
15
+ const headLength = Math.min(size, DETECT_CONTENT_TYPE_BYTES);
16
+ const head = await readRange(target, key, 0, headLength);
17
+ let tail;
18
+ const isZip = ZIP_LOCAL_HEADER.every((byte, i) => head[i] === byte);
19
+ if (isZip && size > head.length) {
20
+ const tailLength = Math.min(size, DETECT_CONTENT_TYPE_TAIL_BYTES);
21
+ const tailStart = size - tailLength;
22
+ tail = await readRange(target, key, tailStart, tailLength);
23
+ const directory = findZipCentralDirectory(tail);
24
+ if (directory && directory.offset < tailStart && directory.offset + directory.size > tailStart) {
25
+ const missing = await readRange(
26
+ target,
27
+ key,
28
+ directory.offset,
29
+ tailStart - directory.offset
30
+ );
31
+ const joined = new Uint8Array(missing.length + tail.length);
32
+ joined.set(missing);
33
+ joined.set(tail, missing.length);
34
+ tail = joined;
35
+ }
36
+ }
37
+ return acceptedContentType(head, [declaredType], declaredType, { tail });
38
+ }
39
+ export {
40
+ verifyStoredContentType
41
+ };
@@ -37,8 +37,11 @@ export interface FsUppyOptions extends BaseUppyOptions {
37
37
  provider: 'fs';
38
38
  /** Full URL of the api's `POST {prefix}/fs` route, or a function returning it. */
39
39
  endpoint: string | (() => string);
40
- /** The upload token to send. Called per request so a refreshed token is used. */
41
- getUploadToken: () => string | null;
40
+ /**
41
+ * The upload token to send with `file`. Called per request, so each file
42
+ * can carry a different token.
43
+ */
44
+ getUploadToken: (file: Pick<CedarUppyFile, 'id'>) => string | null;
42
45
  }
43
46
  export type CreateUppyOptions = S3UppyOptions | FsUppyOptions;
44
47
  /** Response body of the api's `POST {prefix}/fs` route. */
@@ -49,6 +52,20 @@ export interface FsUploadResponseBody {
49
52
  filename: string;
50
53
  }[];
51
54
  }
55
+ /**
56
+ * Applies a profile's constraints to `uppy` as restrictions. The file-count
57
+ * limit is checked against the files still waiting to upload, not every file
58
+ * in the queue: each upload batch gets a token of its own, so files that
59
+ * already finished uploading do not count against the next batch. Uppy's own
60
+ * `maxNumberOfFiles` counts finished files too, so it is not used.
61
+ *
62
+ * The limit is added on top of the instance's current `onBeforeFileAdded`
63
+ * callback, which still runs first: returning `false` from it rejects the
64
+ * file, and returning a file object adds that file instead. Calling this
65
+ * again with new constraints replaces the limit without wrapping the same
66
+ * callback twice.
67
+ */
68
+ export declare function applyUploadConstraints(uppy: CedarUppy, constraints: UploadConstraints): void;
52
69
  /**
53
70
  * Creates an Uppy instance pre-configured for one of Cedar's upload flows.
54
71
  * Direct-to-S3 uploads use `@uppy/aws-s3` with the presigned URLs the api
@@ -1 +1 @@
1
- {"version":3,"file":"createUppy.d.ts","sourceRoot":"","sources":["../../src/web/createUppy.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,YAAY,CAAA;AAC7B,OAAO,KAAK,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAA;AAIhD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAA;AAErD,wEAAwE;AACxE,MAAM,MAAM,SAAS,GAAG,IAAI,CAAA;AAE5B,4CAA4C;AAC5C,MAAM,MAAM,aAAa,GAAG,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC,CAAA;AAEjE,OAAO,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAA;AAErD,kEAAkE;AAClE,eAAO,MAAM,sBAAsB,eAAe,CAAA;AAElD,MAAM,WAAW,eAAe;IAC9B,uEAAuE;IACvE,WAAW,CAAC,EAAE,iBAAiB,GAAG,IAAI,CAAA;IACtC,sEAAsE;IACtE,WAAW,CAAC,EAAE,OAAO,CAAA;IACrB,KAAK,CAAC,EAAE,OAAO,CAAA;CAChB;AAED;;;GAGG;AACH,MAAM,WAAW,yBAAyB;IACxC,QAAQ,EAAE,MAAM,CAAA;IAChB,GAAG,EAAE,MAAM,CAAA;IACX,MAAM,EAAE,MAAM,CAAA;IACd,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAChC;AAED,MAAM,WAAW,aAAc,SAAQ,eAAe;IACpD,QAAQ,EAAE,IAAI,CAAA;IACd;;;OAGG;IACH,mBAAmB,EAAE,CACnB,IAAI,EAAE,aAAa,KAChB,OAAO,CAAC,yBAAyB,CAAC,CAAA;CACxC;AAED,MAAM,WAAW,aAAc,SAAQ,eAAe;IACpD,QAAQ,EAAE,IAAI,CAAA;IACd,kFAAkF;IAClF,QAAQ,EAAE,MAAM,GAAG,CAAC,MAAM,MAAM,CAAC,CAAA;IACjC,iFAAiF;IACjF,cAAc,EAAE,MAAM,MAAM,GAAG,IAAI,CAAA;CACpC;AAED,MAAM,MAAM,iBAAiB,GAAG,aAAa,GAAG,aAAa,CAAA;AAE7D,2DAA2D;AAC3D,MAAM,WAAW,oBAAoB;IACnC,OAAO,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,EAAE,CAAA;CAC5D;AAcD;;;;;;GAMG;AACH,wBAAsB,UAAU,CAC9B,OAAO,EAAE,iBAAiB,GACzB,OAAO,CAAC,SAAS,CAAC,CA2EpB;AAED,wEAAwE;AACxE,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,aAAa,GAAG,MAAM,GAAG,IAAI,CAInE"}
1
+ {"version":3,"file":"createUppy.d.ts","sourceRoot":"","sources":["../../src/web/createUppy.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,YAAY,CAAA;AAC7B,OAAO,KAAK,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAA;AAIhD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAA;AAErD,wEAAwE;AACxE,MAAM,MAAM,SAAS,GAAG,IAAI,CAAA;AAE5B,4CAA4C;AAC5C,MAAM,MAAM,aAAa,GAAG,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC,CAAA;AAEjE,OAAO,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAA;AAErD,kEAAkE;AAClE,eAAO,MAAM,sBAAsB,eAAe,CAAA;AAElD,MAAM,WAAW,eAAe;IAC9B,uEAAuE;IACvE,WAAW,CAAC,EAAE,iBAAiB,GAAG,IAAI,CAAA;IACtC,sEAAsE;IACtE,WAAW,CAAC,EAAE,OAAO,CAAA;IACrB,KAAK,CAAC,EAAE,OAAO,CAAA;CAChB;AAED;;;GAGG;AACH,MAAM,WAAW,yBAAyB;IACxC,QAAQ,EAAE,MAAM,CAAA;IAChB,GAAG,EAAE,MAAM,CAAA;IACX,MAAM,EAAE,MAAM,CAAA;IACd,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAChC;AAED,MAAM,WAAW,aAAc,SAAQ,eAAe;IACpD,QAAQ,EAAE,IAAI,CAAA;IACd;;;OAGG;IACH,mBAAmB,EAAE,CACnB,IAAI,EAAE,aAAa,KAChB,OAAO,CAAC,yBAAyB,CAAC,CAAA;CACxC;AAED,MAAM,WAAW,aAAc,SAAQ,eAAe;IACpD,QAAQ,EAAE,IAAI,CAAA;IACd,kFAAkF;IAClF,QAAQ,EAAE,MAAM,GAAG,CAAC,MAAM,MAAM,CAAC,CAAA;IACjC;;;OAGG;IACH,cAAc,EAAE,CAAC,IAAI,EAAE,IAAI,CAAC,aAAa,EAAE,IAAI,CAAC,KAAK,MAAM,GAAG,IAAI,CAAA;CACnE;AAED,MAAM,MAAM,iBAAiB,GAAG,aAAa,GAAG,aAAa,CAAA;AAE7D,2DAA2D;AAC3D,MAAM,WAAW,oBAAoB;IACnC,OAAO,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,EAAE,CAAA;CAC5D;AAUD;;;;;;;;;;;;GAYG;AACH,wBAAgB,sBAAsB,CACpC,IAAI,EAAE,SAAS,EACf,WAAW,EAAE,iBAAiB,QAwC/B;AAED;;;;;;GAMG;AACH,wBAAsB,UAAU,CAC9B,OAAO,EAAE,iBAAiB,GACzB,OAAO,CAAC,SAAS,CAAC,CA2EpB;AAED,wEAAwE;AACxE,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,aAAa,GAAG,MAAM,GAAG,IAAI,CAInE"}
@@ -2,23 +2,44 @@ import Uppy from "@uppy/core";
2
2
  import { UPLOAD_TOKEN_HEADER } from "../constants.js";
3
3
  import { UPLOAD_TOKEN_HEADER as UPLOAD_TOKEN_HEADER2 } from "../constants.js";
4
4
  const DEFAULT_FS_UPLOAD_PATH = "/upload/fs";
5
- function restrictionsFor(constraints) {
6
- if (!constraints) {
7
- return {};
8
- }
9
- return {
10
- allowedFileTypes: constraints.allowedMimeTypes,
11
- maxFileSize: constraints.maxFileSize,
12
- maxNumberOfFiles: constraints.maxFiles
5
+ const wrappedCallbacks = /* @__PURE__ */ new WeakMap();
6
+ function applyUploadConstraints(uppy, constraints) {
7
+ const current = uppy.opts.onBeforeFileAdded;
8
+ const inner = current ? wrappedCallbacks.get(current) ?? current : void 0;
9
+ const onBeforeFileAdded = (file, files) => {
10
+ const result = inner ? inner(file, files) : !Object.hasOwn(files, file.id);
11
+ if (result === false) {
12
+ return false;
13
+ }
14
+ const pending = Object.values(files).filter(
15
+ (queued) => !queued.isGhost && !queued.progress.uploadComplete
16
+ );
17
+ if (pending.length >= constraints.maxFiles) {
18
+ uppy.info(
19
+ uppy.i18n("youCanOnlyUploadX", { smart_count: constraints.maxFiles }),
20
+ "error"
21
+ );
22
+ return false;
23
+ }
24
+ return result;
13
25
  };
26
+ if (inner) {
27
+ wrappedCallbacks.set(onBeforeFileAdded, inner);
28
+ }
29
+ uppy.setOptions({
30
+ restrictions: {
31
+ allowedFileTypes: constraints.allowedMimeTypes,
32
+ maxFileSize: constraints.maxFileSize
33
+ },
34
+ onBeforeFileAdded
35
+ });
14
36
  }
15
37
  async function createUppy(options) {
16
38
  const { constraints, autoProceed = true, debug = false } = options;
17
- const uppy = new Uppy({
18
- autoProceed,
19
- debug,
20
- restrictions: restrictionsFor(constraints)
21
- });
39
+ const uppy = new Uppy({ autoProceed, debug });
40
+ if (constraints) {
41
+ applyUploadConstraints(uppy, constraints);
42
+ }
22
43
  if (options.provider === "s3") {
23
44
  const { default: AwsS3 } = await import("@uppy/aws-s3");
24
45
  uppy.use(AwsS3, {
@@ -52,9 +73,9 @@ async function createUppy(options) {
52
73
  formData: true,
53
74
  fieldName: "file",
54
75
  bundle: false,
55
- headers: () => {
76
+ headers: (file) => {
56
77
  const headers = {};
57
- const token = options.getUploadToken();
78
+ const token = options.getUploadToken(file);
58
79
  if (token) {
59
80
  headers[UPLOAD_TOKEN_HEADER] = token;
60
81
  }
@@ -77,6 +98,7 @@ function getCedarUploadId(file) {
77
98
  export {
78
99
  DEFAULT_FS_UPLOAD_PATH,
79
100
  UPLOAD_TOKEN_HEADER2 as UPLOAD_TOKEN_HEADER,
101
+ applyUploadConstraints,
80
102
  createUppy,
81
103
  getCedarUploadId
82
104
  };
@@ -1 +1 @@
1
- {"version":3,"file":"useFsUpload.d.ts","sourceRoot":"","sources":["../../../src/web/hooks/useFsUpload.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,mBAAmB,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAA;AAE/E,MAAM,WAAW,kBAAmB,SAAQ,mBAAmB;IAC7D,+CAA+C;IAC/C,OAAO,EAAE,MAAM,CAAA;IACf;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;CAClB;AAED,MAAM,MAAM,iBAAiB,GAAG,gBAAgB,GAAG;IACjD,YAAY,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,CAAA;CACpC,CAAA;AAWD;;;GAGG;AACH,wBAAgB,WAAW,CAAC,EAC1B,OAAO,EACP,QAAQ,EACR,gBAAgB,EAChB,aAAa,GACd,EAAE,kBAAkB,GAAG,iBAAiB,CAyDxC"}
1
+ {"version":3,"file":"useFsUpload.d.ts","sourceRoot":"","sources":["../../../src/web/hooks/useFsUpload.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,mBAAmB,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAA;AAE/E,MAAM,WAAW,kBAAmB,SAAQ,mBAAmB;IAC7D,+CAA+C;IAC/C,OAAO,EAAE,MAAM,CAAA;IACf;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;CAClB;AAED,MAAM,MAAM,iBAAiB,GAAG,gBAAgB,GAAG;IACjD,YAAY,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,CAAA;CACpC,CAAA;AAWD;;;GAGG;AACH,wBAAgB,WAAW,CAAC,EAC1B,OAAO,EACP,QAAQ,EACR,gBAAgB,EAChB,aAAa,GACd,EAAE,kBAAkB,GAAG,iBAAiB,CA6ExC"}