@karpeleslab/klbfw 0.2.18 → 0.2.20

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/upload.js CHANGED
@@ -1,62 +1,9 @@
1
1
  /**
2
2
  * KLB Upload Module
3
- *
4
- * This module handles file uploads to KLB API endpoints.
5
- * It supports both browser and Node.js environments with a unified API.
6
- *
7
- * The module handles:
8
- * - File upload to KLB API endpoints
9
- * - Multiple upload protocols (PUT and AWS multipart)
10
- * - Progress tracking
11
- * - Pause, resume, retry, and cancel operations
12
- * - Browser and Node.js compatibility
13
- *
14
- * Browser usage:
15
- * ```js
16
- * // Open file picker and upload selected files
17
- * upload.init('Misc/Debug:testUpload')()
18
- * .then(result => console.log('Upload complete', result));
19
- *
20
- * // Open file picker with custom parameters and notification callback
21
- * upload.init('Support/Ticket:upload', {image_variation: 'alias=mini&strip&scale_crop=300x200'}, (result) => {
22
- * if (result.status == 'complete') console.log(result.final);
23
- * });
24
- *
25
- * // Upload a specific File object
26
- * upload.append('Misc/Debug:testUpload', fileObject)
27
- * .then(result => console.log('Upload complete', result));
28
- *
29
- * // Track progress
30
- * upload.onprogress = (status) => {
31
- * console.log('Progress:', status.running.map(i => i.status));
32
- * };
33
- *
34
- * // Cancel an upload
35
- * upload.cancelItem(uploadId);
36
- * ```
37
- *
38
- * Node.js usage:
39
- * ```js
40
- * // For Node.js environments, first install dependencies:
41
- * // npm install node-fetch @xmldom/xmldom
42
3
  *
43
- * // Simple upload with a buffer
44
- * const { uploadFile } = require('./upload');
45
- * const buffer = Buffer.from('Hello, World!');
46
- * const result = await uploadFile('Misc/Debug:testUpload', buffer, 'POST', {
47
- * filename: 'hello.txt',
48
- * type: 'text/plain'
49
- * });
4
+ * This module provides the uploadFile function for uploading files to KLB API endpoints.
5
+ * It supports both browser and Node.js environments with a unified API.
50
6
  *
51
- * // Upload large files using a stream (doesn't load entire file into memory)
52
- * const fs = require('fs');
53
- * const stream = fs.createReadStream('/path/to/2tb-file.bin');
54
- * const result = await uploadFile('Misc/Debug:testUpload', stream, 'POST', {
55
- * filename: 'large-file.bin',
56
- * type: 'application/octet-stream'
57
- * });
58
- * ```
59
- *
60
7
  * @module upload
61
8
  */
62
9
 
@@ -64,213 +11,13 @@
64
11
 
65
12
  const rest = require('./rest');
66
13
  const fwWrapper = require('./fw-wrapper');
67
- const sha256 = require('js-sha256').sha256;
68
-
69
- /**
70
- * Environment detection and cross-platform utilities
71
- */
72
- const env = {
73
- /**
74
- * Detect if running in a browser environment
75
- */
76
- isBrowser: typeof window !== 'undefined' && typeof document !== 'undefined',
77
-
78
- /**
79
- * Detect if running in a Node.js environment
80
- */
81
- isNode: typeof process !== 'undefined' && process.versions && process.versions.node,
82
-
83
- /**
84
- * Node.js specific modules (lazy-loaded)
85
- */
86
- node: {
87
- fetch: null,
88
- xmlParser: null,
89
- EventEmitter: null,
90
- eventEmitter: null
91
- }
92
- };
93
-
94
- /**
95
- * Initialize Node.js dependencies when in Node environment
96
- */
97
- if (env.isNode && !env.isBrowser) {
98
- try {
99
- env.node.fetch = require('node-fetch');
100
- env.node.xmlParser = require('@xmldom/xmldom');
101
- env.node.EventEmitter = require('events');
102
- env.node.eventEmitter = new (env.node.EventEmitter)();
103
- } catch (e) {
104
- console.warn('Node.js dependencies not available. Some functionality may be limited:', e.message);
105
- console.warn('To use in Node.js, install: npm install node-fetch @xmldom/xmldom');
106
- }
107
- }
108
-
109
- /**
110
- * Cross-platform utilities
111
- */
112
- const utils = {
113
- /**
114
- * Environment-agnostic fetch implementation
115
- * @param {string} url - The URL to fetch
116
- * @param {Object} options - Fetch options
117
- * @returns {Promise} - Fetch promise
118
- */
119
- fetch(url, options) {
120
- if (env.isBrowser && typeof window.fetch === 'function') {
121
- return window.fetch(url, options);
122
- } else if (env.isNode && env.node.fetch) {
123
- return env.node.fetch(url, options);
124
- } else if (typeof fetch === 'function') {
125
- // For environments where fetch is globally available
126
- return fetch(url, options);
127
- }
128
- return Promise.reject(new Error('fetch not available in this environment'));
129
- },
130
-
131
- /**
132
- * Environment-agnostic XML parser
133
- * @param {string} xmlString - XML string to parse
134
- * @returns {Document} - DOM-like document
135
- */
136
- parseXML(xmlString) {
137
- if (env.isBrowser) {
138
- return new DOMParser().parseFromString(xmlString, 'text/xml');
139
- } else if (env.isNode && env.node.xmlParser) {
140
- const DOMParserNode = env.node.xmlParser.DOMParser;
141
- const dom = new DOMParserNode().parseFromString(xmlString, 'text/xml');
142
-
143
- // Add querySelector interface for compatibility
144
- dom.querySelector = function(selector) {
145
- if (selector === 'UploadId') {
146
- const elements = this.getElementsByTagName('UploadId');
147
- return elements.length > 0 ? { innerHTML: elements[0].textContent } : null;
148
- }
149
- return null;
150
- };
151
-
152
- return dom;
153
- }
154
- throw new Error('XML parsing not available in this environment');
155
- },
156
-
157
- /**
158
- * Read file content as ArrayBuffer
159
- * Compatible with browser File objects and custom objects with content/slice
160
- *
161
- * @param {File|Object} file - File object or file-like object
162
- * @param {Object} options - Options for reading (start, end)
163
- * @param {Function} callback - Callback function(buffer, error)
164
- */
165
- readAsArrayBuffer(file, options, callback) {
166
- // Handle case where options is the callback
167
- if (typeof options === 'function') {
168
- callback = options;
169
- options = {};
170
- }
171
- options = options || {};
172
-
173
- if (env.isBrowser && file instanceof File) {
174
- // Browser: use native File API
175
- const start = options.start || 0;
176
- const end = options.end || file.size;
177
- const slice = file.slice(start, end);
178
-
179
- const reader = new FileReader();
180
- reader.addEventListener('loadend', () => callback(reader.result));
181
- reader.addEventListener('error', (e) => callback(null, e));
182
- reader.readAsArrayBuffer(slice);
183
- } else if (file.content) {
184
- // Memory buffer-based file
185
- const start = options.start || 0;
186
- const end = options.end || file.content.length || file.content.byteLength;
187
- let content = file.content;
188
-
189
- // Handle various content types
190
- if (content instanceof ArrayBuffer) {
191
- // Already an ArrayBuffer
192
- if (start === 0 && end === content.byteLength) {
193
- callback(content);
194
- } else {
195
- callback(content.slice(start, end));
196
- }
197
- } else if (content.buffer instanceof ArrayBuffer) {
198
- // TypedArray (Uint8Array, etc.)
199
- callback(content.buffer.slice(start, end));
200
- } else if (typeof Buffer !== 'undefined' && content instanceof Buffer) {
201
- // Node.js Buffer
202
- const arrayBuffer = content.buffer.slice(
203
- content.byteOffset + start,
204
- content.byteOffset + Math.min(end, content.byteLength)
205
- );
206
- callback(arrayBuffer);
207
- } else if (typeof content === 'string') {
208
- // String content - convert to ArrayBuffer
209
- const encoder = new TextEncoder();
210
- const uint8Array = encoder.encode(content.slice(start, end));
211
- callback(uint8Array.buffer);
212
- } else {
213
- callback(null, new Error('Unsupported content type'));
214
- }
215
- } else if (file.slice) {
216
- // Object with slice method (custom implementation)
217
- const start = options.start || 0;
218
- const end = options.end;
219
- const slice = file.slice(start, end);
220
-
221
- // Recursively handle the slice
222
- utils.readAsArrayBuffer(slice, callback);
223
- } else {
224
- callback(null, new Error('Cannot read file content - no supported method available'));
225
- }
226
- },
227
-
228
- /**
229
- * Dispatch a custom event in any environment
230
- * @param {string} eventName - Event name
231
- * @param {Object} detail - Event details
232
- */
233
- dispatchEvent(eventName, detail) {
234
- if (env.isBrowser) {
235
- const evt = new CustomEvent(eventName, { detail });
236
- document.dispatchEvent(evt);
237
- } else if (env.isNode && env.node.eventEmitter) {
238
- env.node.eventEmitter.emit(eventName, detail);
239
- }
240
- // In other environments, events are silently ignored
241
- },
242
-
243
- /**
244
- * Format a date for AWS (YYYYMMDDTHHMMSSZ)
245
- * @returns {string} Formatted date
246
- */
247
- getAmzTime() {
248
- const t = new Date();
249
- return t.getUTCFullYear() +
250
- this.pad(t.getUTCMonth() + 1) +
251
- this.pad(t.getUTCDate()) +
252
- 'T' + this.pad(t.getUTCHours()) +
253
- this.pad(t.getUTCMinutes()) +
254
- this.pad(t.getUTCSeconds()) +
255
- 'Z';
256
- },
257
-
258
- /**
259
- * Pad a number with leading zero if needed
260
- * @param {number} number - Number to pad
261
- * @returns {string} Padded number
262
- */
263
- pad(number) {
264
- return number < 10 ? '0' + number : String(number);
265
- }
266
- };
14
+ const { env, utils, awsReq, readChunkFromStream, readFileSlice } = require('./upload-internal');
267
15
 
268
16
  /**
269
- * Simple file upload for Node.js consumers
17
+ * Simple file upload function
270
18
  *
271
19
  * This function provides a straightforward way to upload a file and get a Promise
272
- * that resolves when the upload is complete. It doesn't use global state or the
273
- * upload.run() process.
20
+ * that resolves when the upload is complete.
274
21
  *
275
22
  * @param {string} api - API endpoint path (e.g., 'Misc/Debug:testUpload')
276
23
  * @param {Buffer|ArrayBuffer|Uint8Array|File|Object} buffer - File to upload. Can be:
@@ -358,7 +105,7 @@ async function uploadFile(api, buffer, method, params, context, options) {
358
105
  name: params.filename || 'file.txt',
359
106
  size: uint8Array.length,
360
107
  type: params.type || 'text/plain',
361
- lastModified: Date.now(),
108
+ lastModified: Date.now() / 1000,
362
109
  content: uint8Array.buffer
363
110
  };
364
111
  }
@@ -368,7 +115,7 @@ async function uploadFile(api, buffer, method, params, context, options) {
368
115
  name: params.filename || 'file.bin',
369
116
  size: buffer.byteLength,
370
117
  type: params.type || 'application/octet-stream',
371
- lastModified: Date.now(),
118
+ lastModified: Date.now() / 1000,
372
119
  content: buffer
373
120
  };
374
121
  }
@@ -378,7 +125,7 @@ async function uploadFile(api, buffer, method, params, context, options) {
378
125
  name: params.filename || 'file.bin',
379
126
  size: buffer.byteLength,
380
127
  type: params.type || 'application/octet-stream',
381
- lastModified: Date.now(),
128
+ lastModified: Date.now() / 1000,
382
129
  content: buffer
383
130
  };
384
131
  }
@@ -388,27 +135,27 @@ async function uploadFile(api, buffer, method, params, context, options) {
388
135
  name: params.filename || 'file.bin',
389
136
  size: buffer.length,
390
137
  type: params.type || 'application/octet-stream',
391
- lastModified: Date.now(),
138
+ lastModified: Date.now() / 1000,
392
139
  content: buffer
393
140
  };
394
141
  }
395
142
  // Handle browser File object
396
143
  else if (env.isBrowser && typeof File !== 'undefined' && buffer instanceof File) {
397
144
  fileObj = {
398
- name: buffer.name || params.filename || 'file.bin',
145
+ name: params.filename || buffer.name || 'file.bin',
399
146
  size: buffer.size,
400
- type: buffer.type || params.type || 'application/octet-stream',
401
- lastModified: buffer.lastModified || Date.now(),
147
+ type: params.type || buffer.type || 'application/octet-stream',
148
+ lastModified: (buffer.lastModified || Date.now()) / 1000,
402
149
  browserFile: buffer // Keep reference to original File for reading
403
150
  };
404
151
  }
405
152
  // Handle file-like object with content property
406
153
  else if (buffer && buffer.content !== undefined) {
407
154
  fileObj = {
408
- name: buffer.name || params.filename || 'file.bin',
155
+ name: params.filename || buffer.name || 'file.bin',
409
156
  size: buffer.size || buffer.content.byteLength || buffer.content.length,
410
- type: buffer.type || params.type || 'application/octet-stream',
411
- lastModified: buffer.lastModified || Date.now(),
157
+ type: params.type || buffer.type || 'application/octet-stream',
158
+ lastModified: (buffer.lastModified || Date.now()) / 1000,
412
159
  content: buffer.content
413
160
  };
414
161
  }
@@ -418,7 +165,7 @@ async function uploadFile(api, buffer, method, params, context, options) {
418
165
  name: params.filename || 'file.bin',
419
166
  size: params.size || null, // null means unknown size
420
167
  type: params.type || 'application/octet-stream',
421
- lastModified: Date.now(),
168
+ lastModified: Date.now() / 1000,
422
169
  stream: buffer
423
170
  };
424
171
  }
@@ -430,7 +177,7 @@ async function uploadFile(api, buffer, method, params, context, options) {
430
177
  const uploadParams = { ...params };
431
178
  uploadParams.filename = fileObj.name;
432
179
  uploadParams.size = fileObj.size;
433
- uploadParams.lastModified = fileObj.lastModified / 1000;
180
+ uploadParams.lastModified = fileObj.lastModified;
434
181
  uploadParams.type = fileObj.type;
435
182
 
436
183
  // Initialize upload with the server
@@ -493,8 +240,10 @@ async function doPutUpload(file, uploadInfo, context, options) {
493
240
  const startByte = byteOffset;
494
241
  byteOffset += chunkData.byteLength;
495
242
 
243
+ // Only add Content-Range for multi-block uploads
244
+ const useContentRange = blocks === null || blocks > 1;
496
245
  const uploadPromise = uploadPutBlockWithDataAndRetry(
497
- uploadInfo, currentBlock, startByte, chunkData, file.type, onError
246
+ uploadInfo, currentBlock, startByte, chunkData, file.type, onError, useContentRange
498
247
  ).then(() => {
499
248
  completedBlocks++;
500
249
  if (onProgress && blocks) {
@@ -507,17 +256,10 @@ async function doPutUpload(file, uploadInfo, context, options) {
507
256
 
508
257
  // Wait for at least one upload to complete before reading more
509
258
  if (pendingUploads.length > 0) {
510
- await Promise.race(pendingUploads);
511
- // Remove completed promises
512
- for (let i = pendingUploads.length - 1; i >= 0; i--) {
513
- const status = await Promise.race([
514
- pendingUploads[i].then(() => 'done'),
515
- Promise.resolve('pending')
516
- ]);
517
- if (status === 'done') {
518
- pendingUploads.splice(i, 1);
519
- }
520
- }
259
+ // Create indexed promises that return their index when done
260
+ const indexedPromises = pendingUploads.map((p, idx) => p.then(() => idx));
261
+ const completedIdx = await Promise.race(indexedPromises);
262
+ pendingUploads.splice(completedIdx, 1);
521
263
  }
522
264
  }
523
265
 
@@ -564,7 +306,7 @@ async function doPutUpload(file, uploadInfo, context, options) {
564
306
  * Upload a single block via PUT with pre-read data and retry support
565
307
  * @private
566
308
  */
567
- async function uploadPutBlockWithDataAndRetry(uploadInfo, blockNum, startByte, data, contentType, onError) {
309
+ async function uploadPutBlockWithDataAndRetry(uploadInfo, blockNum, startByte, data, contentType, onError, useContentRange) {
568
310
  let attempt = 0;
569
311
  while (true) {
570
312
  attempt++;
@@ -573,8 +315,10 @@ async function uploadPutBlockWithDataAndRetry(uploadInfo, blockNum, startByte, d
573
315
  'Content-Type': contentType || 'application/octet-stream'
574
316
  };
575
317
 
576
- // Add Content-Range for multipart PUT
577
- headers['Content-Range'] = `bytes ${startByte}-${startByte + data.byteLength - 1}/*`;
318
+ // Add Content-Range for multipart PUT (not for single-block uploads)
319
+ if (useContentRange) {
320
+ headers['Content-Range'] = `bytes ${startByte}-${startByte + data.byteLength - 1}/*`;
321
+ }
578
322
 
579
323
  const response = await utils.fetch(uploadInfo.PUT, {
580
324
  method: 'PUT',
@@ -735,17 +479,10 @@ async function doAwsUpload(file, uploadInfo, context, options) {
735
479
 
736
480
  // Wait for at least one upload to complete before reading more
737
481
  if (pendingUploads.length > 0) {
738
- await Promise.race(pendingUploads);
739
- // Remove completed promises
740
- for (let i = pendingUploads.length - 1; i >= 0; i--) {
741
- const status = await Promise.race([
742
- pendingUploads[i].then(() => 'done'),
743
- Promise.resolve('pending')
744
- ]);
745
- if (status === 'done') {
746
- pendingUploads.splice(i, 1);
747
- }
748
- }
482
+ // Create indexed promises that return their index when done
483
+ const indexedPromises = pendingUploads.map((p, idx) => p.then(() => idx));
484
+ const completedIdx = await Promise.race(indexedPromises);
485
+ pendingUploads.splice(completedIdx, 1);
749
486
  }
750
487
  }
751
488
 
@@ -901,1040 +638,5 @@ async function uploadAwsBlock(file, uploadInfo, uploadId, blockNum, blockSize, c
901
638
  return etag;
902
639
  }
903
640
 
904
- /**
905
- * Read a chunk of specified size from a stream
906
- * @private
907
- * @param {ReadableStream} stream - Node.js readable stream
908
- * @param {number} size - Number of bytes to read
909
- * @returns {Promise<ArrayBuffer|null>} - ArrayBuffer with data, or null if stream ended
910
- */
911
- function readChunkFromStream(stream, size) {
912
- return new Promise((resolve, reject) => {
913
- const chunks = [];
914
- let bytesRead = 0;
915
-
916
- const onReadable = () => {
917
- let chunk;
918
- while (bytesRead < size && (chunk = stream.read(Math.min(size - bytesRead, 65536))) !== null) {
919
- chunks.push(chunk);
920
- bytesRead += chunk.length;
921
- }
922
-
923
- if (bytesRead >= size) {
924
- cleanup();
925
- resolve(combineChunks(chunks));
926
- }
927
- };
928
-
929
- const onEnd = () => {
930
- cleanup();
931
- if (bytesRead === 0) {
932
- resolve(null); // Stream ended, no more data
933
- } else {
934
- resolve(combineChunks(chunks));
935
- }
936
- };
937
-
938
- const onError = (err) => {
939
- cleanup();
940
- reject(err);
941
- };
942
-
943
- const cleanup = () => {
944
- stream.removeListener('readable', onReadable);
945
- stream.removeListener('end', onEnd);
946
- stream.removeListener('error', onError);
947
- };
948
-
949
- stream.on('readable', onReadable);
950
- stream.on('end', onEnd);
951
- stream.on('error', onError);
952
-
953
- // Try reading immediately in case data is already buffered
954
- onReadable();
955
- });
956
- }
957
-
958
- /**
959
- * Combine chunks into a single ArrayBuffer
960
- * @private
961
- */
962
- function combineChunks(chunks) {
963
- if (chunks.length === 0) {
964
- return new ArrayBuffer(0);
965
- }
966
- if (chunks.length === 1) {
967
- const chunk = chunks[0];
968
- return chunk.buffer.slice(chunk.byteOffset, chunk.byteOffset + chunk.length);
969
- }
970
- const totalLength = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
971
- const result = new Uint8Array(totalLength);
972
- let offset = 0;
973
- for (const chunk of chunks) {
974
- result.set(new Uint8Array(chunk.buffer, chunk.byteOffset, chunk.length), offset);
975
- offset += chunk.length;
976
- }
977
- return result.buffer;
978
- }
979
-
980
- /**
981
- * Read a slice of a file as ArrayBuffer
982
- * @private
983
- */
984
- function readFileSlice(file, start, end) {
985
- return new Promise((resolve, reject) => {
986
- // Handle browser File objects
987
- if (file.browserFile) {
988
- const slice = file.browserFile.slice(start, end);
989
- const reader = new FileReader();
990
- reader.addEventListener('loadend', () => resolve(reader.result));
991
- reader.addEventListener('error', (e) => reject(e));
992
- reader.readAsArrayBuffer(slice);
993
- return;
994
- }
995
-
996
- if (!file.content) {
997
- reject(new Error('Cannot read file content - no content property'));
998
- return;
999
- }
1000
-
1001
- const content = file.content;
1002
-
1003
- if (content instanceof ArrayBuffer) {
1004
- if (start === 0 && end === content.byteLength) {
1005
- resolve(content);
1006
- } else {
1007
- resolve(content.slice(start, end));
1008
- }
1009
- } else if (content.buffer instanceof ArrayBuffer) {
1010
- // TypedArray (Uint8Array, etc.)
1011
- resolve(content.buffer.slice(content.byteOffset + start, content.byteOffset + end));
1012
- } else if (typeof Buffer !== 'undefined' && content instanceof Buffer) {
1013
- // Node.js Buffer
1014
- const arrayBuffer = content.buffer.slice(
1015
- content.byteOffset + start,
1016
- content.byteOffset + Math.min(end, content.byteLength)
1017
- );
1018
- resolve(arrayBuffer);
1019
- } else if (typeof content === 'string') {
1020
- // String content
1021
- const encoder = new TextEncoder();
1022
- const uint8Array = encoder.encode(content.slice(start, end));
1023
- resolve(uint8Array.buffer);
1024
- } else {
1025
- reject(new Error('Unsupported content type'));
1026
- }
1027
- });
1028
- }
1029
-
1030
- /**
1031
- * AWS S3 request handler
1032
- * Performs a signed request to AWS S3 using a signature obtained from the server
1033
- *
1034
- * @param {Object} upInfo - Upload info including bucket endpoint and key
1035
- * @param {string} method - HTTP method (GET, POST, PUT)
1036
- * @param {string} query - Query parameters
1037
- * @param {*} body - Request body
1038
- * @param {Object} headers - Request headers
1039
- * @param {Object} context - Request context
1040
- * @returns {Promise} - Request promise
1041
- */
1042
- function awsReq(upInfo, method, query, body, headers, context) {
1043
- headers = headers || {};
1044
- context = context || {};
1045
-
1046
- // Calculate body hash for AWS signature
1047
- let bodyHash;
1048
-
1049
- if (!body || body === "") {
1050
- // Empty body hash
1051
- bodyHash = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
1052
- } else {
1053
- try {
1054
- // Handle different body types
1055
- let bodyForHash = body;
1056
-
1057
- if (body instanceof ArrayBuffer || (body.constructor && body.constructor.name === 'ArrayBuffer')) {
1058
- bodyForHash = new Uint8Array(body);
1059
- } else if (body.constructor && body.constructor.name === 'Buffer') {
1060
- bodyForHash = Buffer.from(body).toString();
1061
- }
1062
-
1063
- bodyHash = sha256(bodyForHash);
1064
- } catch (e) {
1065
- console.error("Error calculating hash:", e.message);
1066
- bodyHash = "UNSIGNED-PAYLOAD";
1067
- }
1068
- }
1069
-
1070
- // Create AWS timestamp
1071
- const timestamp = utils.getAmzTime();
1072
- const datestamp = timestamp.substring(0, 8);
1073
-
1074
- // Set AWS headers
1075
- headers["X-Amz-Content-Sha256"] = bodyHash;
1076
- headers["X-Amz-Date"] = timestamp;
1077
-
1078
- // Prepare the string to sign
1079
- const authStringParts = [
1080
- "AWS4-HMAC-SHA256",
1081
- timestamp,
1082
- `${datestamp}/${upInfo.Bucket_Endpoint.Region}/s3/aws4_request`,
1083
- method,
1084
- `/${upInfo.Bucket_Endpoint.Name}/${upInfo.Key}`,
1085
- query,
1086
- `host:${upInfo.Bucket_Endpoint.Host}`
1087
- ];
1088
-
1089
- // Add x-* headers to sign
1090
- const headersToSign = ['host'];
1091
- const sortedHeaderKeys = Object.keys(headers).sort();
1092
-
1093
- for (const key of sortedHeaderKeys) {
1094
- const lowerKey = key.toLowerCase();
1095
- if (lowerKey.startsWith('x-')) {
1096
- headersToSign.push(lowerKey);
1097
- authStringParts.push(`${lowerKey}:${headers[key]}`);
1098
- }
1099
- }
1100
-
1101
- // Complete the string to sign
1102
- authStringParts.push('');
1103
- authStringParts.push(headersToSign.join(';'));
1104
- authStringParts.push(bodyHash);
1105
-
1106
- return new Promise((resolve, reject) => {
1107
- // Get signature from server
1108
- rest.rest(
1109
- `Cloud/Aws/Bucket/Upload/${upInfo.Cloud_Aws_Bucket_Upload__}:signV4`,
1110
- "POST",
1111
- { headers: authStringParts.join("\n") },
1112
- context
1113
- )
1114
- .then(response => {
1115
- // Construct the S3 URL
1116
- let url = `https://${upInfo.Bucket_Endpoint.Host}/${upInfo.Bucket_Endpoint.Name}/${upInfo.Key}`;
1117
- if (query) url += `?${query}`;
1118
-
1119
- // Add the authorization header
1120
- headers["Authorization"] = response.data.authorization;
1121
-
1122
- // Make the actual request to S3
1123
- return utils.fetch(url, {
1124
- method,
1125
- body,
1126
- headers
1127
- });
1128
- })
1129
- .then(resolve)
1130
- .catch(reject);
1131
- });
1132
- }
1133
-
1134
- /**
1135
- * Upload module (IIFE pattern)
1136
- * @returns {Object} Upload interface
1137
- */
1138
- module.exports.upload = (function () {
1139
- /**
1140
- * Upload state
1141
- */
1142
- const state = {
1143
- queue: [], // Queued uploads
1144
- failed: [], // Failed uploads
1145
- running: {}, // Currently processing uploads
1146
- nextId: 0, // Next upload ID
1147
- lastInput: null // Last created file input element (browser only)
1148
- };
1149
-
1150
- // Public API object
1151
- const upload = {};
1152
-
1153
- /**
1154
- * Helper Functions
1155
- */
1156
-
1157
- /**
1158
- * Notify progress to listeners
1159
- * Calls onprogress callback and dispatches events
1160
- */
1161
- function sendProgress() {
1162
- const status = upload.getStatus();
1163
-
1164
- // Call the onprogress callback if defined
1165
- if (typeof upload.onprogress === "function") {
1166
- upload.onprogress(status);
1167
- }
1168
-
1169
- // Dispatch event for listeners
1170
- utils.dispatchEvent("upload:progress", status);
1171
- }
1172
-
1173
- /**
1174
- * Handle upload failure
1175
- * @param {Object} up - Upload object
1176
- * @param {*} error - Error data
1177
- */
1178
- function handleFailure(up, error) {
1179
- // Skip if upload is no longer running
1180
- if (!(up.up_id in state.running)) return;
1181
-
1182
- // Check if already in failed list
1183
- for (const failedItem of state.failed) {
1184
- if (failedItem.up_id === up.up_id) {
1185
- return; // Already recorded as failed
1186
- }
1187
- }
1188
-
1189
- // Record failure
1190
- up.failure = error;
1191
- state.failed.push(up);
1192
- delete state.running[up.up_id];
1193
-
1194
- // Reject the promise so callers know the upload failed
1195
- if (up.reject) {
1196
- up.reject(error);
1197
- }
1198
-
1199
- // Continue processing queue
1200
- upload.run();
1201
-
1202
- // Notify progress
1203
- sendProgress();
1204
-
1205
- // Dispatch failure event
1206
- utils.dispatchEvent("upload:failed", {
1207
- item: up,
1208
- res: error
1209
- });
1210
- }
1211
-
1212
- /**
1213
- * Process a pending upload
1214
- * Initiates the upload process with the server
1215
- * @param {Object} up - Upload object
1216
- */
1217
- function processUpload(up) {
1218
- // Mark as processing
1219
- up.status = "pending-wip";
1220
-
1221
- // Prepare parameters
1222
- const params = up.params || {};
1223
-
1224
- // Set file metadata
1225
- params.filename = up.file.name;
1226
- params.size = up.file.size;
1227
- params.lastModified = up.file.lastModified / 1000;
1228
- params.type = up.file.type || "application/octet-stream";
1229
-
1230
- // Initialize upload with the server
1231
- rest.rest(up.path, "POST", params, up.context)
1232
- .then(function(response) {
1233
- // Method 1: AWS signed multipart upload
1234
- if (response.data.Cloud_Aws_Bucket_Upload__) {
1235
- return handleAwsMultipartUpload(up, response.data);
1236
- }
1237
-
1238
- // Method 2: Direct PUT upload
1239
- if (response.data.PUT) {
1240
- return handlePutUpload(up, response.data);
1241
- }
1242
-
1243
- // Invalid response format
1244
- delete state.running[up.up_id];
1245
- state.failed.push(up);
1246
- up.reject(new Error('Invalid upload response format'));
1247
- })
1248
- .catch(error => handleFailure(up, error));
1249
- }
1250
-
1251
- /**
1252
- * Set up AWS multipart upload
1253
- * @param {Object} up - Upload object
1254
- * @param {Object} data - Server response data
1255
- */
1256
- function handleAwsMultipartUpload(up, data) {
1257
- // Store upload info
1258
- up.info = data;
1259
-
1260
- // Initialize multipart upload
1261
- return awsReq(
1262
- up.info,
1263
- "POST",
1264
- "uploads=",
1265
- "",
1266
- {"Content-Type": up.file.type || "application/octet-stream", "X-Amz-Acl": "private"},
1267
- up.context
1268
- )
1269
- .then(response => response.text())
1270
- .then(str => utils.parseXML(str))
1271
- .then(dom => dom.querySelector('UploadId').innerHTML)
1272
- .then(uploadId => {
1273
- up.uploadId = uploadId;
1274
-
1275
- // Calculate optimal block size
1276
- const fileSize = up.file.size;
1277
-
1278
- // Target ~10k parts, but minimum 5MB per AWS requirements
1279
- let blockSize = Math.ceil(fileSize / 10000);
1280
- if (blockSize < 5242880) blockSize = 5242880;
1281
-
1282
- // Set up upload parameters
1283
- up.method = 'aws';
1284
- up.bsize = blockSize;
1285
- up.blocks = Math.ceil(fileSize / blockSize);
1286
- up.b = {};
1287
- up.status = 'uploading';
1288
-
1289
- // Continue upload process
1290
- upload.run();
1291
- })
1292
- .catch(error => handleFailure(up, error));
1293
- }
1294
-
1295
- /**
1296
- * Set up direct PUT upload
1297
- * @param {Object} up - Upload object
1298
- * @param {Object} data - Server response data
1299
- */
1300
- function handlePutUpload(up, data) {
1301
- // Store upload info
1302
- up.info = data;
1303
-
1304
- // Calculate block size (if multipart PUT is supported)
1305
- const fileSize = up.file.size;
1306
- let blockSize = fileSize; // Default: single block
1307
-
1308
- if (data.Blocksize) {
1309
- // Server supports multipart upload
1310
- blockSize = data.Blocksize;
1311
- }
1312
-
1313
- // Set up upload parameters
1314
- up.method = 'put';
1315
- up.bsize = blockSize;
1316
- up.blocks = Math.ceil(fileSize / blockSize);
1317
- up.b = {};
1318
- up.status = 'uploading';
1319
-
1320
- // Continue upload process
1321
- upload.run();
1322
- }
1323
-
1324
- /**
1325
- * Upload a single part of a file
1326
- * Handles both AWS multipart and direct PUT methods
1327
- * @param {Object} up - Upload object
1328
- * @param {number} partNumber - Part number (0-based)
1329
- */
1330
- function uploadPart(up, partNumber) {
1331
- // Mark part as pending
1332
- up.b[partNumber] = "pending";
1333
-
1334
- // Calculate byte range for this part
1335
- const startByte = partNumber * up.bsize;
1336
- const endByte = Math.min(startByte + up.bsize, up.file.size);
1337
-
1338
- // Read file slice as ArrayBuffer
1339
- utils.readAsArrayBuffer(up.file, {
1340
- start: startByte,
1341
- end: endByte
1342
- }, (arrayBuffer, error) => {
1343
- if (error) {
1344
- handleFailure(up, error);
1345
- return;
1346
- }
1347
-
1348
- // Choose upload method based on protocol
1349
- if (up.method === 'aws') {
1350
- uploadAwsPart(up, partNumber, arrayBuffer);
1351
- } else if (up.method === 'put') {
1352
- uploadPutPart(up, partNumber, startByte, arrayBuffer);
1353
- } else {
1354
- handleFailure(up, new Error(`Unknown upload method: ${up.method}`));
1355
- }
1356
- });
1357
- }
1358
-
1359
- /**
1360
- * Upload a part using AWS multipart upload
1361
- * @param {Object} up - Upload object
1362
- * @param {number} partNumber - Part number (0-based)
1363
- * @param {ArrayBuffer} data - Part data
1364
- */
1365
- function uploadAwsPart(up, partNumber, data) {
1366
- // AWS part numbers are 1-based
1367
- const awsPartNumber = partNumber + 1;
1368
-
1369
- awsReq(
1370
- up.info,
1371
- "PUT",
1372
- `partNumber=${awsPartNumber}&uploadId=${up.uploadId}`,
1373
- data,
1374
- null,
1375
- up.context
1376
- )
1377
- .then(response => {
1378
- // Verify the response is successful
1379
- if (!response.ok) {
1380
- throw new Error(`HTTP ${response.status}: ${response.statusText}`);
1381
- }
1382
- // Store ETag for this part (needed for completion)
1383
- const etag = response.headers.get("ETag");
1384
- // Read response body to ensure request completed
1385
- return response.text().then(() => etag);
1386
- })
1387
- .then(etag => {
1388
- up.b[partNumber] = etag;
1389
-
1390
- // Update progress and continue processing
1391
- sendProgress();
1392
- upload.run();
1393
- })
1394
- .catch(error => handleFailure(up, error));
1395
- }
1396
-
1397
- /**
1398
- * Upload a part using direct PUT
1399
- * @param {Object} up - Upload object
1400
- * @param {number} partNumber - Part number (0-based)
1401
- * @param {number} startByte - Starting byte position
1402
- * @param {ArrayBuffer} data - Part data
1403
- */
1404
- function uploadPutPart(up, partNumber, startByte, data) {
1405
- // Set up headers
1406
- const headers = {
1407
- "Content-Type": up.file.type || "application/octet-stream"
1408
- };
1409
-
1410
- // Add Content-Range header for multipart PUT
1411
- if (up.blocks > 1) {
1412
- const endByte = startByte + data.byteLength - 1; // inclusive
1413
- headers["Content-Range"] = `bytes ${startByte}-${endByte}/*`;
1414
- }
1415
-
1416
- // Perform the PUT request
1417
- utils.fetch(up.info.PUT, {
1418
- method: "PUT",
1419
- body: data,
1420
- headers: headers,
1421
- })
1422
- .then(response => {
1423
- // Verify the response is successful
1424
- if (!response.ok) {
1425
- throw new Error(`HTTP ${response.status}: ${response.statusText}`);
1426
- }
1427
- // Read response body to ensure request completed
1428
- return response.text();
1429
- })
1430
- .then(() => {
1431
- // Mark part as done
1432
- up.b[partNumber] = "done";
1433
-
1434
- // Update progress and continue processing
1435
- sendProgress();
1436
- upload.run();
1437
- })
1438
- .catch(error => handleFailure(up, error));
1439
- }
1440
-
1441
- /**
1442
- * Process an upload in progress
1443
- * Manages uploading parts and completing the upload
1444
- * @param {Object} up - Upload object
1445
- */
1446
- function processActiveUpload(up) {
1447
- // Skip if paused or canceled
1448
- if (up.paused || up.canceled) return;
1449
-
1450
- // Track upload progress
1451
- let pendingParts = 0;
1452
- let completedParts = 0;
1453
-
1454
- // Process each part
1455
- for (let i = 0; i < up.blocks; i++) {
1456
- if (up.b[i] === undefined) {
1457
- // Part not started yet
1458
- if (up.paused) break; // Don't start new parts when paused
1459
-
1460
- // Start uploading this part
1461
- uploadPart(up, i);
1462
- pendingParts++;
1463
- } else if (up.b[i] !== "pending") {
1464
- // Part completed
1465
- completedParts++;
1466
- continue;
1467
- } else {
1468
- // Part in progress
1469
- pendingParts++;
1470
- }
1471
-
1472
- // Limit concurrent uploads
1473
- if (pendingParts >= 3) break;
1474
- }
1475
-
1476
- // Update upload progress
1477
- up.done = completedParts;
1478
-
1479
- // Check if all parts are complete
1480
- if (pendingParts === 0 && completedParts === up.blocks) {
1481
- // All parts complete, finalize the upload
1482
- up.status = "validating";
1483
-
1484
- if (up.method === 'aws') {
1485
- completeAwsUpload(up);
1486
- } else if (up.method === 'put') {
1487
- completePutUpload(up);
1488
- }
1489
- }
1490
- }
1491
-
1492
- /**
1493
- * Complete AWS multipart upload
1494
- * @param {Object} up - Upload object
1495
- */
1496
- function completeAwsUpload(up) {
1497
- // Create completion XML
1498
- // See: https://docs.aws.amazon.com/AmazonS3/latest/API/mpUploadComplete.html
1499
- let xml = "<CompleteMultipartUpload>";
1500
-
1501
- for (let i = 0; i < up.blocks; i++) {
1502
- // AWS part numbers are 1-based
1503
- xml += `<Part><PartNumber>${i + 1}</PartNumber><ETag>${up.b[i]}</ETag></Part>`;
1504
- }
1505
-
1506
- xml += "</CompleteMultipartUpload>";
1507
-
1508
- // Send completion request
1509
- awsReq(up.info, "POST", `uploadId=${up.uploadId}`, xml, null, up.context)
1510
- .then(response => response.text())
1511
- .then(() => {
1512
- // Call server-side completion handler
1513
- return rest.rest(
1514
- `Cloud/Aws/Bucket/Upload/${up.info.Cloud_Aws_Bucket_Upload__}:handleComplete`,
1515
- "POST",
1516
- {},
1517
- up.context
1518
- );
1519
- })
1520
- .then(response => {
1521
- // Mark upload as complete
1522
- up.status = "complete";
1523
- up.final = response.data;
1524
-
1525
- // Notify listeners
1526
- sendProgress();
1527
-
1528
- // Remove from running uploads
1529
- delete state.running[up.up_id];
1530
-
1531
- // Resolve the upload promise
1532
- up.resolve(up);
1533
-
1534
- // Continue processing queue
1535
- upload.run();
1536
- })
1537
- .catch(error => handleFailure(up, error));
1538
- }
1539
-
1540
- /**
1541
- * Complete direct PUT upload
1542
- * @param {Object} up - Upload object
1543
- */
1544
- function completePutUpload(up) {
1545
- // Call completion endpoint
1546
- rest.rest(up.info.Complete, "POST", {}, up.context)
1547
- .then(response => {
1548
- // Mark upload as complete
1549
- up.status = "complete";
1550
- up.final = response.data;
1551
-
1552
- // Notify listeners
1553
- sendProgress();
1554
-
1555
- // Remove from running uploads
1556
- delete state.running[up.up_id];
1557
-
1558
- // Resolve the upload promise
1559
- up.resolve(up);
1560
-
1561
- // Continue processing queue
1562
- upload.run();
1563
- })
1564
- .catch(error => handleFailure(up, error));
1565
- }
1566
-
1567
- /**
1568
- * Fill the upload queue with new upload tasks
1569
- * Takes items from the queue and adds them to running uploads
1570
- */
1571
- function fillUploadQueue() {
1572
- // Skip if we're already running the maximum number of uploads
1573
- if (Object.keys(state.running).length >= 3) return;
1574
-
1575
- // Maximum of 3 concurrent uploads
1576
- while (Object.keys(state.running).length < 3 && state.queue.length > 0) {
1577
- // Get next upload from queue
1578
- const upload = state.queue.shift();
1579
-
1580
- // Add to running uploads
1581
- state.running[upload.up_id] = upload;
1582
- }
1583
-
1584
- // Notify progress
1585
- sendProgress();
1586
- }
1587
-
1588
- /**
1589
- * Get current upload status
1590
- * @returns {Object} Status object with queued, running and failed uploads
1591
- */
1592
- upload.getStatus = function() {
1593
- return {
1594
- queue: state.queue,
1595
- running: Object.keys(state.running).map(id => state.running[id]),
1596
- failed: state.failed
1597
- };
1598
- };
1599
-
1600
- /**
1601
- * Resume all failed uploads
1602
- * Moves failed uploads back to the queue
1603
- */
1604
- upload.resume = function() {
1605
- // Move all failed uploads back to the queue
1606
- while (state.failed.length > 0) {
1607
- state.queue.push(state.failed.shift());
1608
- }
1609
-
1610
- // Restart upload process
1611
- upload.run();
1612
- };
1613
-
1614
- /**
1615
- * Initialize uploads in different environments
1616
- *
1617
- * @param {string} path - API path to upload to
1618
- * @param {Object} params - Upload parameters
1619
- * @param {Function} notify - Notification callback
1620
- * @returns {Function} - Function to start uploads
1621
- */
1622
- upload.init = function(path, params, notify) {
1623
- params = params || {};
1624
-
1625
- if (env.isBrowser) {
1626
- // Browser implementation
1627
- if (state.lastInput !== null) {
1628
- state.lastInput.parentNode.removeChild(state.lastInput);
1629
- state.lastInput = null;
1630
- }
1631
-
1632
- const input = document.createElement("input");
1633
- input.type = "file";
1634
- input.style.display = "none";
1635
- if (!params.single) {
1636
- input.multiple = "multiple";
1637
- }
1638
-
1639
- document.getElementsByTagName('body')[0].appendChild(input);
1640
- state.lastInput = input;
1641
-
1642
- const promise = new Promise(function(resolve, reject) {
1643
- input.onchange = function() {
1644
- if (this.files.length === 0) {
1645
- return resolve();
1646
- }
1647
-
1648
- let count = this.files.length;
1649
- if (notify) notify({status: 'init', count: count});
1650
-
1651
- for (let i = 0; i < this.files.length; i++) {
1652
- upload.append(path, this.files[i], params, fwWrapper.getContext())
1653
- .then(function(obj) {
1654
- count -= 1;
1655
- if (notify) notify(obj);
1656
- if (count === 0) resolve();
1657
- });
1658
- }
1659
- upload.run();
1660
- };
1661
- });
1662
-
1663
- input.click();
1664
- return promise;
1665
- } else {
1666
- // Non-browser environment
1667
- return function(files) {
1668
- // Allow array, single file object, or file content buffer
1669
- if (!Array.isArray(files)) {
1670
- if (files instanceof ArrayBuffer ||
1671
- (files.buffer instanceof ArrayBuffer) ||
1672
- (typeof Buffer !== 'undefined' && files instanceof Buffer)) {
1673
- // If it's a buffer/ArrayBuffer, create a file-like object
1674
- files = [{
1675
- name: params.filename || 'file.bin',
1676
- size: files.byteLength || files.length,
1677
- type: params.type || 'application/octet-stream',
1678
- lastModified: Date.now(),
1679
- content: files
1680
- }];
1681
- } else {
1682
- // Single file object
1683
- files = [files];
1684
- }
1685
- }
1686
-
1687
- return new Promise(function(resolve, reject) {
1688
- const count = files.length;
1689
- if (count === 0) {
1690
- return resolve();
1691
- }
1692
-
1693
- if (notify) notify({status: 'init', count: count});
1694
-
1695
- let remainingCount = count;
1696
-
1697
- files.forEach(file => {
1698
- try {
1699
- // Ensure file has required properties
1700
- if (!file.name) file.name = 'file.bin';
1701
- if (!file.type) file.type = 'application/octet-stream';
1702
- if (!file.lastModified) file.lastModified = Date.now();
1703
-
1704
- // Add slice method if not present
1705
- if (!file.slice && file.content) {
1706
- file.slice = function(start, end) {
1707
- return {
1708
- content: this.content.slice(start, end || this.size)
1709
- };
1710
- };
1711
- }
1712
-
1713
- upload.append(path, file, params, fwWrapper.getContext())
1714
- .then(function(obj) {
1715
- remainingCount -= 1;
1716
- if (notify) notify(obj);
1717
- if (remainingCount === 0) resolve();
1718
- })
1719
- .catch(function(err) {
1720
- remainingCount -= 1;
1721
- console.error('Error uploading file:', err);
1722
- if (remainingCount === 0) resolve();
1723
- });
1724
- } catch (err) {
1725
- remainingCount -= 1;
1726
- console.error('Error processing file:', err);
1727
- if (remainingCount === 0) resolve();
1728
- }
1729
- });
1730
-
1731
- upload.run();
1732
- });
1733
- };
1734
- }
1735
- };
1736
-
1737
- /**
1738
- * Add a file to the upload queue
1739
- * @param {string} path - API path to upload to
1740
- * @param {File|Object} file - File to upload
1741
- * @param {Object} params - Upload parameters
1742
- * @param {Object} context - Request context
1743
- * @returns {Promise} - Upload promise
1744
- */
1745
- upload.append = function(path, file, params, context) {
1746
- return new Promise((resolve, reject) => {
1747
- // Process parameters
1748
- params = params || {};
1749
- context = context || fwWrapper.getContext();
1750
-
1751
- // Create an upload object
1752
- const uploadObject = {
1753
- path: path,
1754
- file: file,
1755
- resolve: resolve,
1756
- reject: reject,
1757
- status: "pending",
1758
- paused: false,
1759
- up_id: state.nextId++,
1760
- params: params,
1761
- context: { ...context } // Create a copy to avoid modification
1762
- };
1763
-
1764
- // Add to queue
1765
- state.queue.push(uploadObject);
1766
- });
1767
- };
1768
-
1769
- /**
1770
- * Cancel an upload in progress or in queue
1771
- * @param {number} uploadId - Upload ID to cancel
1772
- */
1773
- upload.cancelItem = function(uploadId) {
1774
- // Check running uploads
1775
- if (state.running[uploadId]) {
1776
- // Mark running upload as canceled
1777
- state.running[uploadId].canceled = true;
1778
- } else {
1779
- // Check queued uploads
1780
- for (let i = 0; i < state.queue.length; i++) {
1781
- if (state.queue[i].up_id === uploadId) {
1782
- state.queue[i].canceled = true;
1783
- break;
1784
- }
1785
- }
1786
- }
1787
-
1788
- // Update progress
1789
- sendProgress();
1790
- };
1791
-
1792
- /**
1793
- * Delete an upload from queue or failed list
1794
- * Only canceled uploads can be removed from running list
1795
- * @param {number} uploadId - Upload ID to delete
1796
- */
1797
- upload.deleteItem = function(uploadId) {
1798
- // Check running uploads
1799
- if (state.running[uploadId]) {
1800
- // Only delete if canceled
1801
- if (state.running[uploadId].canceled) {
1802
- delete state.running[uploadId];
1803
- }
1804
- } else {
1805
- // Check queue
1806
- for (let i = 0; i < state.queue.length; i++) {
1807
- if (state.queue[i].up_id === uploadId) {
1808
- state.queue.splice(i, 1);
1809
- break;
1810
- }
1811
- }
1812
-
1813
- // Check failed uploads
1814
- for (let i = 0; i < state.failed.length; i++) {
1815
- if (state.failed[i].up_id === uploadId) {
1816
- state.failed.splice(i, 1);
1817
- break;
1818
- }
1819
- }
1820
- }
1821
-
1822
- // Update progress
1823
- sendProgress();
1824
- };
1825
-
1826
- /**
1827
- * Pause an active upload
1828
- * @param {number} uploadId - Upload ID to pause
1829
- */
1830
- upload.pauseItem = function(uploadId) {
1831
- // Find upload in running list
1832
- const upload = state.running[uploadId];
1833
-
1834
- // Only pause if active
1835
- if (upload && upload.status === "uploading") {
1836
- upload.paused = true;
1837
- }
1838
-
1839
- // Update progress
1840
- sendProgress();
1841
- };
1842
-
1843
- /**
1844
- * Resume a paused upload
1845
- * @param {number} uploadId - Upload ID to resume
1846
- */
1847
- upload.resumeItem = function(uploadId) {
1848
- // Find upload in running list
1849
- const upload = state.running[uploadId];
1850
-
1851
- // Only resume if paused
1852
- if (upload && upload.paused) {
1853
- upload.paused = false;
1854
- processActiveUpload(upload);
1855
- }
1856
-
1857
- // Update progress
1858
- sendProgress();
1859
- };
1860
-
1861
- /**
1862
- * Retry a failed upload
1863
- * @param {number} uploadId - Upload ID to retry
1864
- */
1865
- upload.retryItem = function(uploadId) {
1866
- // Find upload in failed list
1867
- let failedUpload = null;
1868
- let failedIndex = -1;
1869
-
1870
- for (let i = 0; i < state.failed.length; i++) {
1871
- if (state.failed[i].up_id === uploadId) {
1872
- failedUpload = state.failed[i];
1873
- failedIndex = i;
1874
- break;
1875
- }
1876
- }
1877
-
1878
- // Skip if not found
1879
- if (!failedUpload) return;
1880
-
1881
- // Check if already in queue
1882
- for (let i = 0; i < state.queue.length; i++) {
1883
- if (state.queue[i].up_id === uploadId) {
1884
- return; // Already in queue
1885
- }
1886
- }
1887
-
1888
- // Reset failure data
1889
- failedUpload.failure = {};
1890
-
1891
- // Reset pending parts
1892
- for (let i = 0; i < failedUpload.blocks; i++) {
1893
- if (failedUpload.b[i] === "pending") {
1894
- failedUpload.b[i] = undefined;
1895
- }
1896
- }
1897
-
1898
- // Move from failed to queue
1899
- state.failed.splice(failedIndex, 1);
1900
- state.queue.push(failedUpload);
1901
-
1902
- // Restart upload
1903
- upload.run();
1904
-
1905
- // Dispatch retry event
1906
- utils.dispatchEvent("upload:retry", { item: failedUpload });
1907
-
1908
- // Update progress
1909
- sendProgress();
1910
- };
1911
-
1912
- /**
1913
- * Start or continue the upload process
1914
- * Processes queued uploads and continues running uploads
1915
- */
1916
- upload.run = function() {
1917
- // Fill queue with new uploads
1918
- fillUploadQueue();
1919
-
1920
- // Process running uploads
1921
- for (const uploadId in state.running) {
1922
- const upload = state.running[uploadId];
1923
-
1924
- // Process based on status
1925
- switch (upload.status) {
1926
- case "pending":
1927
- processUpload(upload);
1928
- break;
1929
- case "uploading":
1930
- processActiveUpload(upload);
1931
- break;
1932
- }
1933
- }
1934
- };
1935
-
1936
- return upload;
1937
- }());
1938
-
1939
- // Export simple upload function for Node.js consumers
1940
- module.exports.uploadFile = uploadFile;
641
+ // Export
642
+ module.exports.uploadFile = uploadFile;