uploaderkit 1.0.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/NOTICE +16 -0
- package/README.es.md +1140 -0
- package/README.md +1135 -0
- package/dist/FileViewer-ChTf7-uY.d.cts +49 -0
- package/dist/FileViewer-nuD-GEdJ.d.ts +49 -0
- package/dist/adapters/gcs.cjs +83 -0
- package/dist/adapters/gcs.cjs.map +1 -0
- package/dist/adapters/gcs.d.cts +31 -0
- package/dist/adapters/gcs.d.ts +31 -0
- package/dist/adapters/gcs.js +81 -0
- package/dist/adapters/gcs.js.map +1 -0
- package/dist/adapters/memory.cjs +27 -0
- package/dist/adapters/memory.cjs.map +1 -0
- package/dist/adapters/memory.d.cts +18 -0
- package/dist/adapters/memory.d.ts +18 -0
- package/dist/adapters/memory.js +25 -0
- package/dist/adapters/memory.js.map +1 -0
- package/dist/adapters/s3.cjs +75 -0
- package/dist/adapters/s3.cjs.map +1 -0
- package/dist/adapters/s3.d.cts +27 -0
- package/dist/adapters/s3.d.ts +27 -0
- package/dist/adapters/s3.js +73 -0
- package/dist/adapters/s3.js.map +1 -0
- package/dist/chunk-3FI44IOW.js +150 -0
- package/dist/chunk-3FI44IOW.js.map +1 -0
- package/dist/chunk-H7BRW5IY.js +308 -0
- package/dist/chunk-H7BRW5IY.js.map +1 -0
- package/dist/chunk-PDKAF4GX.js +707 -0
- package/dist/chunk-PDKAF4GX.js.map +1 -0
- package/dist/chunk-PTEX7F4R.js +309 -0
- package/dist/chunk-PTEX7F4R.js.map +1 -0
- package/dist/chunk-T5YZG6JW.js +511 -0
- package/dist/chunk-T5YZG6JW.js.map +1 -0
- package/dist/index.cjs +220 -27
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +57 -170
- package/dist/index.d.ts +57 -170
- package/dist/index.js +1 -324
- package/dist/index.js.map +1 -1
- package/dist/presets.cjs +1665 -0
- package/dist/presets.cjs.map +1 -0
- package/dist/presets.d.cts +100 -0
- package/dist/presets.d.ts +100 -0
- package/dist/presets.js +405 -0
- package/dist/presets.js.map +1 -0
- package/dist/react.cjs +981 -0
- package/dist/react.cjs.map +1 -0
- package/dist/react.d.cts +106 -0
- package/dist/react.d.ts +106 -0
- package/dist/react.js +5 -0
- package/dist/react.js.map +1 -0
- package/dist/server/express.cjs +179 -0
- package/dist/server/express.cjs.map +1 -0
- package/dist/server/express.d.cts +64 -0
- package/dist/server/express.d.ts +64 -0
- package/dist/server/express.js +111 -0
- package/dist/server/express.js.map +1 -0
- package/dist/server/next.cjs +165 -0
- package/dist/server/next.cjs.map +1 -0
- package/dist/server/next.d.cts +38 -0
- package/dist/server/next.d.ts +38 -0
- package/dist/server/next.js +107 -0
- package/dist/server/next.js.map +1 -0
- package/dist/server.cjs +550 -0
- package/dist/server.cjs.map +1 -0
- package/dist/server.d.cts +19 -0
- package/dist/server.d.ts +19 -0
- package/dist/server.js +80 -0
- package/dist/server.js.map +1 -0
- package/dist/storage-CYkSHWZX.d.cts +133 -0
- package/dist/storage-Qc9epG0G.d.ts +133 -0
- package/dist/types-BSlJJwti.d.cts +341 -0
- package/dist/types-BSlJJwti.d.ts +341 -0
- package/dist/ui.cjs +2325 -0
- package/dist/ui.cjs.map +1 -0
- package/dist/ui.d.cts +331 -0
- package/dist/ui.d.ts +331 -0
- package/dist/ui.js +749 -0
- package/dist/ui.js.map +1 -0
- package/dist/useSlottedUploader-BzT5jV8c.d.cts +139 -0
- package/dist/useSlottedUploader-IkG5RhEO.d.ts +139 -0
- package/dist/useUploader-BiBdS-7y.d.cts +117 -0
- package/dist/useUploader-CQHpj_oI.d.ts +117 -0
- package/package.json +189 -3
- package/tailwind.css +80 -0
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { ListObjectsV2Command, GetObjectCommand, DeleteObjectCommand, PutObjectCommand } from '@aws-sdk/client-s3';
|
|
2
|
+
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
|
|
3
|
+
|
|
4
|
+
// src/adapters/s3.ts
|
|
5
|
+
var createS3Provider = ({
|
|
6
|
+
client,
|
|
7
|
+
bucket,
|
|
8
|
+
publicUrl
|
|
9
|
+
}) => ({
|
|
10
|
+
name: "s3",
|
|
11
|
+
capabilities: { signedUrl: true, resumable: false, rangeRead: true },
|
|
12
|
+
put: async ({ key, body, contentType, visibility, metadata }) => {
|
|
13
|
+
if (visibility === "public" && !publicUrl) {
|
|
14
|
+
throw new Error(
|
|
15
|
+
's3 provider: a public scope needs the "publicUrl" option \u2014 per-object ACLs are blocked on modern buckets, so pass the CDN/public-domain mapping'
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
await client.send(
|
|
19
|
+
new PutObjectCommand({
|
|
20
|
+
Bucket: bucket,
|
|
21
|
+
Key: key,
|
|
22
|
+
Body: body,
|
|
23
|
+
ContentType: contentType,
|
|
24
|
+
Metadata: metadata
|
|
25
|
+
})
|
|
26
|
+
);
|
|
27
|
+
return {
|
|
28
|
+
key,
|
|
29
|
+
url: visibility === "public" ? publicUrl(key) : await getSignedUrl(
|
|
30
|
+
client,
|
|
31
|
+
new GetObjectCommand({ Bucket: bucket, Key: key }),
|
|
32
|
+
{ expiresIn: 300 }
|
|
33
|
+
)
|
|
34
|
+
};
|
|
35
|
+
},
|
|
36
|
+
get: async (key) => {
|
|
37
|
+
const result = await client.send(
|
|
38
|
+
new GetObjectCommand({ Bucket: bucket, Key: key })
|
|
39
|
+
);
|
|
40
|
+
if (!result.Body) throw new Error(`s3 provider: no object at "${key}"`);
|
|
41
|
+
return result.Body.transformToByteArray();
|
|
42
|
+
},
|
|
43
|
+
delete: async (key) => {
|
|
44
|
+
try {
|
|
45
|
+
await client.send(new DeleteObjectCommand({ Bucket: bucket, Key: key }));
|
|
46
|
+
return true;
|
|
47
|
+
} catch {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
signedUrl: (key, { expiresIn, download }) => getSignedUrl(
|
|
52
|
+
client,
|
|
53
|
+
new GetObjectCommand({
|
|
54
|
+
Bucket: bucket,
|
|
55
|
+
Key: key,
|
|
56
|
+
...download ? { ResponseContentDisposition: "attachment" } : {}
|
|
57
|
+
}),
|
|
58
|
+
{ expiresIn }
|
|
59
|
+
),
|
|
60
|
+
list: async (prefix) => {
|
|
61
|
+
const result = await client.send(
|
|
62
|
+
new ListObjectsV2Command({ Bucket: bucket, Prefix: prefix })
|
|
63
|
+
);
|
|
64
|
+
return (result.Contents ?? []).map((object) => ({
|
|
65
|
+
key: object.Key ?? "",
|
|
66
|
+
size: object.Size ?? 0
|
|
67
|
+
}));
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
export { createS3Provider };
|
|
72
|
+
//# sourceMappingURL=s3.js.map
|
|
73
|
+
//# sourceMappingURL=s3.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/adapters/s3.ts"],"names":[],"mappings":";;;;AAgCO,IAAM,mBAAmB,CAAC;AAAA,EAChC,MAAA;AAAA,EACA,MAAA;AAAA,EACA;AACD,CAAA,MAA2C;AAAA,EAC1C,IAAA,EAAM,IAAA;AAAA,EACN,cAAc,EAAE,SAAA,EAAW,MAAM,SAAA,EAAW,KAAA,EAAO,WAAW,IAAA,EAAK;AAAA,EAEnE,GAAA,EAAK,OAAO,EAAE,GAAA,EAAK,MAAM,WAAA,EAAa,UAAA,EAAY,UAAS,KAAM;AAChE,IAAA,IAAI,UAAA,KAAe,QAAA,IAAY,CAAC,SAAA,EAAW;AAC1C,MAAA,MAAM,IAAI,KAAA;AAAA,QACT;AAAA,OACD;AAAA,IACD;AAEA,IAAA,MAAM,MAAA,CAAO,IAAA;AAAA,MACZ,IAAI,gBAAA,CAAiB;AAAA,QACpB,MAAA,EAAQ,MAAA;AAAA,QACR,GAAA,EAAK,GAAA;AAAA,QACL,IAAA,EAAM,IAAA;AAAA,QACN,WAAA,EAAa,WAAA;AAAA,QACb,QAAA,EAAU;AAAA,OACV;AAAA,KACF;AAEA,IAAA,OAAO;AAAA,MACN,GAAA;AAAA,MACA,KACC,UAAA,KAAe,QAAA,GACZ,SAAA,CAAW,GAAG,IACd,MAAM,YAAA;AAAA,QACN,MAAA;AAAA,QACA,IAAI,gBAAA,CAAiB,EAAE,QAAQ,MAAA,EAAQ,GAAA,EAAK,KAAK,CAAA;AAAA,QACjD,EAAE,WAAW,GAAA;AAAI;AAClB,KACJ;AAAA,EACD,CAAA;AAAA,EAEA,GAAA,EAAK,OAAM,GAAA,KAAO;AACjB,IAAA,MAAM,MAAA,GAAS,MAAM,MAAA,CAAO,IAAA;AAAA,MAC3B,IAAI,gBAAA,CAAiB,EAAE,QAAQ,MAAA,EAAQ,GAAA,EAAK,KAAK;AAAA,KAClD;AACA,IAAA,IAAI,CAAC,OAAO,IAAA,EAAM,MAAM,IAAI,KAAA,CAAM,CAAA,2BAAA,EAA8B,GAAG,CAAA,CAAA,CAAG,CAAA;AACtE,IAAA,OAAO,MAAA,CAAO,KAAK,oBAAA,EAAqB;AAAA,EACzC,CAAA;AAAA,EAEA,MAAA,EAAQ,OAAM,GAAA,KAAO;AACpB,IAAA,IAAI;AACH,MAAA,MAAM,MAAA,CAAO,IAAA,CAAK,IAAI,mBAAA,CAAoB,EAAE,QAAQ,MAAA,EAAQ,GAAA,EAAK,GAAA,EAAK,CAAC,CAAA;AACvE,MAAA,OAAO,IAAA;AAAA,IACR,CAAA,CAAA,MAAQ;AACP,MAAA,OAAO,KAAA;AAAA,IACR;AAAA,EACD,CAAA;AAAA,EAEA,WAAW,CAAC,GAAA,EAAK,EAAE,SAAA,EAAW,UAAS,KACtC,YAAA;AAAA,IACC,MAAA;AAAA,IACA,IAAI,gBAAA,CAAiB;AAAA,MACpB,MAAA,EAAQ,MAAA;AAAA,MACR,GAAA,EAAK,GAAA;AAAA,MACL,GAAI,QAAA,GAAW,EAAE,0BAAA,EAA4B,YAAA,KAAiB;AAAC,KAC/D,CAAA;AAAA,IACD,EAAE,SAAA;AAAU,GACb;AAAA,EAED,IAAA,EAAM,OAAM,MAAA,KAAU;AACrB,IAAA,MAAM,MAAA,GAAS,MAAM,MAAA,CAAO,IAAA;AAAA,MAC3B,IAAI,oBAAA,CAAqB,EAAE,QAAQ,MAAA,EAAQ,MAAA,EAAQ,QAAQ;AAAA,KAC5D;AACA,IAAA,OAAA,CAAQ,MAAA,CAAO,QAAA,IAAY,EAAC,EAAG,IAAI,CAAA,MAAA,MAAW;AAAA,MAC7C,GAAA,EAAK,OAAO,GAAA,IAAO,EAAA;AAAA,MACnB,IAAA,EAAM,OAAO,IAAA,IAAQ;AAAA,KACtB,CAAE,CAAA;AAAA,EACH;AACD,CAAA","file":"s3.js","sourcesContent":["import {\n\tDeleteObjectCommand,\n\tGetObjectCommand,\n\tListObjectsV2Command,\n\tPutObjectCommand,\n\ttype S3Client,\n} from '@aws-sdk/client-s3'\nimport { getSignedUrl } from '@aws-sdk/s3-request-presigner'\n\nimport type { StorageProvider } from '../types'\nexport type S3ProviderOptions = {\n\tclient: S3Client\n\tbucket: string\n\t/**\n\t * Direct URL for a public object — your CDN, an R2 public domain, or the\n\t * bucket website. Required before the first `visibility: 'public'` upload:\n\t * modern buckets block per-object ACLs, so the adapter cannot invent a\n\t * stable public URL on its own.\n\t */\n\tpublicUrl?: (key: string) => string\n}\n\n/**\n * S3-compatible provider. One adapter covers AWS S3, Cloudflare R2, Backblaze\n * B2, MinIO and Wasabi — they all speak this protocol; only the client's\n * `endpoint`/credentials change.\n *\n * The bucket is treated as private (objects readable through signed URLs);\n * `publicUrl` maps the keys a CDN or public domain exposes. Both `@aws-sdk`\n * packages are optional peers — importing this module without them installed\n * fails, but only for the app that chose S3.\n */\nexport const createS3Provider = ({\n\tclient,\n\tbucket,\n\tpublicUrl,\n}: S3ProviderOptions): StorageProvider => ({\n\tname: 's3',\n\tcapabilities: { signedUrl: true, resumable: false, rangeRead: true },\n\n\tput: async ({ key, body, contentType, visibility, metadata }) => {\n\t\tif (visibility === 'public' && !publicUrl) {\n\t\t\tthrow new Error(\n\t\t\t\t's3 provider: a public scope needs the \"publicUrl\" option — per-object ACLs are blocked on modern buckets, so pass the CDN/public-domain mapping'\n\t\t\t)\n\t\t}\n\n\t\tawait client.send(\n\t\t\tnew PutObjectCommand({\n\t\t\t\tBucket: bucket,\n\t\t\t\tKey: key,\n\t\t\t\tBody: body,\n\t\t\t\tContentType: contentType,\n\t\t\t\tMetadata: metadata,\n\t\t\t})\n\t\t)\n\n\t\treturn {\n\t\t\tkey,\n\t\t\turl:\n\t\t\t\tvisibility === 'public'\n\t\t\t\t\t? publicUrl!(key)\n\t\t\t\t\t: await getSignedUrl(\n\t\t\t\t\t\t\tclient,\n\t\t\t\t\t\t\tnew GetObjectCommand({ Bucket: bucket, Key: key }),\n\t\t\t\t\t\t\t{ expiresIn: 300 }\n\t\t\t\t\t\t),\n\t\t}\n\t},\n\n\tget: async key => {\n\t\tconst result = await client.send(\n\t\t\tnew GetObjectCommand({ Bucket: bucket, Key: key })\n\t\t)\n\t\tif (!result.Body) throw new Error(`s3 provider: no object at \"${key}\"`)\n\t\treturn result.Body.transformToByteArray()\n\t},\n\n\tdelete: async key => {\n\t\ttry {\n\t\t\tawait client.send(new DeleteObjectCommand({ Bucket: bucket, Key: key }))\n\t\t\treturn true\n\t\t} catch {\n\t\t\treturn false\n\t\t}\n\t},\n\n\tsignedUrl: (key, { expiresIn, download }) =>\n\t\tgetSignedUrl(\n\t\t\tclient,\n\t\t\tnew GetObjectCommand({\n\t\t\t\tBucket: bucket,\n\t\t\t\tKey: key,\n\t\t\t\t...(download ? { ResponseContentDisposition: 'attachment' } : {}),\n\t\t\t}),\n\t\t\t{ expiresIn }\n\t\t),\n\n\tlist: async prefix => {\n\t\tconst result = await client.send(\n\t\t\tnew ListObjectsV2Command({ Bucket: bucket, Prefix: prefix })\n\t\t)\n\t\treturn (result.Contents ?? []).map(object => ({\n\t\t\tkey: object.Key ?? '',\n\t\t\tsize: object.Size ?? 0,\n\t\t}))\n\t},\n})\n"]}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { resolveLabels, assertProviderSupports, ScopeError, validateForScope, resolveKey, resolveReplaceMode, resolveScopePrefix } from './chunk-T5YZG6JW.js';
|
|
2
|
+
import { Readable } from 'stream';
|
|
3
|
+
|
|
4
|
+
var StorageRequestError = class extends Error {
|
|
5
|
+
constructor(message, status) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.status = status;
|
|
8
|
+
}
|
|
9
|
+
status;
|
|
10
|
+
name = "StorageRequestError";
|
|
11
|
+
};
|
|
12
|
+
var toHex = (buffer) => [...new Uint8Array(buffer)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
13
|
+
var createStorage = ({
|
|
14
|
+
scopes,
|
|
15
|
+
provider,
|
|
16
|
+
crypto,
|
|
17
|
+
signedUrlTtl = 300,
|
|
18
|
+
encryptedUrl,
|
|
19
|
+
labels
|
|
20
|
+
}) => {
|
|
21
|
+
const copy = resolveLabels(labels);
|
|
22
|
+
assertProviderSupports(scopes, provider);
|
|
23
|
+
const encrypted = scopes.names.filter((name) => scopes.get(name).encrypt);
|
|
24
|
+
if (encrypted.length > 0 && !crypto) {
|
|
25
|
+
throw new ScopeError(
|
|
26
|
+
`Scopes ${encrypted.join(", ")} declare "encrypt" but no CryptoHooks were provided to createStorage`
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
if (encrypted.length > 0 && !encryptedUrl) {
|
|
30
|
+
throw new ScopeError(
|
|
31
|
+
`Scopes ${encrypted.join(", ")} declare "encrypt" but no "encryptedUrl" was provided \u2014 a signed URL would serve ciphertext. Point it at the app's authenticated view endpoint.`
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
const getScope = (name) => {
|
|
35
|
+
if (!scopes.has(name)) {
|
|
36
|
+
throw new StorageRequestError(copy.unknownScope, 404);
|
|
37
|
+
}
|
|
38
|
+
return scopes.get(name);
|
|
39
|
+
};
|
|
40
|
+
const upload = async ({
|
|
41
|
+
scope: name,
|
|
42
|
+
entityId,
|
|
43
|
+
file,
|
|
44
|
+
uploadedBy
|
|
45
|
+
}) => {
|
|
46
|
+
const scope = getScope(name);
|
|
47
|
+
const result = await validateForScope(scopes, name, file, copy);
|
|
48
|
+
if (!result.valid) throw new StorageRequestError(result.message, 422);
|
|
49
|
+
const key = resolveKey(scopes, name, entityId, file);
|
|
50
|
+
const raw = new Uint8Array(await file.arrayBuffer());
|
|
51
|
+
const checksum = toHex(
|
|
52
|
+
await globalThis.crypto.subtle.digest("SHA-256", raw)
|
|
53
|
+
);
|
|
54
|
+
const body = scope.encrypt ? await crypto.encrypt(raw) : raw;
|
|
55
|
+
const put = await provider.put({
|
|
56
|
+
key,
|
|
57
|
+
body,
|
|
58
|
+
// Encrypted bytes are opaque on purpose: serving them with the real
|
|
59
|
+
// MIME would let a browser try to render ciphertext.
|
|
60
|
+
contentType: scope.encrypt ? "application/octet-stream" : file.type,
|
|
61
|
+
visibility: scope.visibility,
|
|
62
|
+
metadata: scope.metadata
|
|
63
|
+
});
|
|
64
|
+
const replaced = [];
|
|
65
|
+
if (resolveReplaceMode(scope) === "entity") {
|
|
66
|
+
const prefix = resolveScopePrefix(scope, entityId);
|
|
67
|
+
const stale = (await provider.list(prefix)).filter(
|
|
68
|
+
(object) => object.key !== put.key
|
|
69
|
+
);
|
|
70
|
+
await Promise.all(
|
|
71
|
+
stale.map(
|
|
72
|
+
(object) => provider.delete(object.key).then((deleted) => {
|
|
73
|
+
if (deleted) replaced.push(object.key);
|
|
74
|
+
}).catch(() => void 0)
|
|
75
|
+
)
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
const contentChecksum = put.checksum ?? checksum;
|
|
79
|
+
const publicUrl = put.url ? `${put.url}${put.url.includes("?") ? "&" : "?"}v=${contentChecksum.slice(0, 16)}` : put.url;
|
|
80
|
+
return {
|
|
81
|
+
key: put.key,
|
|
82
|
+
replaced,
|
|
83
|
+
url: scope.encrypt ? encryptedUrl({ scope: name, entityId, key: put.key }) : scope.visibility === "private" ? await provider.signedUrl(put.key, { expiresIn: signedUrlTtl }) : publicUrl,
|
|
84
|
+
scope: name,
|
|
85
|
+
entityId,
|
|
86
|
+
fileName: file.name,
|
|
87
|
+
mimeType: file.type,
|
|
88
|
+
size: file.size,
|
|
89
|
+
checksum: contentChecksum,
|
|
90
|
+
uploadedAt: Date.now(),
|
|
91
|
+
...uploadedBy ? { uploadedBy } : {}
|
|
92
|
+
};
|
|
93
|
+
};
|
|
94
|
+
const read = async ({ scope: name, key }) => {
|
|
95
|
+
const scope = getScope(name);
|
|
96
|
+
const data = await provider.get(key);
|
|
97
|
+
return scope.encrypt ? crypto.decrypt(data) : data;
|
|
98
|
+
};
|
|
99
|
+
const readStream = async ({
|
|
100
|
+
scope: name,
|
|
101
|
+
key
|
|
102
|
+
}) => {
|
|
103
|
+
const scope = getScope(name);
|
|
104
|
+
const canStream = provider.getStream && (!scope.encrypt || crypto?.decryptStream);
|
|
105
|
+
if (!canStream) {
|
|
106
|
+
return Readable.from([Buffer.from(await read({ scope: name, key }))]);
|
|
107
|
+
}
|
|
108
|
+
const source = await provider.getStream(key);
|
|
109
|
+
return scope.encrypt ? crypto.decryptStream(source) : source;
|
|
110
|
+
};
|
|
111
|
+
const remove = ({ scope: name, key }) => {
|
|
112
|
+
const scope = getScope(name);
|
|
113
|
+
if (scope.keepOnRemove) {
|
|
114
|
+
return Promise.resolve(false);
|
|
115
|
+
}
|
|
116
|
+
return provider.delete(key);
|
|
117
|
+
};
|
|
118
|
+
const signedUrl = ({
|
|
119
|
+
scope: name,
|
|
120
|
+
key,
|
|
121
|
+
download,
|
|
122
|
+
expiresIn
|
|
123
|
+
}) => {
|
|
124
|
+
const scope = getScope(name);
|
|
125
|
+
if (scope.visibility !== "private") {
|
|
126
|
+
throw new ScopeError(
|
|
127
|
+
`Scope "${name}" is public; its objects are reachable by their stored URL and never signed`
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
const options = {
|
|
131
|
+
expiresIn: expiresIn ?? signedUrlTtl,
|
|
132
|
+
...download !== void 0 ? { download } : {}
|
|
133
|
+
};
|
|
134
|
+
return provider.signedUrl(key, options);
|
|
135
|
+
};
|
|
136
|
+
return {
|
|
137
|
+
upload,
|
|
138
|
+
read,
|
|
139
|
+
readStream,
|
|
140
|
+
remove,
|
|
141
|
+
signedUrl,
|
|
142
|
+
list: (prefix) => provider.list(prefix),
|
|
143
|
+
scopes,
|
|
144
|
+
labels: copy
|
|
145
|
+
};
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
export { StorageRequestError, createStorage };
|
|
149
|
+
//# sourceMappingURL=chunk-3FI44IOW.js.map
|
|
150
|
+
//# sourceMappingURL=chunk-3FI44IOW.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/server/storage.ts"],"names":[],"mappings":"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","file":"chunk-3FI44IOW.js","sourcesContent":["import { Readable } from 'node:stream'\n\nimport { resolveLabels, type UploaderLabels } from '../labels'\nimport { resolveReplaceMode, resolveScopePrefix } from '../scopes'\nimport { resolveKey } from '../scopes'\nimport { assertProviderSupports, ScopeError, validateForScope } from '../scopes'\nimport type { ScopeConfig, ScopeRegistry } from '../types'\nimport type {\n\tCryptoHooks,\n\tFileLike,\n\tSignedUrlOptions,\n\tStorageProvider,\n\tStoredFile,\n} from '../types'\n\n/**\n * A request-level failure: bad file, unknown scope name from a URL, missing\n * part. Carries the HTTP status and a message safe to show the user — unlike\n * `ScopeError`, which flags wiring bugs and must stay dev-facing.\n */\nexport class StorageRequestError extends Error {\n\toverride name = 'StorageRequestError'\n\tconstructor(\n\t\tmessage: string,\n\t\treadonly status: number\n\t) {\n\t\tsuper(message)\n\t}\n}\n\nexport type CreateStorageOptions<T extends Record<string, ScopeConfig>> = {\n\tscopes: ScopeRegistry<T>\n\tprovider: StreamingStorageProvider\n\t/** Required when any scope declares `encrypt`. The app owns the cipher. */\n\tcrypto?: StreamingCryptoHooks\n\t/** Lifetime of signed URLs, in seconds. @defaultValue 300 */\n\tsignedUrlTtl?: number\n\t/**\n\t * `StoredFile.url` for encrypted scopes. Required alongside them: a signed\n\t * URL points at the bucket object, which is CIPHERTEXT — the only readable\n\t * route is the app's authenticated view endpoint (which runs `read` and\n\t * decrypts). Return that endpoint's URL, absolute or app-relative.\n\t */\n\tencryptedUrl?: (input: {\n\t\tscope: string\n\t\tentityId: string\n\t\tkey: string\n\t}) => string\n\t/**\n\t * Copy for every `StorageRequestError` this service words, and for the\n\t * validation messages it answers with. Omitted means English.\n\t *\n\t * Hand it the same object the client renders with: the server re-runs the\n\t * very validation the browser ran, so a mismatch here is the one place a\n\t * user sees the same rejection twice, in two languages.\n\t */\n\tlabels?: Partial<UploaderLabels>\n}\n\nexport type UploadInput = {\n\tscope: string\n\tentityId: string\n\tfile: FileLike\n\tuploadedBy?: string\n}\n\n/**\n * A stored file plus the keys the upload removed. `replaced` is empty unless\n * the scope sweeps per entity; when it is not, the app must drop those keys\n * from whatever it persisted, or it keeps rendering objects that are gone.\n */\nexport type UploadResult = StoredFile & { replaced: string[] }\n\n/**\n * A provider that can also read without buffering. Additive and optional, so\n * every existing provider still satisfies it — `readStream` falls back to\n * `get` when the method is absent.\n *\n * Declared here rather than in the core because the core's `StorageProvider`\n * is the published contract; widening it there would be a breaking change for\n * anyone implementing it.\n */\nexport type StreamingStorageProvider = StorageProvider & {\n\t/**\n\t * Reads without holding the object in memory. Serving a 20MB document\n\t * through `get` costs its full size in RAM per concurrent request.\n\t */\n\tgetStream?(key: string): Promise<Readable>\n}\n\n/** {@link CryptoHooks} that can also decrypt progressively. */\nexport type StreamingCryptoHooks = CryptoHooks & {\n\t/**\n\t * Decrypts as bytes arrive, so a read never materializes the plaintext.\n\t *\n\t * Weigh the cost: plaintext reaches the consumer BEFORE the authentication\n\t * tag is verified, so tampering surfaces as a stream that fails at the end,\n\t * mid-delivery. `decrypt` verifies before returning a single byte.\n\t *\n\t * There is no streaming counterpart for encryption on purpose — AES-GCM\n\t * only knows its tag once the input ends, and the layout writes it first.\n\t */\n\tdecryptStream?: (source: Readable) => Readable\n}\n\nexport type StorageService<\n\tT extends Record<string, ScopeConfig> = Record<string, ScopeConfig>,\n> = {\n\tupload(input: UploadInput): Promise<UploadResult>\n\t/** Raw bytes, decrypted when the scope is encrypted. */\n\tread(input: { scope: string; key: string }): Promise<Uint8Array>\n\t/**\n\t * The same content as a stream, so serving a file costs a buffer's worth of\n\t * memory instead of the whole object. Falls back to `read` when the provider\n\t * or the cipher cannot stream, so it is always safe to prefer.\n\t */\n\treadStream(input: { scope: string; key: string }): Promise<Readable>\n\tremove(input: { scope: string; key: string }): Promise<boolean>\n\t/** Fresh expiring URL for a private object. Throws on public scopes. */\n\tsignedUrl(input: {\n\t\tscope: string\n\t\tkey: string\n\t\tdownload?: boolean\n\t\texpiresIn?: number\n\t}): Promise<string>\n\tlist(prefix: string): Promise<{ key: string; size: number }[]>\n\tscopes: ScopeRegistry<T>\n\t/**\n\t * The resolved copy, exposed so the framework adapters word their own\n\t * failures with it. One `labels` on `createStorage` therefore covers the\n\t * whole round trip — the service and the route that wraps it cannot end up\n\t * answering in two languages.\n\t */\n\tlabels: UploaderLabels\n}\n\nconst toHex = (buffer: ArrayBuffer): string =>\n\t[...new Uint8Array(buffer)]\n\t\t.map(byte => byte.toString(16).padStart(2, '0'))\n\t\t.join('')\n\n/**\n * The server side of the contract. Boots defensively: a provider that cannot\n * honour the scopes, or an encrypted scope without an injected cipher, throws\n * here — before the first request, while a deploy can still fail loudly.\n */\nexport const createStorage = <T extends Record<string, ScopeConfig>>({\n\tscopes,\n\tprovider,\n\tcrypto,\n\tsignedUrlTtl = 300,\n\tencryptedUrl,\n\tlabels,\n}: CreateStorageOptions<T>): StorageService<T> => {\n\tconst copy = resolveLabels(labels)\n\n\tassertProviderSupports(scopes, provider)\n\n\tconst encrypted = scopes.names.filter(name => scopes.get(name).encrypt)\n\tif (encrypted.length > 0 && !crypto) {\n\t\tthrow new ScopeError(\n\t\t\t`Scopes ${encrypted.join(', ')} declare \"encrypt\" but no CryptoHooks were provided to createStorage`\n\t\t)\n\t}\n\tif (encrypted.length > 0 && !encryptedUrl) {\n\t\tthrow new ScopeError(\n\t\t\t`Scopes ${encrypted.join(', ')} declare \"encrypt\" but no \"encryptedUrl\" was provided — a signed URL would serve ciphertext. Point it at the app's authenticated view endpoint.`\n\t\t)\n\t}\n\n\tconst getScope = (name: string): ScopeConfig => {\n\t\tif (!scopes.has(name)) {\n\t\t\tthrow new StorageRequestError(copy.unknownScope, 404)\n\t\t}\n\t\treturn scopes.get(name)\n\t}\n\n\tconst upload = async ({\n\t\tscope: name,\n\t\tentityId,\n\t\tfile,\n\t\tuploadedBy,\n\t}: UploadInput): Promise<UploadResult> => {\n\t\tconst scope = getScope(name)\n\n\t\t// The client already validated; running the same function again here is\n\t\t// the part that makes the client check advisory instead of load-bearing.\n\t\tconst result = await validateForScope(scopes, name, file, copy)\n\t\tif (!result.valid) throw new StorageRequestError(result.message, 422)\n\n\t\tconst key = resolveKey(scopes, name, entityId, file)\n\t\tconst raw = new Uint8Array(await file.arrayBuffer())\n\t\tconst checksum = toHex(\n\t\t\tawait globalThis.crypto.subtle.digest('SHA-256', raw)\n\t\t)\n\t\tconst body = scope.encrypt ? await crypto!.encrypt(raw) : raw\n\n\t\tconst put = await provider.put({\n\t\t\tkey,\n\t\t\tbody,\n\t\t\t// Encrypted bytes are opaque on purpose: serving them with the real\n\t\t\t// MIME would let a browser try to render ciphertext.\n\t\t\tcontentType: scope.encrypt ? 'application/octet-stream' : file.type,\n\t\t\tvisibility: scope.visibility,\n\t\t\tmetadata: scope.metadata,\n\t\t})\n\n\t\t// Orphan sweep AFTER a successful put: the new object must exist before\n\t\t// anything is deleted, or a failure between the two leaves the entity\n\t\t// with nothing. Delete failures are swallowed on purpose — the upload\n\t\t// the caller asked for did happen, and a stale object is not worth\n\t\t// failing it over.\n\t\tconst replaced: string[] = []\n\t\tif (resolveReplaceMode(scope) === 'entity') {\n\t\t\tconst prefix = resolveScopePrefix(scope, entityId)\n\t\t\tconst stale = (await provider.list(prefix)).filter(\n\t\t\t\tobject => object.key !== put.key\n\t\t\t)\n\t\t\tawait Promise.all(\n\t\t\t\tstale.map(object =>\n\t\t\t\t\tprovider\n\t\t\t\t\t\t.delete(object.key)\n\t\t\t\t\t\t// Only what the provider confirmed gone: `replaced` is what\n\t\t\t\t\t\t// the app drops from its own records.\n\t\t\t\t\t\t.then(deleted => {\n\t\t\t\t\t\t\tif (deleted) replaced.push(object.key)\n\t\t\t\t\t\t})\n\t\t\t\t\t\t.catch(() => undefined)\n\t\t\t\t)\n\t\t\t)\n\t\t}\n\n\t\tconst contentChecksum = put.checksum ?? checksum\n\t\tconst publicUrl = put.url\n\t\t\t? `${put.url}${put.url.includes('?') ? '&' : '?'}v=${contentChecksum.slice(0, 16)}`\n\t\t\t: put.url\n\n\t\treturn {\n\t\t\tkey: put.key,\n\t\t\treplaced,\n\t\t\turl: scope.encrypt\n\t\t\t\t? encryptedUrl!({ scope: name, entityId, key: put.key })\n\t\t\t\t: scope.visibility === 'private'\n\t\t\t\t\t? await provider.signedUrl!(put.key, { expiresIn: signedUrlTtl })\n\t\t\t\t\t: publicUrl,\n\t\t\tscope: name,\n\t\t\tentityId,\n\t\t\tfileName: file.name,\n\t\t\tmimeType: file.type,\n\t\t\tsize: file.size,\n\t\t\tchecksum: contentChecksum,\n\t\t\tuploadedAt: Date.now(),\n\t\t\t...(uploadedBy ? { uploadedBy } : {}),\n\t\t}\n\t}\n\n\tconst read = async ({ scope: name, key }: { scope: string; key: string }) => {\n\t\tconst scope = getScope(name)\n\t\tconst data = await provider.get(key)\n\t\treturn scope.encrypt ? crypto!.decrypt(data) : data\n\t}\n\n\t/**\n\t * The same read without holding the object in memory.\n\t *\n\t * Degrades honestly: a provider with no `getStream`, or an encrypted scope\n\t * whose crypto has no `decryptStream`, falls back to `read` and is wrapped\n\t * in a one-chunk stream. Callers get one shape either way and never have to\n\t * ask which capabilities are present.\n\t */\n\tconst readStream = async ({\n\t\tscope: name,\n\t\tkey,\n\t}: {\n\t\tscope: string\n\t\tkey: string\n\t}): Promise<Readable> => {\n\t\tconst scope = getScope(name)\n\t\tconst canStream =\n\t\t\tprovider.getStream && (!scope.encrypt || crypto?.decryptStream)\n\n\t\tif (!canStream) {\n\t\t\treturn Readable.from([Buffer.from(await read({ scope: name, key }))])\n\t\t}\n\n\t\tconst source = (await provider.getStream!(key)) as Readable\n\t\treturn scope.encrypt ? (crypto!.decryptStream!(source) as Readable) : source\n\t}\n\n\tconst remove = ({ scope: name, key }: { scope: string; key: string }) => {\n\t\tconst scope = getScope(name)\n\t\t// The history contract, enforced where it cannot be bypassed: a scope\n\t\t// marked `keepOnRemove` answers `false` instead of destroying the\n\t\t// trail, whatever any client asks.\n\t\tif (scope.keepOnRemove) {\n\t\t\treturn Promise.resolve(false)\n\t\t}\n\t\treturn provider.delete(key)\n\t}\n\n\tconst signedUrl = ({\n\t\tscope: name,\n\t\tkey,\n\t\tdownload,\n\t\texpiresIn,\n\t}: {\n\t\tscope: string\n\t\tkey: string\n\t\tdownload?: boolean\n\t\texpiresIn?: number\n\t}) => {\n\t\tconst scope = getScope(name)\n\t\tif (scope.visibility !== 'private') {\n\t\t\tthrow new ScopeError(\n\t\t\t\t`Scope \"${name}\" is public; its objects are reachable by their stored URL and never signed`\n\t\t\t)\n\t\t}\n\t\tconst options: SignedUrlOptions = {\n\t\t\texpiresIn: expiresIn ?? signedUrlTtl,\n\t\t\t...(download !== undefined ? { download } : {}),\n\t\t}\n\t\treturn provider.signedUrl!(key, options)\n\t}\n\n\treturn {\n\t\tupload,\n\t\tread,\n\t\treadStream,\n\t\tremove,\n\t\tsignedUrl,\n\t\tlist: prefix => provider.list(prefix),\n\t\tscopes,\n\t\tlabels: copy,\n\t}\n}\n"]}
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
import { DEFAULT_LABELS, resolveLabels, ES_LABELS, validateForScope } from './chunk-T5YZG6JW.js';
|
|
2
|
+
import { createContext, useMemo, useContext, useState, useRef, useCallback, useEffect } from 'react';
|
|
3
|
+
import { jsx } from 'react/jsx-runtime';
|
|
4
|
+
|
|
5
|
+
// src/react/compressImage.ts
|
|
6
|
+
var COMPRESSIBLE = /* @__PURE__ */ new Set(["image/jpeg", "image/png", "image/webp"]);
|
|
7
|
+
var compressImage = async (file, { maxWidth, maxHeight, quality = 0.8 }) => {
|
|
8
|
+
if (!COMPRESSIBLE.has(file.type)) return file;
|
|
9
|
+
if (typeof createImageBitmap !== "function") return file;
|
|
10
|
+
try {
|
|
11
|
+
const bitmap = await createImageBitmap(file);
|
|
12
|
+
const scale = Math.min(
|
|
13
|
+
1,
|
|
14
|
+
(maxWidth ?? Infinity) / bitmap.width,
|
|
15
|
+
(maxHeight ?? Infinity) / bitmap.height
|
|
16
|
+
);
|
|
17
|
+
const canvas = document.createElement("canvas");
|
|
18
|
+
canvas.width = Math.max(1, Math.round(bitmap.width * scale));
|
|
19
|
+
canvas.height = Math.max(1, Math.round(bitmap.height * scale));
|
|
20
|
+
const context = canvas.getContext("2d");
|
|
21
|
+
if (!context) return file;
|
|
22
|
+
context.drawImage(bitmap, 0, 0, canvas.width, canvas.height);
|
|
23
|
+
bitmap.close();
|
|
24
|
+
const blob = await new Promise(
|
|
25
|
+
(resolve) => canvas.toBlob(resolve, file.type, quality)
|
|
26
|
+
);
|
|
27
|
+
if (!blob || blob.size >= file.size) return file;
|
|
28
|
+
return new File([blob], file.name, { type: file.type });
|
|
29
|
+
} catch {
|
|
30
|
+
return file;
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
var LabelsContext = createContext(DEFAULT_LABELS);
|
|
34
|
+
var UploaderProvider = ({
|
|
35
|
+
children,
|
|
36
|
+
language = "en",
|
|
37
|
+
labels
|
|
38
|
+
}) => {
|
|
39
|
+
const value = useMemo(
|
|
40
|
+
() => resolveLabels(labels, language === "es" ? ES_LABELS : DEFAULT_LABELS),
|
|
41
|
+
[language, labels]
|
|
42
|
+
);
|
|
43
|
+
return /* @__PURE__ */ jsx(LabelsContext.Provider, { value, children });
|
|
44
|
+
};
|
|
45
|
+
var useUploaderLabels = (overrides) => {
|
|
46
|
+
const base = useContext(LabelsContext);
|
|
47
|
+
return useMemo(
|
|
48
|
+
() => overrides ? { ...base, ...overrides } : base,
|
|
49
|
+
[base, overrides]
|
|
50
|
+
);
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
// src/warn.ts
|
|
54
|
+
var seen = /* @__PURE__ */ new Set();
|
|
55
|
+
var warnDev = (key, message) => {
|
|
56
|
+
if (process.env.NODE_ENV === "production") return;
|
|
57
|
+
if (seen.has(key)) return;
|
|
58
|
+
seen.add(key);
|
|
59
|
+
console.warn(`[uploaderkit] ${message}`);
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
// src/react/useUploader.ts
|
|
63
|
+
var createId = () => typeof crypto !== "undefined" && "randomUUID" in crypto ? crypto.randomUUID() : Math.random().toString(36).slice(2);
|
|
64
|
+
var previewOf = (file) => file.type.startsWith("image/") && typeof URL !== "undefined" && typeof URL.createObjectURL === "function" ? URL.createObjectURL(file) : void 0;
|
|
65
|
+
var revokePreview = (file) => {
|
|
66
|
+
if (file.preview) URL.revokeObjectURL(file.preview);
|
|
67
|
+
};
|
|
68
|
+
var normalizeRetry = (retry) => {
|
|
69
|
+
const options = typeof retry === "number" ? { attempts: retry } : retry ?? {};
|
|
70
|
+
return {
|
|
71
|
+
attempts: Math.max(1, options.attempts ?? 1),
|
|
72
|
+
backoffMs: options.backoffMs ?? 500
|
|
73
|
+
};
|
|
74
|
+
};
|
|
75
|
+
var wait = (ms, signal) => new Promise((resolve, reject) => {
|
|
76
|
+
const abort = () => {
|
|
77
|
+
clearTimeout(timer);
|
|
78
|
+
const error = new Error("Upload cancelled");
|
|
79
|
+
error.name = "AbortError";
|
|
80
|
+
reject(error);
|
|
81
|
+
};
|
|
82
|
+
const timer = setTimeout(() => {
|
|
83
|
+
signal.removeEventListener("abort", abort);
|
|
84
|
+
resolve();
|
|
85
|
+
}, ms);
|
|
86
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
87
|
+
});
|
|
88
|
+
var runPool = async (items, limit, run) => {
|
|
89
|
+
const results = new Array(items.length);
|
|
90
|
+
let cursor = 0;
|
|
91
|
+
const workers = Array.from(
|
|
92
|
+
{ length: Math.min(Math.max(1, limit), items.length) },
|
|
93
|
+
async () => {
|
|
94
|
+
while (cursor < items.length) {
|
|
95
|
+
const index = cursor++;
|
|
96
|
+
results[index] = await run(items[index]);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
);
|
|
100
|
+
await Promise.all(workers);
|
|
101
|
+
return results;
|
|
102
|
+
};
|
|
103
|
+
var useUploader = ({
|
|
104
|
+
scopes,
|
|
105
|
+
scope,
|
|
106
|
+
entityId,
|
|
107
|
+
strategy,
|
|
108
|
+
multiple: multipleOption,
|
|
109
|
+
maxFiles: maxFilesOption,
|
|
110
|
+
uploadOn = "manual",
|
|
111
|
+
onUploadStart,
|
|
112
|
+
onUploaded,
|
|
113
|
+
onError,
|
|
114
|
+
retry,
|
|
115
|
+
concurrency = Infinity,
|
|
116
|
+
rename,
|
|
117
|
+
labels
|
|
118
|
+
}) => {
|
|
119
|
+
const scopeMaxFiles = scopes.get(scope).maxFiles;
|
|
120
|
+
const maxFiles = maxFilesOption ?? scopeMaxFiles;
|
|
121
|
+
const multiple = multipleOption ?? (maxFiles !== void 0 && maxFiles > 1);
|
|
122
|
+
const [files, setFiles] = useState([]);
|
|
123
|
+
const controllers = useRef(/* @__PURE__ */ new Map());
|
|
124
|
+
const filesRef = useRef([]);
|
|
125
|
+
filesRef.current = files;
|
|
126
|
+
const copy = useUploaderLabels(labels);
|
|
127
|
+
const { attempts, backoffMs } = normalizeRetry(retry);
|
|
128
|
+
const patch = useCallback((id, changes) => {
|
|
129
|
+
setFiles(
|
|
130
|
+
(previous) => previous.map((file) => file.id === id ? { ...file, ...changes } : file)
|
|
131
|
+
);
|
|
132
|
+
}, []);
|
|
133
|
+
const uploadOne = useCallback(
|
|
134
|
+
async (target) => {
|
|
135
|
+
if (!strategy) {
|
|
136
|
+
warnDev(
|
|
137
|
+
"upload-without-strategy",
|
|
138
|
+
"upload() ran without a `strategy` \u2014 files stay local. Pass `strategy` (e.g. createXhrUploadStrategy) to useUploader."
|
|
139
|
+
);
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
const config = scopes.get(scope);
|
|
143
|
+
const controller = new AbortController();
|
|
144
|
+
controllers.current.set(target.id, controller);
|
|
145
|
+
patch(target.id, { status: "uploading", progress: 0 });
|
|
146
|
+
try {
|
|
147
|
+
const payload = config.compress ? await compressImage(target.file, config.compress) : target.file;
|
|
148
|
+
for (let attempt = 1; ; attempt++) {
|
|
149
|
+
try {
|
|
150
|
+
const stored = await strategy(payload, scope, entityId, {
|
|
151
|
+
onProgress: (percent) => patch(target.id, { progress: percent }),
|
|
152
|
+
signal: controller.signal
|
|
153
|
+
});
|
|
154
|
+
patch(target.id, { status: "success", progress: 100, stored });
|
|
155
|
+
return stored;
|
|
156
|
+
} catch (error) {
|
|
157
|
+
const aborted = error instanceof Error && error.name === "AbortError";
|
|
158
|
+
if (aborted || attempt >= attempts) throw error;
|
|
159
|
+
patch(target.id, { progress: 0 });
|
|
160
|
+
await wait(backoffMs * 2 ** (attempt - 1), controller.signal);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
} catch (error) {
|
|
164
|
+
const aborted = error instanceof Error && error.name === "AbortError";
|
|
165
|
+
const message = error instanceof Error && error.message ? error.message : copy.uploadFailed;
|
|
166
|
+
patch(target.id, {
|
|
167
|
+
status: aborted ? "idle" : "error",
|
|
168
|
+
progress: 0,
|
|
169
|
+
...aborted ? {} : { error: message }
|
|
170
|
+
});
|
|
171
|
+
if (!aborted) onError?.(message);
|
|
172
|
+
return null;
|
|
173
|
+
} finally {
|
|
174
|
+
controllers.current.delete(target.id);
|
|
175
|
+
}
|
|
176
|
+
},
|
|
177
|
+
[
|
|
178
|
+
strategy,
|
|
179
|
+
scopes,
|
|
180
|
+
scope,
|
|
181
|
+
entityId,
|
|
182
|
+
patch,
|
|
183
|
+
onError,
|
|
184
|
+
attempts,
|
|
185
|
+
backoffMs,
|
|
186
|
+
copy
|
|
187
|
+
]
|
|
188
|
+
);
|
|
189
|
+
const uploadBatch = useCallback(
|
|
190
|
+
async (targets) => {
|
|
191
|
+
if (targets.length === 0) return [];
|
|
192
|
+
onUploadStart?.(targets);
|
|
193
|
+
const results = await runPool(targets, concurrency, uploadOne);
|
|
194
|
+
const stored = results.filter((r) => r !== null);
|
|
195
|
+
if (stored.length > 0) onUploaded?.(stored);
|
|
196
|
+
return stored;
|
|
197
|
+
},
|
|
198
|
+
[uploadOne, onUploadStart, onUploaded, concurrency]
|
|
199
|
+
);
|
|
200
|
+
const addFiles = useCallback(
|
|
201
|
+
async (incoming) => {
|
|
202
|
+
let batch = Array.from(incoming);
|
|
203
|
+
if (!multiple && batch.length > 1) {
|
|
204
|
+
warnDev(
|
|
205
|
+
"single-mode-multi-drop",
|
|
206
|
+
`addFiles received ${batch.length} files but \`multiple\` is false \u2014 only the first is kept. Pass multiple: true if that is not what you meant.`
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
if (!multiple) batch = batch.slice(0, 1);
|
|
210
|
+
if (maxFiles !== void 0) {
|
|
211
|
+
const room = maxFiles - (multiple ? filesRef.current.length : 0);
|
|
212
|
+
if (batch.length > room) {
|
|
213
|
+
batch = batch.slice(0, Math.max(0, room));
|
|
214
|
+
onError?.(copy.maxFilesReached(maxFiles));
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
if (batch.length === 0) return;
|
|
218
|
+
if (rename) {
|
|
219
|
+
batch = batch.map(
|
|
220
|
+
(file) => new File([file], rename(file), { type: file.type })
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
const next = [];
|
|
224
|
+
for (const file of batch) {
|
|
225
|
+
const result = await validateForScope(scopes, scope, file, copy);
|
|
226
|
+
next.push({
|
|
227
|
+
id: createId(),
|
|
228
|
+
file,
|
|
229
|
+
status: result.valid ? "idle" : "error",
|
|
230
|
+
progress: 0,
|
|
231
|
+
...result.valid ? { preview: previewOf(file) } : { error: result.message }
|
|
232
|
+
});
|
|
233
|
+
if (!result.valid) onError?.(next[next.length - 1].error);
|
|
234
|
+
}
|
|
235
|
+
setFiles((previous) => {
|
|
236
|
+
if (multiple) return [...previous, ...next];
|
|
237
|
+
previous.forEach(revokePreview);
|
|
238
|
+
return next;
|
|
239
|
+
});
|
|
240
|
+
if (uploadOn === "select") {
|
|
241
|
+
await uploadBatch(next.filter((file) => file.status === "idle"));
|
|
242
|
+
}
|
|
243
|
+
},
|
|
244
|
+
[
|
|
245
|
+
multiple,
|
|
246
|
+
maxFiles,
|
|
247
|
+
scopes,
|
|
248
|
+
scope,
|
|
249
|
+
uploadOn,
|
|
250
|
+
uploadBatch,
|
|
251
|
+
onError,
|
|
252
|
+
rename,
|
|
253
|
+
copy
|
|
254
|
+
]
|
|
255
|
+
);
|
|
256
|
+
const upload = useCallback(
|
|
257
|
+
() => uploadBatch(filesRef.current.filter((f) => f.status === "idle")),
|
|
258
|
+
[uploadBatch]
|
|
259
|
+
);
|
|
260
|
+
const abort = useCallback((id) => {
|
|
261
|
+
if (id) {
|
|
262
|
+
controllers.current.get(id)?.abort();
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
controllers.current.forEach((controller) => controller.abort());
|
|
266
|
+
}, []);
|
|
267
|
+
const removeFile = useCallback(
|
|
268
|
+
(id) => {
|
|
269
|
+
abort(id);
|
|
270
|
+
setFiles((previous) => {
|
|
271
|
+
const target = previous.find((file) => file.id === id);
|
|
272
|
+
if (target) revokePreview(target);
|
|
273
|
+
return previous.filter((file) => file.id !== id);
|
|
274
|
+
});
|
|
275
|
+
},
|
|
276
|
+
[abort]
|
|
277
|
+
);
|
|
278
|
+
const clear = useCallback(() => {
|
|
279
|
+
abort();
|
|
280
|
+
setFiles((previous) => {
|
|
281
|
+
previous.forEach(revokePreview);
|
|
282
|
+
return [];
|
|
283
|
+
});
|
|
284
|
+
}, [abort]);
|
|
285
|
+
useEffect(
|
|
286
|
+
() => () => {
|
|
287
|
+
filesRef.current.forEach(revokePreview);
|
|
288
|
+
controllers.current.forEach((controller) => controller.abort());
|
|
289
|
+
},
|
|
290
|
+
[]
|
|
291
|
+
);
|
|
292
|
+
const accept = useMemo(() => scopes.accept(scope), [scopes, scope]);
|
|
293
|
+
return {
|
|
294
|
+
files,
|
|
295
|
+
accept,
|
|
296
|
+
isUploading: files.some((file) => file.status === "uploading"),
|
|
297
|
+
hasPending: files.some((file) => file.status === "idle"),
|
|
298
|
+
addFiles,
|
|
299
|
+
upload,
|
|
300
|
+
abort,
|
|
301
|
+
removeFile,
|
|
302
|
+
clear
|
|
303
|
+
};
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
export { UploaderProvider, compressImage, useUploader, useUploaderLabels, warnDev };
|
|
307
|
+
//# sourceMappingURL=chunk-H7BRW5IY.js.map
|
|
308
|
+
//# sourceMappingURL=chunk-H7BRW5IY.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/react/compressImage.ts","../src/react/UploaderProvider.tsx","../src/warn.ts","../src/react/useUploader.ts"],"names":["useMemo"],"mappings":"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","file":"chunk-H7BRW5IY.js","sourcesContent":["import type { CompressOptions } from '../types'\n// Formats a canvas can re-encode. GIF would lose animation and SVG would\n// rasterize, so both pass through untouched.\nconst COMPRESSIBLE = new Set(['image/jpeg', 'image/png', 'image/webp'])\n\n/**\n * Client-side image trim before upload: downscale to the scope's box and\n * re-encode at the scope's quality. Re-encoding through a canvas drops EXIF as\n * a side effect — GPS and camera metadata never leave the device, regardless\n * of `stripExif` (a canvas cannot keep it).\n *\n * Falls back to the original file whenever it cannot help: non-compressible\n * formats, environments without `createImageBitmap`, decode failures, or a\n * \"compressed\" result that came out larger.\n */\nexport const compressImage = async (\n\tfile: File,\n\t{ maxWidth, maxHeight, quality = 0.8 }: CompressOptions\n): Promise<File> => {\n\tif (!COMPRESSIBLE.has(file.type)) return file\n\tif (typeof createImageBitmap !== 'function') return file\n\n\ttry {\n\t\tconst bitmap = await createImageBitmap(file)\n\t\tconst scale = Math.min(\n\t\t\t1,\n\t\t\t(maxWidth ?? Infinity) / bitmap.width,\n\t\t\t(maxHeight ?? Infinity) / bitmap.height\n\t\t)\n\n\t\tconst canvas = document.createElement('canvas')\n\t\tcanvas.width = Math.max(1, Math.round(bitmap.width * scale))\n\t\tcanvas.height = Math.max(1, Math.round(bitmap.height * scale))\n\t\tconst context = canvas.getContext('2d')\n\t\tif (!context) return file\n\t\tcontext.drawImage(bitmap, 0, 0, canvas.width, canvas.height)\n\t\tbitmap.close()\n\n\t\tconst blob = await new Promise<Blob | null>(resolve =>\n\t\t\tcanvas.toBlob(resolve, file.type, quality)\n\t\t)\n\t\tif (!blob || blob.size >= file.size) return file\n\n\t\treturn new File([blob], file.name, { type: file.type })\n\t} catch {\n\t\treturn file\n\t}\n}\n","import { createContext, type ReactNode, useContext, useMemo } from 'react'\n\nimport {\n\tDEFAULT_LABELS,\n\tES_LABELS,\n\tresolveLabels,\n\ttype UploaderLabels,\n} from '../labels'\n\nconst LabelsContext = createContext<UploaderLabels>(DEFAULT_LABELS)\n\n/** The two built-in copy sets. */\nexport type UploaderLanguage = 'en' | 'es'\n\nexport type UploaderProviderProps = {\n\tchildren: ReactNode\n\t/**\n\t * Which built-in copy set every uploader under it speaks. English is the\n\t * default so the kit ships globalized; a Spanish app opts in ONCE here\n\t * instead of passing `labels` to each component.\n\t * @defaultValue 'en'\n\t */\n\tlanguage?: UploaderLanguage\n\t/** Per-key overrides on top of the selected language. */\n\tlabels?: Partial<UploaderLabels>\n}\n\n/**\n * Optional app-root provider carrying the copy. Components work without it\n * (they fall back to {@link DEFAULT_LABELS}), and a per-component `labels`\n * prop still wins for one-off rewording.\n */\nexport const UploaderProvider = ({\n\tchildren,\n\tlanguage = 'en',\n\tlabels,\n}: UploaderProviderProps) => {\n\tconst value = useMemo(\n\t\t() => resolveLabels(labels, language === 'es' ? ES_LABELS : DEFAULT_LABELS),\n\t\t[language, labels]\n\t)\n\n\treturn (\n\t\t<LabelsContext.Provider value={value}>{children}</LabelsContext.Provider>\n\t)\n}\n\n/**\n * The active copy, merged: provider base ← per-call `overrides`. What every\n * component and hook in the package reads instead of `resolveLabels` directly,\n * so a single provider re-words all of them at once.\n */\nexport const useUploaderLabels = (\n\toverrides?: Partial<UploaderLabels>\n): UploaderLabels => {\n\tconst base = useContext(LabelsContext)\n\treturn useMemo(\n\t\t() => (overrides ? { ...base, ...overrides } : base),\n\t\t[base, overrides]\n\t)\n}\n","const seen = new Set<string>()\n\n/**\n * Dev-only warning, once per `key` — for paths that run per render or per\n * file, where the same misconfiguration would otherwise print hundreds of\n * times. Same contract as listkit's `warnDev`.\n *\n * @see AGENTS.md §3.1 — this tier vs the `ScopeError` throw\n */\nexport const warnDev = (key: string, message: string): void => {\n\tif (process.env.NODE_ENV === 'production') return\n\tif (seen.has(key)) return\n\tseen.add(key)\n\tconsole.warn(`[uploaderkit] ${message}`)\n}\n","import { useCallback, useEffect, useMemo, useRef, useState } from 'react'\n\nimport type { UploaderLabels } from '../labels'\nimport { validateForScope } from '../scopes'\nimport type { ScopeConfig, ScopeRegistry } from '../types'\nimport type { StoredFile } from '../types'\nimport { warnDev } from '../warn'\nimport { compressImage } from './compressImage'\nimport type { UploaderFile, UploadStrategy } from './types'\nimport { useUploaderLabels } from './UploaderProvider'\n\n/**\n * When the machine sends: `'manual'` holds files in `idle` until the app calls\n * `upload()` — the form-submit flow, where the file travels together with the\n * rest of the fields' action. `'select'` fires as soon as a valid file lands.\n */\nexport type UploadTrigger = 'select' | 'manual'\n\nexport type RetryOptions = {\n\t/** Total attempts per file, the first one included. @defaultValue 1 */\n\tattempts?: number\n\t/** Delay before the first retry; doubles on each further one. @defaultValue 500 */\n\tbackoffMs?: number\n}\n\nexport type UseUploaderOptions<T extends Record<string, ScopeConfig>> = {\n\t/** The app's registry — the same object the server authorizes against. */\n\tscopes: ScopeRegistry<T>\n\tscope: keyof T & string\n\t/** Owner of the uploads (customerId, userId, …), forwarded to the strategy. */\n\tentityId: string\n\t/** Transport. Omit for local-only selection + validation (no `upload`). */\n\tstrategy?: UploadStrategy\n\t/**\n\t * Overrides the arity derived from the scope's `maxFiles`. Rarely needed:\n\t * the registry is what the server validates against.\n\t */\n\tmultiple?: boolean\n\t/** Overrides the scope's own `maxFiles`, downward. */\n\tmaxFiles?: number\n\t/** @defaultValue 'manual' — the styled `Uploader` flips it to `'select'`. */\n\tuploadOn?: UploadTrigger\n\t/**\n\t * Fired when a batch actually starts travelling — from either trigger — with\n\t * the files it carries. The place to flip a form into its \"sending\" state.\n\t */\n\tonUploadStart?: (files: UploaderFile[]) => void\n\t/** Fired once per batch with the files the server confirmed. */\n\tonUploaded?: (stored: StoredFile[]) => void\n\tonError?: (message: string) => void\n\t/**\n\t * Re-run a failed strategy call before surfacing the error. A number is\n\t * shorthand for `{ attempts }`. Aborts never retry, and validation failures\n\t * never reach the strategy in the first place.\n\t */\n\tretry?: number | RetryOptions\n\t/** Max uploads in flight per batch; the rest queue. @defaultValue Infinity */\n\tconcurrency?: number\n\t/**\n\t * Rename each file before it enters the machine — a client-side input, a\n\t * folio, a slug. Runs before validation, so a rename that changes the\n\t * extension is rejected like any other invalid file. The stored key comes\n\t * from the scope's `path(entityId, file)`, which reads this name.\n\t */\n\trename?: (file: File) => string\n\tlabels?: Partial<UploaderLabels>\n}\n\nexport type UseUploaderReturn = {\n\tfiles: UploaderFile[]\n\t/** Ready-made value for `<input accept>`, derived from the scope. */\n\taccept: string\n\tisUploading: boolean\n\t/** `true` while any file waits in `idle` — a manual trigger has work to do. */\n\thasPending: boolean\n\taddFiles: (incoming: FileList | File[]) => Promise<void>\n\t/** Uploads every file still in `idle`. Resolves with the confirmed ones. */\n\tupload: () => Promise<StoredFile[]>\n\t/** Abort one in-flight upload, or all of them when no id is given. */\n\tabort: (id?: string) => void\n\tremoveFile: (id: string) => void\n\tclear: () => void\n}\n\nconst createId = () =>\n\ttypeof crypto !== 'undefined' && 'randomUUID' in crypto\n\t\t? crypto.randomUUID()\n\t\t: Math.random().toString(36).slice(2)\n\nconst previewOf = (file: File): string | undefined =>\n\tfile.type.startsWith('image/') &&\n\ttypeof URL !== 'undefined' &&\n\ttypeof URL.createObjectURL === 'function'\n\t\t? URL.createObjectURL(file)\n\t\t: undefined\n\nconst revokePreview = (file: UploaderFile) => {\n\tif (file.preview) URL.revokeObjectURL(file.preview)\n}\n\nconst normalizeRetry = (retry: number | RetryOptions | undefined) => {\n\tconst options =\n\t\ttypeof retry === 'number' ? { attempts: retry } : (retry ?? {})\n\treturn {\n\t\tattempts: Math.max(1, options.attempts ?? 1),\n\t\tbackoffMs: options.backoffMs ?? 500,\n\t}\n}\n\n/** Abortable sleep: the backoff between retries must die with the upload. */\nconst wait = (ms: number, signal: AbortSignal) =>\n\tnew Promise<void>((resolve, reject) => {\n\t\tconst abort = () => {\n\t\t\tclearTimeout(timer)\n\t\t\tconst error = new Error('Upload cancelled')\n\t\t\terror.name = 'AbortError'\n\t\t\treject(error)\n\t\t}\n\t\tconst timer = setTimeout(() => {\n\t\t\tsignal.removeEventListener('abort', abort)\n\t\t\tresolve()\n\t\t}, ms)\n\t\tsignal.addEventListener('abort', abort, { once: true })\n\t})\n\n/**\n * Run `run` over every item with at most `limit` in flight. Order of results\n * matches the input; a settled slot immediately frees a worker for the next.\n */\nconst runPool = async <T, R>(\n\titems: T[],\n\tlimit: number,\n\trun: (item: T) => Promise<R>\n): Promise<R[]> => {\n\tconst results = new Array<R>(items.length)\n\tlet cursor = 0\n\tconst workers = Array.from(\n\t\t{ length: Math.min(Math.max(1, limit), items.length) },\n\t\tasync () => {\n\t\t\twhile (cursor < items.length) {\n\t\t\t\tconst index = cursor++\n\t\t\t\tresults[index] = await run(items[index]!)\n\t\t\t}\n\t\t}\n\t)\n\tawait Promise.all(workers)\n\treturn results\n}\n\n/**\n * Headless upload state machine: selection → validation → (compression) →\n * upload with progress and abort. Owns no markup — render `files` however the\n * screen needs and wire `addFiles` to an input or a drop zone.\n *\n * Validation runs here with the same `validateForScope` the server runs, so\n * the user sees the rejection before any byte leaves the machine.\n */\nexport const useUploader = <T extends Record<string, ScopeConfig>>({\n\tscopes,\n\tscope,\n\tentityId,\n\tstrategy,\n\tmultiple: multipleOption,\n\tmaxFiles: maxFilesOption,\n\tuploadOn = 'manual',\n\tonUploadStart,\n\tonUploaded,\n\tonError,\n\tretry,\n\tconcurrency = Infinity,\n\trename,\n\tlabels,\n}: UseUploaderOptions<T>): UseUploaderReturn => {\n\t// Derived from the registry, never from the call site — see AGENTS.md §3.\n\t// The props stay as a narrowing override.\n\tconst scopeMaxFiles = scopes.get(scope).maxFiles\n\tconst maxFiles = maxFilesOption ?? scopeMaxFiles\n\tconst multiple = multipleOption ?? (maxFiles !== undefined && maxFiles > 1)\n\n\tconst [files, setFiles] = useState<UploaderFile[]>([])\n\tconst controllers = useRef(new Map<string, AbortController>())\n\t// Kept in a ref so `addFiles` → auto-upload reads the batch it just built,\n\t// not the state snapshot from before the setFiles commit.\n\tconst filesRef = useRef<UploaderFile[]>([])\n\tfilesRef.current = files\n\tconst copy = useUploaderLabels(labels)\n\tconst { attempts, backoffMs } = normalizeRetry(retry)\n\n\tconst patch = useCallback((id: string, changes: Partial<UploaderFile>) => {\n\t\tsetFiles(previous =>\n\t\t\tprevious.map(file => (file.id === id ? { ...file, ...changes } : file))\n\t\t)\n\t}, [])\n\n\tconst uploadOne = useCallback(\n\t\tasync (target: UploaderFile): Promise<StoredFile | null> => {\n\t\t\tif (!strategy) {\n\t\t\t\twarnDev(\n\t\t\t\t\t'upload-without-strategy',\n\t\t\t\t\t'upload() ran without a `strategy` — files stay local. Pass `strategy` (e.g. createXhrUploadStrategy) to useUploader.'\n\t\t\t\t)\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst config = scopes.get(scope)\n\n\t\t\tconst controller = new AbortController()\n\t\t\tcontrollers.current.set(target.id, controller)\n\t\t\tpatch(target.id, { status: 'uploading', progress: 0 })\n\n\t\t\ttry {\n\t\t\t\tconst payload = config.compress\n\t\t\t\t\t? await compressImage(target.file, config.compress)\n\t\t\t\t\t: target.file\n\n\t\t\t\t// Compression runs once; only the transport retries.\n\t\t\t\tfor (let attempt = 1; ; attempt++) {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tconst stored = await strategy(payload, scope, entityId, {\n\t\t\t\t\t\t\tonProgress: percent => patch(target.id, { progress: percent }),\n\t\t\t\t\t\t\tsignal: controller.signal,\n\t\t\t\t\t\t})\n\t\t\t\t\t\tpatch(target.id, { status: 'success', progress: 100, stored })\n\t\t\t\t\t\treturn stored\n\t\t\t\t\t} catch (error) {\n\t\t\t\t\t\tconst aborted =\n\t\t\t\t\t\t\terror instanceof Error && error.name === 'AbortError'\n\t\t\t\t\t\tif (aborted || attempt >= attempts) throw error\n\t\t\t\t\t\tpatch(target.id, { progress: 0 })\n\t\t\t\t\t\tawait wait(backoffMs * 2 ** (attempt - 1), controller.signal)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} catch (error) {\n\t\t\t\tconst aborted = error instanceof Error && error.name === 'AbortError'\n\t\t\t\tconst message =\n\t\t\t\t\terror instanceof Error && error.message\n\t\t\t\t\t\t? error.message\n\t\t\t\t\t\t: copy.uploadFailed\n\t\t\t\tpatch(target.id, {\n\t\t\t\t\tstatus: aborted ? 'idle' : 'error',\n\t\t\t\t\tprogress: 0,\n\t\t\t\t\t...(aborted ? {} : { error: message }),\n\t\t\t\t})\n\t\t\t\tif (!aborted) onError?.(message)\n\t\t\t\treturn null\n\t\t\t} finally {\n\t\t\t\tcontrollers.current.delete(target.id)\n\t\t\t}\n\t\t},\n\t\t[\n\t\t\tstrategy,\n\t\t\tscopes,\n\t\t\tscope,\n\t\t\tentityId,\n\t\t\tpatch,\n\t\t\tonError,\n\t\t\tattempts,\n\t\t\tbackoffMs,\n\t\t\tcopy,\n\t\t]\n\t)\n\n\tconst uploadBatch = useCallback(\n\t\tasync (targets: UploaderFile[]): Promise<StoredFile[]> => {\n\t\t\tif (targets.length === 0) return []\n\t\t\tonUploadStart?.(targets)\n\t\t\tconst results = await runPool(targets, concurrency, uploadOne)\n\t\t\tconst stored = results.filter((r): r is StoredFile => r !== null)\n\t\t\tif (stored.length > 0) onUploaded?.(stored)\n\t\t\treturn stored\n\t\t},\n\t\t[uploadOne, onUploadStart, onUploaded, concurrency]\n\t)\n\n\tconst addFiles = useCallback(\n\t\tasync (incoming: FileList | File[]) => {\n\t\t\tlet batch = Array.from(incoming)\n\t\t\tif (!multiple && batch.length > 1) {\n\t\t\t\twarnDev(\n\t\t\t\t\t'single-mode-multi-drop',\n\t\t\t\t\t`addFiles received ${batch.length} files but \\`multiple\\` is false — only the first is kept. Pass multiple: true if that is not what you meant.`\n\t\t\t\t)\n\t\t\t}\n\t\t\tif (!multiple) batch = batch.slice(0, 1)\n\n\t\t\tif (maxFiles !== undefined) {\n\t\t\t\tconst room = maxFiles - (multiple ? filesRef.current.length : 0)\n\t\t\t\tif (batch.length > room) {\n\t\t\t\t\tbatch = batch.slice(0, Math.max(0, room))\n\t\t\t\t\tonError?.(copy.maxFilesReached(maxFiles))\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (batch.length === 0) return\n\n\t\t\tif (rename) {\n\t\t\t\tbatch = batch.map(\n\t\t\t\t\tfile => new File([file], rename(file), { type: file.type })\n\t\t\t\t)\n\t\t\t}\n\n\t\t\tconst next: UploaderFile[] = []\n\t\t\tfor (const file of batch) {\n\t\t\t\tconst result = await validateForScope(scopes, scope, file, copy)\n\t\t\t\tnext.push({\n\t\t\t\t\tid: createId(),\n\t\t\t\t\tfile,\n\t\t\t\t\tstatus: result.valid ? 'idle' : 'error',\n\t\t\t\t\tprogress: 0,\n\t\t\t\t\t...(result.valid\n\t\t\t\t\t\t? { preview: previewOf(file) }\n\t\t\t\t\t\t: { error: result.message }),\n\t\t\t\t})\n\t\t\t\tif (!result.valid) onError?.(next[next.length - 1]!.error!)\n\t\t\t}\n\n\t\t\tsetFiles(previous => {\n\t\t\t\tif (multiple) return [...previous, ...next]\n\t\t\t\tprevious.forEach(revokePreview)\n\t\t\t\treturn next\n\t\t\t})\n\n\t\t\tif (uploadOn === 'select') {\n\t\t\t\tawait uploadBatch(next.filter(file => file.status === 'idle'))\n\t\t\t}\n\t\t},\n\t\t[\n\t\t\tmultiple,\n\t\t\tmaxFiles,\n\t\t\tscopes,\n\t\t\tscope,\n\t\t\tuploadOn,\n\t\t\tuploadBatch,\n\t\t\tonError,\n\t\t\trename,\n\t\t\tcopy,\n\t\t]\n\t)\n\n\tconst upload = useCallback(\n\t\t() => uploadBatch(filesRef.current.filter(f => f.status === 'idle')),\n\t\t[uploadBatch]\n\t)\n\n\tconst abort = useCallback((id?: string) => {\n\t\tif (id) {\n\t\t\tcontrollers.current.get(id)?.abort()\n\t\t\treturn\n\t\t}\n\t\tcontrollers.current.forEach(controller => controller.abort())\n\t}, [])\n\n\tconst removeFile = useCallback(\n\t\t(id: string) => {\n\t\t\tabort(id)\n\t\t\tsetFiles(previous => {\n\t\t\t\tconst target = previous.find(file => file.id === id)\n\t\t\t\tif (target) revokePreview(target)\n\t\t\t\treturn previous.filter(file => file.id !== id)\n\t\t\t})\n\t\t},\n\t\t[abort]\n\t)\n\n\tconst clear = useCallback(() => {\n\t\tabort()\n\t\tsetFiles(previous => {\n\t\t\tprevious.forEach(revokePreview)\n\t\t\treturn []\n\t\t})\n\t}, [abort])\n\n\t// Object URLs leak until revoked; the unmount sweep catches whatever the\n\t// screen never removed explicitly.\n\tuseEffect(\n\t\t() => () => {\n\t\t\tfilesRef.current.forEach(revokePreview)\n\t\t\tcontrollers.current.forEach(controller => controller.abort())\n\t\t},\n\t\t[]\n\t)\n\n\tconst accept = useMemo(() => scopes.accept(scope), [scopes, scope])\n\n\treturn {\n\t\tfiles,\n\t\taccept,\n\t\tisUploading: files.some(file => file.status === 'uploading'),\n\t\thasPending: files.some(file => file.status === 'idle'),\n\t\taddFiles,\n\t\tupload,\n\t\tabort,\n\t\tremoveFile,\n\t\tclear,\n\t}\n}\n"]}
|