@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-many.js
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* KLB Upload Many Module
|
|
3
|
+
*
|
|
4
|
+
* This module provides the uploadManyFiles function for batch uploading files.
|
|
5
|
+
*
|
|
6
|
+
* @module upload-many
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
'use strict';
|
|
10
|
+
|
|
11
|
+
const { uploadFile } = require('./upload');
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Upload multiple files with concurrency control
|
|
15
|
+
*
|
|
16
|
+
* This function uploads an array of files to the same API endpoint, with up to 3
|
|
17
|
+
* concurrent uploads at a time. It provides progress information for both individual
|
|
18
|
+
* files and overall progress.
|
|
19
|
+
*
|
|
20
|
+
* @param {string} api - API endpoint path (e.g., 'Misc/Debug:testUpload')
|
|
21
|
+
* @param {Array} files - Array of files to upload. Each element can be:
|
|
22
|
+
* - A browser File object
|
|
23
|
+
* - A Node.js Buffer
|
|
24
|
+
* - An ArrayBuffer or Uint8Array
|
|
25
|
+
* - A file-like object with { name, size, type, content }
|
|
26
|
+
* - A string (will be converted to UTF-8 bytes)
|
|
27
|
+
* @param {string} [method='POST'] - HTTP method for the initial API call
|
|
28
|
+
* @param {Object} [params={}] - Additional parameters to send with each upload.
|
|
29
|
+
* Note: filename, size, type, lastModified are set per-file automatically.
|
|
30
|
+
* @param {Object} [context=null] - Request context (uses default context if not provided)
|
|
31
|
+
* @param {Object} [options={}] - Upload options
|
|
32
|
+
* @param {Function} [options.onProgress] - Progress callback({ fileIndex, fileCount, fileProgress, totalProgress })
|
|
33
|
+
* - fileIndex: Current file index (0-based)
|
|
34
|
+
* - fileCount: Total number of files
|
|
35
|
+
* - fileProgress: Progress of current file (0-1)
|
|
36
|
+
* - totalProgress: Overall progress (0-1)
|
|
37
|
+
* @param {Function} [options.onFileComplete] - Called when each file completes({ fileIndex, fileCount, result })
|
|
38
|
+
* @param {Function} [options.onError] - Error callback(error, context). Can return a Promise
|
|
39
|
+
* that, if resolved, will cause the failed operation to be retried.
|
|
40
|
+
* Context includes { fileIndex, phase, attempt } where phase is 'file' for file-level errors,
|
|
41
|
+
* or 'upload'/'init'/'complete' for block-level errors (also includes blockNum for 'upload').
|
|
42
|
+
* @param {number} [options.concurrency=3] - Maximum concurrent uploads (1-10)
|
|
43
|
+
* @returns {Promise<Array>} - Resolves with array of upload results in same order as input files
|
|
44
|
+
*
|
|
45
|
+
* @example
|
|
46
|
+
* // Upload multiple files from a file input
|
|
47
|
+
* const files = document.querySelector('input[type="file"]').files;
|
|
48
|
+
* const results = await uploadManyFiles('Misc/Debug:testUpload', Array.from(files), 'POST', {
|
|
49
|
+
* image_variation: 'alias=thumb&scale_crop=200x200'
|
|
50
|
+
* }, null, {
|
|
51
|
+
* onProgress: ({ fileIndex, fileCount, totalProgress }) => {
|
|
52
|
+
* console.log(`File ${fileIndex + 1}/${fileCount}, Total: ${Math.round(totalProgress * 100)}%`);
|
|
53
|
+
* },
|
|
54
|
+
* onFileComplete: ({ fileIndex, fileCount, result }) => {
|
|
55
|
+
* console.log(`File ${fileIndex + 1}/${fileCount} complete:`, result);
|
|
56
|
+
* }
|
|
57
|
+
* });
|
|
58
|
+
*
|
|
59
|
+
* @example
|
|
60
|
+
* // Upload buffers with custom concurrency
|
|
61
|
+
* const buffers = [buffer1, buffer2, buffer3, buffer4, buffer5];
|
|
62
|
+
* const results = await uploadManyFiles('Misc/Debug:testUpload', buffers, 'POST', {}, null, {
|
|
63
|
+
* concurrency: 5,
|
|
64
|
+
* onProgress: ({ totalProgress }) => console.log(`${Math.round(totalProgress * 100)}%`)
|
|
65
|
+
* });
|
|
66
|
+
*/
|
|
67
|
+
async function uploadManyFiles(api, files, method, params, context, options) {
|
|
68
|
+
// Handle default values
|
|
69
|
+
method = method || 'POST';
|
|
70
|
+
params = params || {};
|
|
71
|
+
options = options || {};
|
|
72
|
+
|
|
73
|
+
const fileCount = files.length;
|
|
74
|
+
if (fileCount === 0) {
|
|
75
|
+
return [];
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const concurrency = Math.min(Math.max(options.concurrency || 3, 1), 10);
|
|
79
|
+
const { onProgress, onFileComplete, onError } = options;
|
|
80
|
+
|
|
81
|
+
// Results array in same order as input
|
|
82
|
+
const results = new Array(fileCount);
|
|
83
|
+
|
|
84
|
+
// Track progress for each file (0-1)
|
|
85
|
+
const fileProgressArray = new Array(fileCount).fill(0);
|
|
86
|
+
|
|
87
|
+
// Queue of file indices to process
|
|
88
|
+
let nextIndex = 0;
|
|
89
|
+
|
|
90
|
+
// Currently running uploads
|
|
91
|
+
const running = new Set();
|
|
92
|
+
|
|
93
|
+
// Helper to calculate and report progress
|
|
94
|
+
const reportProgress = (fileIndex, fileProgress) => {
|
|
95
|
+
fileProgressArray[fileIndex] = fileProgress;
|
|
96
|
+
|
|
97
|
+
if (onProgress) {
|
|
98
|
+
// Calculate total progress as average of all file progress
|
|
99
|
+
const totalProgress = fileProgressArray.reduce((sum, p) => sum + p, 0) / fileCount;
|
|
100
|
+
|
|
101
|
+
onProgress({
|
|
102
|
+
fileIndex,
|
|
103
|
+
fileCount,
|
|
104
|
+
fileProgress,
|
|
105
|
+
totalProgress
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
// Upload a single file and return its result (with retry support)
|
|
111
|
+
const uploadOne = async (fileIndex) => {
|
|
112
|
+
const file = files[fileIndex];
|
|
113
|
+
let attempt = 0;
|
|
114
|
+
|
|
115
|
+
while (true) {
|
|
116
|
+
attempt++;
|
|
117
|
+
|
|
118
|
+
// Create per-file options with wrapped callbacks
|
|
119
|
+
const fileOptions = {
|
|
120
|
+
onProgress: (progress) => {
|
|
121
|
+
reportProgress(fileIndex, progress);
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
// Wrap onError to include fileIndex for block-level errors
|
|
126
|
+
if (onError) {
|
|
127
|
+
fileOptions.onError = (error, ctx) => {
|
|
128
|
+
return onError(error, { ...ctx, fileIndex });
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
try {
|
|
133
|
+
const result = await uploadFile(api, file, method, { ...params }, context, fileOptions);
|
|
134
|
+
|
|
135
|
+
// Mark as complete
|
|
136
|
+
fileProgressArray[fileIndex] = 1;
|
|
137
|
+
results[fileIndex] = result;
|
|
138
|
+
|
|
139
|
+
if (onFileComplete) {
|
|
140
|
+
onFileComplete({ fileIndex, fileCount, result });
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return result;
|
|
144
|
+
} catch (error) {
|
|
145
|
+
// Give onError a chance to retry the whole file
|
|
146
|
+
if (onError) {
|
|
147
|
+
try {
|
|
148
|
+
await onError(error, { fileIndex, phase: 'file', attempt });
|
|
149
|
+
// Reset progress for retry
|
|
150
|
+
fileProgressArray[fileIndex] = 0;
|
|
151
|
+
continue; // Retry
|
|
152
|
+
} catch (e) {
|
|
153
|
+
// onError rejected, don't retry
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Store error in results and give up
|
|
158
|
+
results[fileIndex] = { error };
|
|
159
|
+
throw error;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
// Process files with concurrency limit
|
|
165
|
+
const processQueue = async () => {
|
|
166
|
+
const workers = [];
|
|
167
|
+
|
|
168
|
+
for (let i = 0; i < concurrency; i++) {
|
|
169
|
+
workers.push((async () => {
|
|
170
|
+
while (nextIndex < fileCount) {
|
|
171
|
+
const fileIndex = nextIndex++;
|
|
172
|
+
running.add(fileIndex);
|
|
173
|
+
|
|
174
|
+
try {
|
|
175
|
+
await uploadOne(fileIndex);
|
|
176
|
+
} catch (error) {
|
|
177
|
+
// Continue with next file even if one fails
|
|
178
|
+
// Error is already stored in results
|
|
179
|
+
} finally {
|
|
180
|
+
running.delete(fileIndex);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
})());
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
await Promise.all(workers);
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
await processQueue();
|
|
190
|
+
|
|
191
|
+
// Check if any uploads failed
|
|
192
|
+
const errors = results.filter(r => r && r.error).map(r => r.error);
|
|
193
|
+
if (errors.length > 0) {
|
|
194
|
+
const error = new Error(`${errors.length} of ${fileCount} uploads failed`);
|
|
195
|
+
error.errors = errors;
|
|
196
|
+
error.results = results;
|
|
197
|
+
throw error;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return results;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Export
|
|
204
|
+
module.exports.uploadManyFiles = uploadManyFiles;
|