@karpeleslab/klbfw 0.2.19 → 0.2.21
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/index.d.ts +72 -3
- package/index.js +6 -1
- package/package.json +1 -1
- package/upload-internal.js +474 -0
- package/upload-legacy.js +821 -0
- package/upload-many.js +204 -0
- package/upload.js +19 -1331
package/upload-legacy.js
ADDED
|
@@ -0,0 +1,821 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* KLB Upload Legacy Module
|
|
3
|
+
*
|
|
4
|
+
* This module provides the deprecated upload object for backwards compatibility.
|
|
5
|
+
* New code should use uploadFile() or uploadManyFiles() instead.
|
|
6
|
+
*
|
|
7
|
+
* @module upload-legacy
|
|
8
|
+
* @deprecated Use uploadFile() or uploadManyFiles() instead
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
'use strict';
|
|
12
|
+
|
|
13
|
+
const rest = require('./rest');
|
|
14
|
+
const fwWrapper = require('./fw-wrapper');
|
|
15
|
+
const { env, utils, awsReq } = require('./upload-internal');
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Upload module (IIFE pattern)
|
|
19
|
+
* @deprecated Use uploadFile() or uploadManyFiles() instead
|
|
20
|
+
* @returns {Object} Upload interface
|
|
21
|
+
*/
|
|
22
|
+
module.exports.upload = (function () {
|
|
23
|
+
/**
|
|
24
|
+
* Upload state
|
|
25
|
+
*/
|
|
26
|
+
const state = {
|
|
27
|
+
queue: [], // Queued uploads
|
|
28
|
+
failed: [], // Failed uploads
|
|
29
|
+
running: {}, // Currently processing uploads
|
|
30
|
+
nextId: 0, // Next upload ID
|
|
31
|
+
lastInput: null // Last created file input element (browser only)
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
// Public API object
|
|
35
|
+
const upload = {};
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Helper Functions
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Notify progress to listeners
|
|
43
|
+
* Calls onprogress callback and dispatches events
|
|
44
|
+
*/
|
|
45
|
+
function sendProgress() {
|
|
46
|
+
const status = upload.getStatus();
|
|
47
|
+
|
|
48
|
+
// Call the onprogress callback if defined
|
|
49
|
+
if (typeof upload.onprogress === "function") {
|
|
50
|
+
upload.onprogress(status);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Dispatch event for listeners
|
|
54
|
+
utils.dispatchEvent("upload:progress", status);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Handle upload failure
|
|
59
|
+
* @param {Object} up - Upload object
|
|
60
|
+
* @param {*} error - Error data
|
|
61
|
+
*/
|
|
62
|
+
function handleFailure(up, error) {
|
|
63
|
+
// Skip if upload is no longer running
|
|
64
|
+
if (!(up.up_id in state.running)) return;
|
|
65
|
+
|
|
66
|
+
// Check if already in failed list
|
|
67
|
+
for (const failedItem of state.failed) {
|
|
68
|
+
if (failedItem.up_id === up.up_id) {
|
|
69
|
+
return; // Already recorded as failed
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Record failure
|
|
74
|
+
up.failure = error;
|
|
75
|
+
state.failed.push(up);
|
|
76
|
+
delete state.running[up.up_id];
|
|
77
|
+
|
|
78
|
+
// Reject the promise so callers know the upload failed
|
|
79
|
+
if (up.reject) {
|
|
80
|
+
up.reject(error);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Continue processing queue
|
|
84
|
+
upload.run();
|
|
85
|
+
|
|
86
|
+
// Notify progress
|
|
87
|
+
sendProgress();
|
|
88
|
+
|
|
89
|
+
// Dispatch failure event
|
|
90
|
+
utils.dispatchEvent("upload:failed", {
|
|
91
|
+
item: up,
|
|
92
|
+
res: error
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Process a pending upload
|
|
98
|
+
* Initiates the upload process with the server
|
|
99
|
+
* @param {Object} up - Upload object
|
|
100
|
+
*/
|
|
101
|
+
function processUpload(up) {
|
|
102
|
+
// Mark as processing
|
|
103
|
+
up.status = "pending-wip";
|
|
104
|
+
|
|
105
|
+
// Prepare parameters
|
|
106
|
+
const params = up.params || {};
|
|
107
|
+
|
|
108
|
+
// Set file metadata
|
|
109
|
+
params.filename = up.file.name;
|
|
110
|
+
params.size = up.file.size;
|
|
111
|
+
params.lastModified = up.file.lastModified / 1000;
|
|
112
|
+
params.type = up.file.type || "application/octet-stream";
|
|
113
|
+
|
|
114
|
+
// Initialize upload with the server
|
|
115
|
+
rest.rest(up.path, "POST", params, up.context)
|
|
116
|
+
.then(function(response) {
|
|
117
|
+
// Method 1: AWS signed multipart upload
|
|
118
|
+
if (response.data.Cloud_Aws_Bucket_Upload__) {
|
|
119
|
+
return handleAwsMultipartUpload(up, response.data);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Method 2: Direct PUT upload
|
|
123
|
+
if (response.data.PUT) {
|
|
124
|
+
return handlePutUpload(up, response.data);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Invalid response format
|
|
128
|
+
delete state.running[up.up_id];
|
|
129
|
+
state.failed.push(up);
|
|
130
|
+
up.reject(new Error('Invalid upload response format'));
|
|
131
|
+
})
|
|
132
|
+
.catch(error => handleFailure(up, error));
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Set up AWS multipart upload
|
|
137
|
+
* @param {Object} up - Upload object
|
|
138
|
+
* @param {Object} data - Server response data
|
|
139
|
+
*/
|
|
140
|
+
function handleAwsMultipartUpload(up, data) {
|
|
141
|
+
// Store upload info
|
|
142
|
+
up.info = data;
|
|
143
|
+
|
|
144
|
+
// Initialize multipart upload
|
|
145
|
+
return awsReq(
|
|
146
|
+
up.info,
|
|
147
|
+
"POST",
|
|
148
|
+
"uploads=",
|
|
149
|
+
"",
|
|
150
|
+
{"Content-Type": up.file.type || "application/octet-stream", "X-Amz-Acl": "private"},
|
|
151
|
+
up.context
|
|
152
|
+
)
|
|
153
|
+
.then(response => response.text())
|
|
154
|
+
.then(str => utils.parseXML(str))
|
|
155
|
+
.then(dom => dom.querySelector('UploadId').innerHTML)
|
|
156
|
+
.then(uploadId => {
|
|
157
|
+
up.uploadId = uploadId;
|
|
158
|
+
|
|
159
|
+
// Calculate optimal block size
|
|
160
|
+
const fileSize = up.file.size;
|
|
161
|
+
|
|
162
|
+
// Target ~10k parts, but minimum 5MB per AWS requirements
|
|
163
|
+
let blockSize = Math.ceil(fileSize / 10000);
|
|
164
|
+
if (blockSize < 5242880) blockSize = 5242880;
|
|
165
|
+
|
|
166
|
+
// Set up upload parameters
|
|
167
|
+
up.method = 'aws';
|
|
168
|
+
up.bsize = blockSize;
|
|
169
|
+
up.blocks = Math.ceil(fileSize / blockSize);
|
|
170
|
+
up.b = {};
|
|
171
|
+
up.status = 'uploading';
|
|
172
|
+
|
|
173
|
+
// Continue upload process
|
|
174
|
+
upload.run();
|
|
175
|
+
})
|
|
176
|
+
.catch(error => handleFailure(up, error));
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Set up direct PUT upload
|
|
181
|
+
* @param {Object} up - Upload object
|
|
182
|
+
* @param {Object} data - Server response data
|
|
183
|
+
*/
|
|
184
|
+
function handlePutUpload(up, data) {
|
|
185
|
+
// Store upload info
|
|
186
|
+
up.info = data;
|
|
187
|
+
|
|
188
|
+
// Calculate block size (if multipart PUT is supported)
|
|
189
|
+
const fileSize = up.file.size;
|
|
190
|
+
let blockSize = fileSize; // Default: single block
|
|
191
|
+
|
|
192
|
+
if (data.Blocksize) {
|
|
193
|
+
// Server supports multipart upload
|
|
194
|
+
blockSize = data.Blocksize;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Set up upload parameters
|
|
198
|
+
up.method = 'put';
|
|
199
|
+
up.bsize = blockSize;
|
|
200
|
+
up.blocks = Math.ceil(fileSize / blockSize);
|
|
201
|
+
up.b = {};
|
|
202
|
+
up.status = 'uploading';
|
|
203
|
+
|
|
204
|
+
// Continue upload process
|
|
205
|
+
upload.run();
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Upload a single part of a file
|
|
210
|
+
* Handles both AWS multipart and direct PUT methods
|
|
211
|
+
* @param {Object} up - Upload object
|
|
212
|
+
* @param {number} partNumber - Part number (0-based)
|
|
213
|
+
*/
|
|
214
|
+
function uploadPart(up, partNumber) {
|
|
215
|
+
// Mark part as pending
|
|
216
|
+
up.b[partNumber] = "pending";
|
|
217
|
+
|
|
218
|
+
// Calculate byte range for this part
|
|
219
|
+
const startByte = partNumber * up.bsize;
|
|
220
|
+
const endByte = Math.min(startByte + up.bsize, up.file.size);
|
|
221
|
+
|
|
222
|
+
// Read file slice as ArrayBuffer
|
|
223
|
+
utils.readAsArrayBuffer(up.file, {
|
|
224
|
+
start: startByte,
|
|
225
|
+
end: endByte
|
|
226
|
+
}, (arrayBuffer, error) => {
|
|
227
|
+
if (error) {
|
|
228
|
+
handleFailure(up, error);
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Choose upload method based on protocol
|
|
233
|
+
if (up.method === 'aws') {
|
|
234
|
+
uploadAwsPart(up, partNumber, arrayBuffer);
|
|
235
|
+
} else if (up.method === 'put') {
|
|
236
|
+
uploadPutPart(up, partNumber, startByte, arrayBuffer);
|
|
237
|
+
} else {
|
|
238
|
+
handleFailure(up, new Error(`Unknown upload method: ${up.method}`));
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Upload a part using AWS multipart upload
|
|
245
|
+
* @param {Object} up - Upload object
|
|
246
|
+
* @param {number} partNumber - Part number (0-based)
|
|
247
|
+
* @param {ArrayBuffer} data - Part data
|
|
248
|
+
*/
|
|
249
|
+
function uploadAwsPart(up, partNumber, data) {
|
|
250
|
+
// AWS part numbers are 1-based
|
|
251
|
+
const awsPartNumber = partNumber + 1;
|
|
252
|
+
|
|
253
|
+
awsReq(
|
|
254
|
+
up.info,
|
|
255
|
+
"PUT",
|
|
256
|
+
`partNumber=${awsPartNumber}&uploadId=${up.uploadId}`,
|
|
257
|
+
data,
|
|
258
|
+
null,
|
|
259
|
+
up.context
|
|
260
|
+
)
|
|
261
|
+
.then(response => {
|
|
262
|
+
// Verify the response is successful
|
|
263
|
+
if (!response.ok) {
|
|
264
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
265
|
+
}
|
|
266
|
+
// Store ETag for this part (needed for completion)
|
|
267
|
+
const etag = response.headers.get("ETag");
|
|
268
|
+
// Read response body to ensure request completed
|
|
269
|
+
return response.text().then(() => etag);
|
|
270
|
+
})
|
|
271
|
+
.then(etag => {
|
|
272
|
+
up.b[partNumber] = etag;
|
|
273
|
+
|
|
274
|
+
// Update progress and continue processing
|
|
275
|
+
sendProgress();
|
|
276
|
+
upload.run();
|
|
277
|
+
})
|
|
278
|
+
.catch(error => handleFailure(up, error));
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Upload a part using direct PUT
|
|
283
|
+
* @param {Object} up - Upload object
|
|
284
|
+
* @param {number} partNumber - Part number (0-based)
|
|
285
|
+
* @param {number} startByte - Starting byte position
|
|
286
|
+
* @param {ArrayBuffer} data - Part data
|
|
287
|
+
*/
|
|
288
|
+
function uploadPutPart(up, partNumber, startByte, data) {
|
|
289
|
+
// Set up headers
|
|
290
|
+
const headers = {
|
|
291
|
+
"Content-Type": up.file.type || "application/octet-stream"
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
// Add Content-Range header for multipart PUT
|
|
295
|
+
if (up.blocks > 1) {
|
|
296
|
+
const endByte = startByte + data.byteLength - 1; // inclusive
|
|
297
|
+
headers["Content-Range"] = `bytes ${startByte}-${endByte}/*`;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Perform the PUT request
|
|
301
|
+
utils.fetch(up.info.PUT, {
|
|
302
|
+
method: "PUT",
|
|
303
|
+
body: data,
|
|
304
|
+
headers: headers,
|
|
305
|
+
})
|
|
306
|
+
.then(response => {
|
|
307
|
+
// Verify the response is successful
|
|
308
|
+
if (!response.ok) {
|
|
309
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
310
|
+
}
|
|
311
|
+
// Read response body to ensure request completed
|
|
312
|
+
return response.text();
|
|
313
|
+
})
|
|
314
|
+
.then(() => {
|
|
315
|
+
// Mark part as done
|
|
316
|
+
up.b[partNumber] = "done";
|
|
317
|
+
|
|
318
|
+
// Update progress and continue processing
|
|
319
|
+
sendProgress();
|
|
320
|
+
upload.run();
|
|
321
|
+
})
|
|
322
|
+
.catch(error => handleFailure(up, error));
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Process an upload in progress
|
|
327
|
+
* Manages uploading parts and completing the upload
|
|
328
|
+
* @param {Object} up - Upload object
|
|
329
|
+
*/
|
|
330
|
+
function processActiveUpload(up) {
|
|
331
|
+
// Skip if paused or canceled
|
|
332
|
+
if (up.paused || up.canceled) return;
|
|
333
|
+
|
|
334
|
+
// Track upload progress
|
|
335
|
+
let pendingParts = 0;
|
|
336
|
+
let completedParts = 0;
|
|
337
|
+
|
|
338
|
+
// Process each part
|
|
339
|
+
for (let i = 0; i < up.blocks; i++) {
|
|
340
|
+
if (up.b[i] === undefined) {
|
|
341
|
+
// Part not started yet
|
|
342
|
+
if (up.paused) break; // Don't start new parts when paused
|
|
343
|
+
|
|
344
|
+
// Start uploading this part
|
|
345
|
+
uploadPart(up, i);
|
|
346
|
+
pendingParts++;
|
|
347
|
+
} else if (up.b[i] !== "pending") {
|
|
348
|
+
// Part completed
|
|
349
|
+
completedParts++;
|
|
350
|
+
continue;
|
|
351
|
+
} else {
|
|
352
|
+
// Part in progress
|
|
353
|
+
pendingParts++;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// Limit concurrent uploads
|
|
357
|
+
if (pendingParts >= 3) break;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// Update upload progress
|
|
361
|
+
up.done = completedParts;
|
|
362
|
+
|
|
363
|
+
// Check if all parts are complete
|
|
364
|
+
if (pendingParts === 0 && completedParts === up.blocks) {
|
|
365
|
+
// All parts complete, finalize the upload
|
|
366
|
+
up.status = "validating";
|
|
367
|
+
|
|
368
|
+
if (up.method === 'aws') {
|
|
369
|
+
completeAwsUpload(up);
|
|
370
|
+
} else if (up.method === 'put') {
|
|
371
|
+
completePutUpload(up);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Complete AWS multipart upload
|
|
378
|
+
* @param {Object} up - Upload object
|
|
379
|
+
*/
|
|
380
|
+
function completeAwsUpload(up) {
|
|
381
|
+
// Create completion XML
|
|
382
|
+
// See: https://docs.aws.amazon.com/AmazonS3/latest/API/mpUploadComplete.html
|
|
383
|
+
let xml = "<CompleteMultipartUpload>";
|
|
384
|
+
|
|
385
|
+
for (let i = 0; i < up.blocks; i++) {
|
|
386
|
+
// AWS part numbers are 1-based
|
|
387
|
+
xml += `<Part><PartNumber>${i + 1}</PartNumber><ETag>${up.b[i]}</ETag></Part>`;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
xml += "</CompleteMultipartUpload>";
|
|
391
|
+
|
|
392
|
+
// Send completion request
|
|
393
|
+
awsReq(up.info, "POST", `uploadId=${up.uploadId}`, xml, null, up.context)
|
|
394
|
+
.then(response => response.text())
|
|
395
|
+
.then(() => {
|
|
396
|
+
// Call server-side completion handler
|
|
397
|
+
return rest.rest(
|
|
398
|
+
`Cloud/Aws/Bucket/Upload/${up.info.Cloud_Aws_Bucket_Upload__}:handleComplete`,
|
|
399
|
+
"POST",
|
|
400
|
+
{},
|
|
401
|
+
up.context
|
|
402
|
+
);
|
|
403
|
+
})
|
|
404
|
+
.then(response => {
|
|
405
|
+
// Mark upload as complete
|
|
406
|
+
up.status = "complete";
|
|
407
|
+
up.final = response.data;
|
|
408
|
+
|
|
409
|
+
// Notify listeners
|
|
410
|
+
sendProgress();
|
|
411
|
+
|
|
412
|
+
// Remove from running uploads
|
|
413
|
+
delete state.running[up.up_id];
|
|
414
|
+
|
|
415
|
+
// Resolve the upload promise
|
|
416
|
+
up.resolve(up);
|
|
417
|
+
|
|
418
|
+
// Continue processing queue
|
|
419
|
+
upload.run();
|
|
420
|
+
})
|
|
421
|
+
.catch(error => handleFailure(up, error));
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* Complete direct PUT upload
|
|
426
|
+
* @param {Object} up - Upload object
|
|
427
|
+
*/
|
|
428
|
+
function completePutUpload(up) {
|
|
429
|
+
// Call completion endpoint
|
|
430
|
+
rest.rest(up.info.Complete, "POST", {}, up.context)
|
|
431
|
+
.then(response => {
|
|
432
|
+
// Mark upload as complete
|
|
433
|
+
up.status = "complete";
|
|
434
|
+
up.final = response.data;
|
|
435
|
+
|
|
436
|
+
// Notify listeners
|
|
437
|
+
sendProgress();
|
|
438
|
+
|
|
439
|
+
// Remove from running uploads
|
|
440
|
+
delete state.running[up.up_id];
|
|
441
|
+
|
|
442
|
+
// Resolve the upload promise
|
|
443
|
+
up.resolve(up);
|
|
444
|
+
|
|
445
|
+
// Continue processing queue
|
|
446
|
+
upload.run();
|
|
447
|
+
})
|
|
448
|
+
.catch(error => handleFailure(up, error));
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/**
|
|
452
|
+
* Fill the upload queue with new upload tasks
|
|
453
|
+
* Takes items from the queue and adds them to running uploads
|
|
454
|
+
*/
|
|
455
|
+
function fillUploadQueue() {
|
|
456
|
+
// Skip if we're already running the maximum number of uploads
|
|
457
|
+
if (Object.keys(state.running).length >= 3) return;
|
|
458
|
+
|
|
459
|
+
// Maximum of 3 concurrent uploads
|
|
460
|
+
while (Object.keys(state.running).length < 3 && state.queue.length > 0) {
|
|
461
|
+
// Get next upload from queue
|
|
462
|
+
const upload = state.queue.shift();
|
|
463
|
+
|
|
464
|
+
// Add to running uploads
|
|
465
|
+
state.running[upload.up_id] = upload;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// Notify progress
|
|
469
|
+
sendProgress();
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* Get current upload status
|
|
474
|
+
* @returns {Object} Status object with queued, running and failed uploads
|
|
475
|
+
*/
|
|
476
|
+
upload.getStatus = function() {
|
|
477
|
+
return {
|
|
478
|
+
queue: state.queue,
|
|
479
|
+
running: Object.keys(state.running).map(id => state.running[id]),
|
|
480
|
+
failed: state.failed
|
|
481
|
+
};
|
|
482
|
+
};
|
|
483
|
+
|
|
484
|
+
/**
|
|
485
|
+
* Resume all failed uploads
|
|
486
|
+
* Moves failed uploads back to the queue
|
|
487
|
+
*/
|
|
488
|
+
upload.resume = function() {
|
|
489
|
+
// Move all failed uploads back to the queue
|
|
490
|
+
while (state.failed.length > 0) {
|
|
491
|
+
state.queue.push(state.failed.shift());
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// Restart upload process
|
|
495
|
+
upload.run();
|
|
496
|
+
};
|
|
497
|
+
|
|
498
|
+
/**
|
|
499
|
+
* Initialize uploads in different environments
|
|
500
|
+
*
|
|
501
|
+
* @param {string} path - API path to upload to
|
|
502
|
+
* @param {Object} params - Upload parameters
|
|
503
|
+
* @param {Function} notify - Notification callback
|
|
504
|
+
* @returns {Function} - Function to start uploads
|
|
505
|
+
*/
|
|
506
|
+
upload.init = function(path, params, notify) {
|
|
507
|
+
params = params || {};
|
|
508
|
+
|
|
509
|
+
if (env.isBrowser) {
|
|
510
|
+
// Browser implementation
|
|
511
|
+
if (state.lastInput !== null) {
|
|
512
|
+
state.lastInput.parentNode.removeChild(state.lastInput);
|
|
513
|
+
state.lastInput = null;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
const input = document.createElement("input");
|
|
517
|
+
input.type = "file";
|
|
518
|
+
input.style.display = "none";
|
|
519
|
+
if (!params.single) {
|
|
520
|
+
input.multiple = "multiple";
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
document.getElementsByTagName('body')[0].appendChild(input);
|
|
524
|
+
state.lastInput = input;
|
|
525
|
+
|
|
526
|
+
const promise = new Promise(function(resolve, reject) {
|
|
527
|
+
input.onchange = function() {
|
|
528
|
+
if (this.files.length === 0) {
|
|
529
|
+
return resolve();
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
let count = this.files.length;
|
|
533
|
+
if (notify) notify({status: 'init', count: count});
|
|
534
|
+
|
|
535
|
+
for (let i = 0; i < this.files.length; i++) {
|
|
536
|
+
upload.append(path, this.files[i], params, fwWrapper.getContext())
|
|
537
|
+
.then(function(obj) {
|
|
538
|
+
count -= 1;
|
|
539
|
+
if (notify) notify(obj);
|
|
540
|
+
if (count === 0) resolve();
|
|
541
|
+
});
|
|
542
|
+
}
|
|
543
|
+
upload.run();
|
|
544
|
+
};
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
input.click();
|
|
548
|
+
return promise;
|
|
549
|
+
} else {
|
|
550
|
+
// Non-browser environment
|
|
551
|
+
return function(files) {
|
|
552
|
+
// Allow array, single file object, or file content buffer
|
|
553
|
+
if (!Array.isArray(files)) {
|
|
554
|
+
if (files instanceof ArrayBuffer ||
|
|
555
|
+
(files.buffer instanceof ArrayBuffer) ||
|
|
556
|
+
(typeof Buffer !== 'undefined' && files instanceof Buffer)) {
|
|
557
|
+
// If it's a buffer/ArrayBuffer, create a file-like object
|
|
558
|
+
files = [{
|
|
559
|
+
name: params.filename || 'file.bin',
|
|
560
|
+
size: files.byteLength || files.length,
|
|
561
|
+
type: params.type || 'application/octet-stream',
|
|
562
|
+
lastModified: Date.now(),
|
|
563
|
+
content: files
|
|
564
|
+
}];
|
|
565
|
+
} else {
|
|
566
|
+
// Single file object
|
|
567
|
+
files = [files];
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
return new Promise(function(resolve, reject) {
|
|
572
|
+
const count = files.length;
|
|
573
|
+
if (count === 0) {
|
|
574
|
+
return resolve();
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
if (notify) notify({status: 'init', count: count});
|
|
578
|
+
|
|
579
|
+
let remainingCount = count;
|
|
580
|
+
|
|
581
|
+
files.forEach(file => {
|
|
582
|
+
try {
|
|
583
|
+
// Ensure file has required properties
|
|
584
|
+
if (!file.name) file.name = 'file.bin';
|
|
585
|
+
if (!file.type) file.type = 'application/octet-stream';
|
|
586
|
+
if (!file.lastModified) file.lastModified = Date.now();
|
|
587
|
+
|
|
588
|
+
// Add slice method if not present
|
|
589
|
+
if (!file.slice && file.content) {
|
|
590
|
+
file.slice = function(start, end) {
|
|
591
|
+
return {
|
|
592
|
+
content: this.content.slice(start, end || this.size)
|
|
593
|
+
};
|
|
594
|
+
};
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
upload.append(path, file, params, fwWrapper.getContext())
|
|
598
|
+
.then(function(obj) {
|
|
599
|
+
remainingCount -= 1;
|
|
600
|
+
if (notify) notify(obj);
|
|
601
|
+
if (remainingCount === 0) resolve();
|
|
602
|
+
})
|
|
603
|
+
.catch(function(err) {
|
|
604
|
+
remainingCount -= 1;
|
|
605
|
+
console.error('Error uploading file:', err);
|
|
606
|
+
if (remainingCount === 0) resolve();
|
|
607
|
+
});
|
|
608
|
+
} catch (err) {
|
|
609
|
+
remainingCount -= 1;
|
|
610
|
+
console.error('Error processing file:', err);
|
|
611
|
+
if (remainingCount === 0) resolve();
|
|
612
|
+
}
|
|
613
|
+
});
|
|
614
|
+
|
|
615
|
+
upload.run();
|
|
616
|
+
});
|
|
617
|
+
};
|
|
618
|
+
}
|
|
619
|
+
};
|
|
620
|
+
|
|
621
|
+
/**
|
|
622
|
+
* Add a file to the upload queue
|
|
623
|
+
* @param {string} path - API path to upload to
|
|
624
|
+
* @param {File|Object} file - File to upload
|
|
625
|
+
* @param {Object} params - Upload parameters
|
|
626
|
+
* @param {Object} context - Request context
|
|
627
|
+
* @returns {Promise} - Upload promise
|
|
628
|
+
*/
|
|
629
|
+
upload.append = function(path, file, params, context) {
|
|
630
|
+
return new Promise((resolve, reject) => {
|
|
631
|
+
// Process parameters
|
|
632
|
+
params = params || {};
|
|
633
|
+
context = context || fwWrapper.getContext();
|
|
634
|
+
|
|
635
|
+
// Create an upload object
|
|
636
|
+
const uploadObject = {
|
|
637
|
+
path: path,
|
|
638
|
+
file: file,
|
|
639
|
+
resolve: resolve,
|
|
640
|
+
reject: reject,
|
|
641
|
+
status: "pending",
|
|
642
|
+
paused: false,
|
|
643
|
+
up_id: state.nextId++,
|
|
644
|
+
params: params,
|
|
645
|
+
context: { ...context } // Create a copy to avoid modification
|
|
646
|
+
};
|
|
647
|
+
|
|
648
|
+
// Add to queue
|
|
649
|
+
state.queue.push(uploadObject);
|
|
650
|
+
});
|
|
651
|
+
};
|
|
652
|
+
|
|
653
|
+
/**
|
|
654
|
+
* Cancel an upload in progress or in queue
|
|
655
|
+
* @param {number} uploadId - Upload ID to cancel
|
|
656
|
+
*/
|
|
657
|
+
upload.cancelItem = function(uploadId) {
|
|
658
|
+
// Check running uploads
|
|
659
|
+
if (state.running[uploadId]) {
|
|
660
|
+
// Mark running upload as canceled
|
|
661
|
+
state.running[uploadId].canceled = true;
|
|
662
|
+
} else {
|
|
663
|
+
// Check queued uploads
|
|
664
|
+
for (let i = 0; i < state.queue.length; i++) {
|
|
665
|
+
if (state.queue[i].up_id === uploadId) {
|
|
666
|
+
state.queue[i].canceled = true;
|
|
667
|
+
break;
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
// Update progress
|
|
673
|
+
sendProgress();
|
|
674
|
+
};
|
|
675
|
+
|
|
676
|
+
/**
|
|
677
|
+
* Delete an upload from queue or failed list
|
|
678
|
+
* Only canceled uploads can be removed from running list
|
|
679
|
+
* @param {number} uploadId - Upload ID to delete
|
|
680
|
+
*/
|
|
681
|
+
upload.deleteItem = function(uploadId) {
|
|
682
|
+
// Check running uploads
|
|
683
|
+
if (state.running[uploadId]) {
|
|
684
|
+
// Only delete if canceled
|
|
685
|
+
if (state.running[uploadId].canceled) {
|
|
686
|
+
delete state.running[uploadId];
|
|
687
|
+
}
|
|
688
|
+
} else {
|
|
689
|
+
// Check queue
|
|
690
|
+
for (let i = 0; i < state.queue.length; i++) {
|
|
691
|
+
if (state.queue[i].up_id === uploadId) {
|
|
692
|
+
state.queue.splice(i, 1);
|
|
693
|
+
break;
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
// Check failed uploads
|
|
698
|
+
for (let i = 0; i < state.failed.length; i++) {
|
|
699
|
+
if (state.failed[i].up_id === uploadId) {
|
|
700
|
+
state.failed.splice(i, 1);
|
|
701
|
+
break;
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
// Update progress
|
|
707
|
+
sendProgress();
|
|
708
|
+
};
|
|
709
|
+
|
|
710
|
+
/**
|
|
711
|
+
* Pause an active upload
|
|
712
|
+
* @param {number} uploadId - Upload ID to pause
|
|
713
|
+
*/
|
|
714
|
+
upload.pauseItem = function(uploadId) {
|
|
715
|
+
// Find upload in running list
|
|
716
|
+
const upload = state.running[uploadId];
|
|
717
|
+
|
|
718
|
+
// Only pause if active
|
|
719
|
+
if (upload && upload.status === "uploading") {
|
|
720
|
+
upload.paused = true;
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
// Update progress
|
|
724
|
+
sendProgress();
|
|
725
|
+
};
|
|
726
|
+
|
|
727
|
+
/**
|
|
728
|
+
* Resume a paused upload
|
|
729
|
+
* @param {number} uploadId - Upload ID to resume
|
|
730
|
+
*/
|
|
731
|
+
upload.resumeItem = function(uploadId) {
|
|
732
|
+
// Find upload in running list
|
|
733
|
+
const upload = state.running[uploadId];
|
|
734
|
+
|
|
735
|
+
// Only resume if paused
|
|
736
|
+
if (upload && upload.paused) {
|
|
737
|
+
upload.paused = false;
|
|
738
|
+
processActiveUpload(upload);
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
// Update progress
|
|
742
|
+
sendProgress();
|
|
743
|
+
};
|
|
744
|
+
|
|
745
|
+
/**
|
|
746
|
+
* Retry a failed upload
|
|
747
|
+
* @param {number} uploadId - Upload ID to retry
|
|
748
|
+
*/
|
|
749
|
+
upload.retryItem = function(uploadId) {
|
|
750
|
+
// Find upload in failed list
|
|
751
|
+
let failedUpload = null;
|
|
752
|
+
let failedIndex = -1;
|
|
753
|
+
|
|
754
|
+
for (let i = 0; i < state.failed.length; i++) {
|
|
755
|
+
if (state.failed[i].up_id === uploadId) {
|
|
756
|
+
failedUpload = state.failed[i];
|
|
757
|
+
failedIndex = i;
|
|
758
|
+
break;
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
// Skip if not found
|
|
763
|
+
if (!failedUpload) return;
|
|
764
|
+
|
|
765
|
+
// Check if already in queue
|
|
766
|
+
for (let i = 0; i < state.queue.length; i++) {
|
|
767
|
+
if (state.queue[i].up_id === uploadId) {
|
|
768
|
+
return; // Already in queue
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
// Reset failure data
|
|
773
|
+
failedUpload.failure = {};
|
|
774
|
+
|
|
775
|
+
// Reset pending parts
|
|
776
|
+
for (let i = 0; i < failedUpload.blocks; i++) {
|
|
777
|
+
if (failedUpload.b[i] === "pending") {
|
|
778
|
+
failedUpload.b[i] = undefined;
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
// Move from failed to queue
|
|
783
|
+
state.failed.splice(failedIndex, 1);
|
|
784
|
+
state.queue.push(failedUpload);
|
|
785
|
+
|
|
786
|
+
// Restart upload
|
|
787
|
+
upload.run();
|
|
788
|
+
|
|
789
|
+
// Dispatch retry event
|
|
790
|
+
utils.dispatchEvent("upload:retry", { item: failedUpload });
|
|
791
|
+
|
|
792
|
+
// Update progress
|
|
793
|
+
sendProgress();
|
|
794
|
+
};
|
|
795
|
+
|
|
796
|
+
/**
|
|
797
|
+
* Start or continue the upload process
|
|
798
|
+
* Processes queued uploads and continues running uploads
|
|
799
|
+
*/
|
|
800
|
+
upload.run = function() {
|
|
801
|
+
// Fill queue with new uploads
|
|
802
|
+
fillUploadQueue();
|
|
803
|
+
|
|
804
|
+
// Process running uploads
|
|
805
|
+
for (const uploadId in state.running) {
|
|
806
|
+
const upload = state.running[uploadId];
|
|
807
|
+
|
|
808
|
+
// Process based on status
|
|
809
|
+
switch (upload.status) {
|
|
810
|
+
case "pending":
|
|
811
|
+
processUpload(upload);
|
|
812
|
+
break;
|
|
813
|
+
case "uploading":
|
|
814
|
+
processActiveUpload(upload);
|
|
815
|
+
break;
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
};
|
|
819
|
+
|
|
820
|
+
return upload;
|
|
821
|
+
}());
|