@es-labs/jslib 0.0.1

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 (72) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/README.md +42 -0
  3. package/__test__/services.test.js +32 -0
  4. package/auth/index.js +226 -0
  5. package/auth/keyv.js +23 -0
  6. package/auth/knex.js +29 -0
  7. package/auth/redis.js +23 -0
  8. package/comms/email.js +123 -0
  9. package/comms/nexmo.js +44 -0
  10. package/comms/telegram.js +43 -0
  11. package/comms/telegram2/inbound.js +314 -0
  12. package/comms/telegram2/outbound.js +574 -0
  13. package/comms/webpush.js +60 -0
  14. package/config.js +37 -0
  15. package/express/controller/auth/oauth.js +39 -0
  16. package/express/controller/auth/oidc.js +87 -0
  17. package/express/controller/auth/own.js +100 -0
  18. package/express/controller/auth/saml.js +74 -0
  19. package/express/upload.js +48 -0
  20. package/index.js +1 -0
  21. package/iso/README.md +4 -0
  22. package/iso/__tests__/csv-utils.spec.js +128 -0
  23. package/iso/__tests__/datetime.spec.js +101 -0
  24. package/iso/__tests__/fetch.spec.js +270 -0
  25. package/iso/csv-utils.js +206 -0
  26. package/iso/datetime.js +103 -0
  27. package/iso/fetch.js +129 -0
  28. package/iso/fetch2.js +180 -0
  29. package/iso/log-filter.js +17 -0
  30. package/iso/sleep.js +6 -0
  31. package/iso/ws.js +63 -0
  32. package/node/oss-files/oss-uploader-client-fetch.js +258 -0
  33. package/node/oss-files/oss-uploader-client-fetch.md +31 -0
  34. package/node/oss-files/oss-uploader-client.js +219 -0
  35. package/node/oss-files/oss-uploader-server.js +199 -0
  36. package/node/oss-files/oss-uploader-usage.js +121 -0
  37. package/node/oss-files/oss-uploader-usage.md +34 -0
  38. package/node/oss-files/s3-uploader-client.js +217 -0
  39. package/node/oss-files/s3-uploader-server.js +123 -0
  40. package/node/oss-files/s3-uploader-usage.js +77 -0
  41. package/node/oss-files/s3-uploader-usage.md +34 -0
  42. package/package.json +53 -0
  43. package/packageInfo.js +9 -0
  44. package/services/ali.js +279 -0
  45. package/services/aws.js +194 -0
  46. package/services/db/__tests__/keyv.spec.js +31 -0
  47. package/services/db/keyv.js +14 -0
  48. package/services/db/knex.js +67 -0
  49. package/services/db/redis.js +51 -0
  50. package/services/index.js +57 -0
  51. package/services/mq/README.md +8 -0
  52. package/services/websocket.js +139 -0
  53. package/t4t/README.md +1 -0
  54. package/traps.js +20 -0
  55. package/utils/__tests__/aes.spec.js +52 -0
  56. package/utils/aes.js +23 -0
  57. package/web/UI.md +71 -0
  58. package/web/bwc-autocomplete.js +211 -0
  59. package/web/bwc-combobox.js +343 -0
  60. package/web/bwc-fileupload.js +87 -0
  61. package/web/bwc-loading-overlay.js +54 -0
  62. package/web/bwc-t4t-form.js +511 -0
  63. package/web/bwc-table.js +756 -0
  64. package/web/fetch.js +129 -0
  65. package/web/i18n.js +24 -0
  66. package/web/idle.js +49 -0
  67. package/web/parse-jwt.js +15 -0
  68. package/web/pwa.js +84 -0
  69. package/web/sign-pad.js +164 -0
  70. package/web/t4t-fe.js +164 -0
  71. package/web/util.js +126 -0
  72. package/web/web-cam.js +182 -0
@@ -0,0 +1,219 @@
1
+ /**
2
+ * Alibaba Cloud OSS Large File Uploader (Client-Side)
3
+ * Uses the S3-compatible API via pre-signed URLs generated by your backend.
4
+ *
5
+ * OSS S3-compatibility docs:
6
+ * https://www.alibabacloud.com/help/en/oss/developer-reference/compatibility-with-amazon-s3
7
+ *
8
+ * Key OSS-specific differences from AWS S3:
9
+ * - Endpoint format: https://<bucket>.<region>.aliyuncs.com (path-style not needed)
10
+ * - Minimum multipart part size: 100KB (S3 is 5MB) — we still use 10MB for reliability
11
+ * - Max parts: 10,000 (same as S3)
12
+ * - ETag is returned without quotes in some OSS responses — we normalize this below
13
+ * - CORS must expose the ETag header explicitly on the OSS bucket
14
+ *
15
+ * Usage:
16
+ * const uploader = new OSSUploader({ signEndpoint: '/api/oss/sign' });
17
+ * const result = await uploader.upload(file, { onProgress: pct => console.log(pct + '%') });
18
+ */
19
+
20
+ const CHUNK_SIZE = 10 * 1024 * 1024; // 10MB per part
21
+ const MULTIPART_THRESHOLD = 5 * 1024 * 1024; // Use multipart above 5MB
22
+
23
+ class OSSUploader {
24
+ /**
25
+ * @param {object} options
26
+ * @param {string} options.signEndpoint - Your backend endpoint for signed URL generation
27
+ * @param {number} [options.chunkSize] - Bytes per part (default 10MB, min 100KB for OSS)
28
+ * @param {number} [options.maxConcurrent]- Parallel part uploads (default 3)
29
+ */
30
+ constructor(options = {}) {
31
+ if (!options.signEndpoint) throw new Error('signEndpoint is required');
32
+ this.signEndpoint = options.signEndpoint;
33
+ this.chunkSize = options.chunkSize || CHUNK_SIZE;
34
+ this.maxConcurrent = options.maxConcurrent || 3;
35
+ }
36
+
37
+ // ─── Public API ──────────────────────────────────────────────────────────────
38
+
39
+ /**
40
+ * Upload a File or Blob to Alibaba OSS.
41
+ *
42
+ * @param {File|Blob} file
43
+ * @param {object} [opts]
44
+ * @param {string} [opts.key] - OSS object key (defaults to file.name)
45
+ * @param {Function} [opts.onProgress] - Callback with integer 0–100
46
+ * @param {AbortSignal} [opts.signal] - AbortController signal to cancel
47
+ * @returns {Promise<{ key: string, location: string }>}
48
+ */
49
+ async upload(file, opts = {}) {
50
+ const key = opts.key || file.name;
51
+ const onProgress = opts.onProgress || (() => {});
52
+ const signal = opts.signal || null;
53
+
54
+ if (file.size <= MULTIPART_THRESHOLD) {
55
+ return this._singleUpload(file, key, onProgress, signal);
56
+ }
57
+ return this._multipartUpload(file, key, onProgress, signal);
58
+ }
59
+
60
+ // ─── Single-Part Upload (≤5MB) ───────────────────────────────────────────────
61
+
62
+ async _singleUpload(file, key, onProgress, signal) {
63
+ onProgress(0);
64
+
65
+ const { signedUrl, location } = await this._callBackend({
66
+ type: 'single',
67
+ key,
68
+ contentType: file.type || 'application/octet-stream',
69
+ size: file.size,
70
+ });
71
+
72
+ await this._putBlob(signedUrl, file, file.type, (loaded) => {
73
+ onProgress(Math.round((loaded / file.size) * 100));
74
+ }, signal);
75
+
76
+ onProgress(100);
77
+ return { key, location };
78
+ }
79
+
80
+ // ─── Multipart Upload (>5MB) ─────────────────────────────────────────────────
81
+
82
+ async _multipartUpload(file, key, onProgress, signal) {
83
+ // Step 1 — Initiate: backend calls CreateMultipartUpload, returns uploadId
84
+ const { uploadId } = await this._callBackend({
85
+ type: 'initiate',
86
+ key,
87
+ contentType: file.type || 'application/octet-stream',
88
+ });
89
+
90
+ const chunks = this._splitFile(file);
91
+ const partProgress = new Array(chunks.length).fill(0);
92
+
93
+ const reportProgress = () => {
94
+ const uploaded = partProgress.reduce((s, v) => s + v, 0);
95
+ onProgress(Math.round((uploaded / file.size) * 100));
96
+ };
97
+
98
+ // Step 2 — Upload parts concurrently
99
+ const completedParts = [];
100
+ const queue = chunks.map((chunk, i) => ({ chunk, partNumber: i + 1, index: i }));
101
+
102
+ const worker = async () => {
103
+ while (queue.length > 0) {
104
+ const { chunk, partNumber, index } = queue.shift();
105
+
106
+ if (signal?.aborted) throw new DOMException('Upload aborted', 'AbortError');
107
+
108
+ // Get a signed URL for this specific part
109
+ const { signedUrl } = await this._callBackend({
110
+ type: 'part',
111
+ key,
112
+ uploadId,
113
+ partNumber,
114
+ });
115
+
116
+ // PUT the chunk; OSS returns ETag in response header
117
+ const rawETag = await this._putBlob(
118
+ signedUrl,
119
+ chunk,
120
+ file.type || 'application/octet-stream',
121
+ (loaded) => { partProgress[index] = loaded; reportProgress(); },
122
+ signal,
123
+ /* returnETag= */ true,
124
+ );
125
+
126
+ // OSS sometimes returns ETag without surrounding quotes — normalise
127
+ const etag = rawETag?.replace(/"/g, '') ? `"${rawETag.replace(/"/g, '')}"` : rawETag;
128
+
129
+ completedParts.push({ PartNumber: partNumber, ETag: etag });
130
+ }
131
+ };
132
+
133
+ const workers = Array.from({ length: this.maxConcurrent }, worker);
134
+ try {
135
+ await Promise.all(workers);
136
+ } catch (err) {
137
+ // Best-effort abort to avoid orphaned multipart uploads costing storage
138
+ await this._callBackend({ type: 'abort', key, uploadId }).catch(() => {});
139
+ throw err;
140
+ }
141
+
142
+ // Step 3 — Complete: parts must be sorted by PartNumber
143
+ completedParts.sort((a, b) => a.PartNumber - b.PartNumber);
144
+
145
+ const { location } = await this._callBackend({
146
+ type: 'complete',
147
+ key,
148
+ uploadId,
149
+ parts: completedParts,
150
+ });
151
+
152
+ onProgress(100);
153
+ return { key, location };
154
+ }
155
+
156
+ // ─── Helpers ─────────────────────────────────────────────────────────────────
157
+
158
+ _splitFile(file) {
159
+ const chunks = [];
160
+ for (let offset = 0; offset < file.size; offset += this.chunkSize) {
161
+ chunks.push(file.slice(offset, offset + this.chunkSize));
162
+ }
163
+ return chunks;
164
+ }
165
+
166
+ async _callBackend(payload) {
167
+ const res = await fetch(this.signEndpoint, {
168
+ method: 'POST',
169
+ headers: { 'Content-Type': 'application/json' },
170
+ body: JSON.stringify(payload),
171
+ });
172
+ if (!res.ok) {
173
+ const text = await res.text();
174
+ throw new Error(`Backend error (${res.status}): ${text}`);
175
+ }
176
+ return res.json();
177
+ }
178
+
179
+ /**
180
+ * PUT a Blob to a pre-signed OSS URL via XHR (for upload progress events).
181
+ *
182
+ * OSS S3-compatible PUT requires:
183
+ * - Content-Type header must match the value used when signing
184
+ * - Do NOT send Content-MD5 unless you included it in the signed headers
185
+ *
186
+ * @returns {Promise<string|undefined>} ETag value when returnETag=true
187
+ */
188
+ _putBlob(signedUrl, blob, contentType, onProgress, signal, returnETag = false) {
189
+ return new Promise((resolve, reject) => {
190
+ const xhr = new XMLHttpRequest();
191
+ xhr.open('PUT', signedUrl);
192
+
193
+ // Content-Type must match what was signed on the backend
194
+ if (contentType) xhr.setRequestHeader('Content-Type', contentType);
195
+
196
+ xhr.upload.addEventListener('progress', (e) => {
197
+ if (e.lengthComputable) onProgress(e.loaded);
198
+ });
199
+
200
+ xhr.addEventListener('load', () => {
201
+ if (xhr.status >= 200 && xhr.status < 300) {
202
+ resolve(returnETag ? xhr.getResponseHeader('ETag') : undefined);
203
+ } else {
204
+ // OSS returns XML error bodies — surface them for easier debugging
205
+ reject(new Error(`OSS PUT failed (HTTP ${xhr.status}): ${xhr.responseText}`));
206
+ }
207
+ });
208
+
209
+ xhr.addEventListener('error', () => reject(new Error('Network error during OSS upload')));
210
+ xhr.addEventListener('abort', () => reject(new DOMException('Upload aborted', 'AbortError')));
211
+
212
+ if (signal) signal.addEventListener('abort', () => xhr.abort(), { once: true });
213
+
214
+ xhr.send(blob);
215
+ });
216
+ }
217
+ }
218
+
219
+ export default OSSUploader;
@@ -0,0 +1,199 @@
1
+ /**
2
+ * Alibaba Cloud OSS — Signed URL Backend (Node.js / Express)
3
+ * Uses the AWS SDK v3 pointed at OSS's S3-compatible endpoint.
4
+ *
5
+ * Install:
6
+ * npm install @aws-sdk/client-s3 @aws-sdk/s3-request-presigner express
7
+ *
8
+ * Required env vars:
9
+ * OSS_REGION e.g. oss-ap-southeast-1
10
+ * OSS_BUCKET your bucket name
11
+ * OSS_ACCESS_KEY_ID Alibaba RAM user AccessKey ID
12
+ * OSS_ACCESS_KEY_SECRET
13
+ *
14
+ * OSS S3-compatible endpoint format:
15
+ * https://<region>.aliyuncs.com (global/internal)
16
+ * https://<region>.aliyuncs.com (use forcePathStyle: false — virtual-hosted default)
17
+ *
18
+ * Compatibility notes:
19
+ * - OSS supports Signature V4 (same as S3) via the S3-compatible API
20
+ * - Use forcePathStyle: false (virtual-hosted style) — OSS prefers bucket in hostname
21
+ * - OSS presigned URLs use x-oss-* headers, but the S3 SDK generates x-amz-* — this is
22
+ * fine because OSS accepts both via the S3-compat layer
23
+ * - ETag exposure in CORS must be configured on the OSS bucket (see notes at bottom)
24
+ */
25
+
26
+ import express from 'express';
27
+ import {
28
+ S3Client,
29
+ PutObjectCommand,
30
+ CreateMultipartUploadCommand,
31
+ UploadPartCommand,
32
+ CompleteMultipartUploadCommand,
33
+ AbortMultipartUploadCommand,
34
+ } from '@aws-sdk/client-s3';
35
+ import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
36
+
37
+ const app = express();
38
+ app.use(express.json());
39
+
40
+ // ─── OSS Client via S3-compatible endpoint ────────────────────────────────────
41
+
42
+ const REGION = process.env.OSS_REGION; // e.g. 'oss-ap-southeast-1'
43
+ const BUCKET = process.env.OSS_BUCKET;
44
+ const URL_TTL = 3600; // signed URL valid for 1 hour
45
+
46
+ if (!REGION || !BUCKET) {
47
+ throw new Error('OSS_REGION and OSS_BUCKET env vars are required');
48
+ }
49
+
50
+ const ossClient = new S3Client({
51
+ // OSS S3-compatible endpoint — region subdomain, no bucket prefix here
52
+ endpoint: `https://${REGION}.aliyuncs.com`,
53
+
54
+ region: REGION,
55
+
56
+ // Virtual-hosted style: bucket is placed in the hostname (recommended for OSS)
57
+ // e.g. https://my-bucket.oss-ap-southeast-1.aliyuncs.com/object-key
58
+ forcePathStyle: false,
59
+
60
+ credentials: {
61
+ accessKeyId: process.env.OSS_ACCESS_KEY_ID,
62
+ secretAccessKey: process.env.OSS_ACCESS_KEY_SECRET,
63
+ },
64
+ });
65
+
66
+ // ─── Single endpoint handling all upload lifecycle phases ─────────────────────
67
+ // POST /api/oss/sign
68
+ // Body: { type, key, contentType?, size?, uploadId?, partNumber?, parts? }
69
+
70
+ app.post('/api/oss/sign', async (req, res) => {
71
+ const { type, key, contentType, size, uploadId, partNumber, parts } = req.body;
72
+
73
+ if (!key) return res.status(400).json({ error: 'key is required' });
74
+
75
+ try {
76
+ switch (type) {
77
+
78
+ // ── 1. Single PUT (≤5MB) ───────────────────────────────────────────────
79
+ case 'single': {
80
+ const cmd = new PutObjectCommand({
81
+ Bucket: BUCKET,
82
+ Key: key,
83
+ ContentType: contentType || 'application/octet-stream',
84
+ ContentLength: size,
85
+ });
86
+ const signedUrl = await getSignedUrl(ossClient, cmd, { expiresIn: URL_TTL });
87
+
88
+ // OSS virtual-hosted URL for the completed object
89
+ const location = `https://${BUCKET}.${REGION}.aliyuncs.com/${key}`;
90
+ return res.json({ signedUrl, location });
91
+ }
92
+
93
+ // ── 2. Initiate multipart ──────────────────────────────────────────────
94
+ case 'initiate': {
95
+ const cmd = new CreateMultipartUploadCommand({
96
+ Bucket: BUCKET,
97
+ Key: key,
98
+ ContentType: contentType || 'application/octet-stream',
99
+ });
100
+ const response = await ossClient.send(cmd);
101
+ return res.json({ uploadId: response.UploadId });
102
+ }
103
+
104
+ // ── 3. Sign a part URL ─────────────────────────────────────────────────
105
+ case 'part': {
106
+ if (!uploadId || !partNumber) {
107
+ return res.status(400).json({ error: 'uploadId and partNumber are required' });
108
+ }
109
+ const cmd = new UploadPartCommand({
110
+ Bucket: BUCKET,
111
+ Key: key,
112
+ UploadId: uploadId,
113
+ PartNumber: Number(partNumber),
114
+ });
115
+ const signedUrl = await getSignedUrl(ossClient, cmd, { expiresIn: URL_TTL });
116
+ return res.json({ signedUrl });
117
+ }
118
+
119
+ // ── 4. Complete multipart ──────────────────────────────────────────────
120
+ case 'complete': {
121
+ if (!uploadId || !Array.isArray(parts) || parts.length === 0) {
122
+ return res.status(400).json({ error: 'uploadId and parts[] are required' });
123
+ }
124
+ const cmd = new CompleteMultipartUploadCommand({
125
+ Bucket: BUCKET,
126
+ Key: key,
127
+ UploadId: uploadId,
128
+ MultipartUpload: {
129
+ // OSS is strict: parts must be sorted by PartNumber
130
+ Parts: parts
131
+ .slice()
132
+ .sort((a, b) => a.PartNumber - b.PartNumber)
133
+ .map(({ PartNumber, ETag }) => ({ PartNumber, ETag })),
134
+ },
135
+ });
136
+ const response = await ossClient.send(cmd);
137
+ const location = response.Location ||
138
+ `https://${BUCKET}.${REGION}.aliyuncs.com/${key}`;
139
+ return res.json({ location });
140
+ }
141
+
142
+ // ── 5. Abort multipart (clean up on cancel/error) ──────────────────────
143
+ case 'abort': {
144
+ if (!uploadId) return res.status(400).json({ error: 'uploadId is required' });
145
+ const cmd = new AbortMultipartUploadCommand({
146
+ Bucket: BUCKET,
147
+ Key: key,
148
+ UploadId: uploadId,
149
+ });
150
+ await ossClient.send(cmd);
151
+ return res.json({ success: true });
152
+ }
153
+
154
+ default:
155
+ return res.status(400).json({ error: `Unknown type: "${type}"` });
156
+ }
157
+ } catch (err) {
158
+ console.error('[OSS sign error]', err);
159
+ res.status(500).json({ error: err.message });
160
+ }
161
+ });
162
+
163
+ app.listen(3000, () => console.log('OSS sign server running on http://localhost:3000'));
164
+ export default app;
165
+
166
+ /*
167
+ * ─── OSS Bucket CORS Configuration (required) ────────────────────────────────
168
+ *
169
+ * In the Alibaba Cloud OSS console → Bucket → Data Security → CORS:
170
+ *
171
+ * Rule:
172
+ * Allowed Origins: https://your-frontend-domain.com (or * for dev)
173
+ * Allowed Methods: PUT
174
+ * Allowed Headers: *
175
+ * Exposed Headers: ETag ← CRITICAL for multipart complete
176
+ * Max Age (seconds): 3600
177
+ *
178
+ * Without exposing ETag, the browser cannot read it from the PUT response
179
+ * and multipart complete will fail.
180
+ *
181
+ * ─── OSS RAM Policy (minimum permissions for the signing user) ───────────────
182
+ *
183
+ * {
184
+ * "Statement": [{
185
+ * "Effect": "Allow",
186
+ * "Action": [
187
+ * "oss:PutObject",
188
+ * "oss:InitiateMultipartUpload",
189
+ * "oss:UploadPart",
190
+ * "oss:CompleteMultipartUpload",
191
+ * "oss:AbortMultipartUpload"
192
+ * ],
193
+ * "Resource": [
194
+ * "acs:oss:*:*:your-bucket-name",
195
+ * "acs:oss:*:*:your-bucket-name/*"
196
+ * ]
197
+ * }]
198
+ * }
199
+ */
@@ -0,0 +1,121 @@
1
+ /**
2
+ * Alibaba OSS Uploader — Usage Examples
3
+ */
4
+
5
+ // ─── 1. Vanilla JS ────────────────────────────────────────────────────────────
6
+ import OSSUploader from './oss-uploader-client.js';
7
+
8
+ const uploader = new OSSUploader({
9
+ signEndpoint: '/api/oss/sign',
10
+ chunkSize: 10 * 1024 * 1024, // 10MB
11
+ maxConcurrent: 3,
12
+ });
13
+
14
+ // Basic upload
15
+ async function uploadFile(file) {
16
+ try {
17
+ const result = await uploader.upload(file, {
18
+ key: `uploads/${Date.now()}-${file.name}`,
19
+ onProgress: (pct) => {
20
+ console.log(`${pct}%`);
21
+ document.getElementById('progress').value = pct;
22
+ },
23
+ });
24
+ console.log('Done:', result.location);
25
+ } catch (err) {
26
+ console.error('Upload failed:', err.message);
27
+ }
28
+ }
29
+
30
+ // With cancel support
31
+ let controller = null;
32
+
33
+ async function uploadWithCancel(file) {
34
+ controller = new AbortController();
35
+ try {
36
+ const result = await uploader.upload(file, {
37
+ key: `uploads/${file.name}`,
38
+ onProgress: (pct) => console.log(`${pct}%`),
39
+ signal: controller.signal,
40
+ });
41
+ console.log('Uploaded to:', result.location);
42
+ } catch (err) {
43
+ if (err.name === 'AbortError') {
44
+ console.log('Upload cancelled');
45
+ // The client already called /api/oss/sign with type=abort to clean up OSS
46
+ } else {
47
+ console.error(err);
48
+ }
49
+ }
50
+ }
51
+
52
+ document.getElementById('fileInput').addEventListener('change', (e) => {
53
+ if (e.target.files[0]) uploadWithCancel(e.target.files[0]);
54
+ });
55
+ document.getElementById('cancelBtn').addEventListener('click', () => controller?.abort());
56
+
57
+
58
+ // ─── 2. React component ───────────────────────────────────────────────────────
59
+ /*
60
+ import { useState, useRef } from 'react';
61
+ import OSSUploader from './oss-uploader-client.js';
62
+
63
+ const uploader = new OSSUploader({ signEndpoint: '/api/oss/sign' });
64
+
65
+ export function OSSFileUpload() {
66
+ const [progress, setProgress] = useState(0);
67
+ const [status, setStatus] = useState('idle'); // idle | uploading | done | error
68
+ const [location, setLocation] = useState('');
69
+ const controllerRef = useRef(null);
70
+
71
+ const handleFileChange = async (e) => {
72
+ const file = e.target.files[0];
73
+ if (!file) return;
74
+
75
+ controllerRef.current = new AbortController();
76
+ setStatus('uploading');
77
+ setProgress(0);
78
+
79
+ try {
80
+ const result = await uploader.upload(file, {
81
+ key: `uploads/${Date.now()}-${file.name}`,
82
+ onProgress: setProgress,
83
+ signal: controllerRef.current.signal,
84
+ });
85
+ setStatus('done');
86
+ setLocation(result.location);
87
+ } catch (err) {
88
+ if (err.name === 'AbortError') {
89
+ setStatus('idle');
90
+ } else {
91
+ setStatus('error');
92
+ console.error(err);
93
+ }
94
+ }
95
+ };
96
+
97
+ return (
98
+ <div>
99
+ <input type="file" onChange={handleFileChange} disabled={status === 'uploading'} />
100
+ {status === 'uploading' && (
101
+ <>
102
+ <progress value={progress} max={100} />
103
+ <span>{progress}%</span>
104
+ <button onClick={() => controllerRef.current?.abort()}>Cancel</button>
105
+ </>
106
+ )}
107
+ {status === 'done' && <p>Uploaded: <a href={location}>{location}</a></p>}
108
+ {status === 'error' && <p style={{ color: 'red' }}>Upload failed</p>}
109
+ </div>
110
+ );
111
+ }
112
+ */
113
+
114
+
115
+ /*
116
+ Minimal HTML (vanilla):
117
+
118
+ <input type="file" id="fileInput" />
119
+ <progress id="progress" max="100" value="0"></progress>
120
+ <button id="cancelBtn">Cancel</button>
121
+ */
@@ -0,0 +1,34 @@
1
+ Here's a comprehensive JavaScript solution for reading and uploading large files to S3 using signed URLs, with multipart upload support for reliability:Here's a breakdown of what's in each file:
2
+
3
+ ---
4
+
5
+ **`s3-uploader-client.js`** — Browser-side `S3Uploader` class
6
+ - Files **≤ 5MB** → single `PUT` with a signed URL
7
+ - Files **> 5MB** → S3 multipart upload (10MB chunks by default)
8
+ - Concurrent part uploads (default: 3 in parallel) for speed
9
+ - `XMLHttpRequest`-based for accurate `upload.progress` events
10
+ - `AbortController` support to cancel mid-upload
11
+
12
+ **`s3-uploader-server.js`** — Node.js/Express backend
13
+ - Single `/api/s3/sign` endpoint handles all 5 phases: `single`, `initiate`, `part`, `complete`, `abort`
14
+ - Uses the AWS SDK v3 (`@aws-sdk/client-s3` + `@aws-sdk/s3-request-presigner`)
15
+ - Your AWS credentials never touch the browser
16
+
17
+ **`s3-uploader-usage.js`** — Integration examples with cancel support
18
+
19
+ ---
20
+
21
+ **Quick setup:**
22
+
23
+ ```bash
24
+ # Backend
25
+ npm install @aws-sdk/client-s3 @aws-sdk/s3-request-presigner express
26
+ ```
27
+
28
+ Set env vars: `AWS_REGION`, `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, `S3_BUCKET_NAME`
29
+
30
+ **S3 CORS config** you'll also need on your bucket:
31
+ ```json
32
+ [{ "AllowedOrigins": ["*"], "AllowedMethods": ["PUT"], "AllowedHeaders": ["*"], "ExposeHeaders": ["ETag"] }]
33
+ ```
34
+ The `ETag` exposure is critical — S3 multipart complete requires the ETags from each part.