@idealyst/files 1.2.96
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/package.json +94 -0
- package/src/components/DropZone.native.tsx +96 -0
- package/src/components/DropZone.styles.tsx +99 -0
- package/src/components/DropZone.web.tsx +178 -0
- package/src/components/FilePickerButton.native.tsx +82 -0
- package/src/components/FilePickerButton.styles.tsx +112 -0
- package/src/components/FilePickerButton.web.tsx +84 -0
- package/src/components/UploadProgress.native.tsx +203 -0
- package/src/components/UploadProgress.styles.tsx +90 -0
- package/src/components/UploadProgress.web.tsx +201 -0
- package/src/components/index.native.ts +8 -0
- package/src/components/index.ts +6 -0
- package/src/components/index.web.ts +8 -0
- package/src/constants.ts +336 -0
- package/src/examples/index.ts +181 -0
- package/src/hooks/createUseFilePickerHook.ts +169 -0
- package/src/hooks/createUseFileUploadHook.ts +173 -0
- package/src/hooks/index.native.ts +12 -0
- package/src/hooks/index.ts +12 -0
- package/src/hooks/index.web.ts +12 -0
- package/src/index.native.ts +142 -0
- package/src/index.ts +139 -0
- package/src/index.web.ts +142 -0
- package/src/permissions/index.native.ts +8 -0
- package/src/permissions/index.ts +8 -0
- package/src/permissions/index.web.ts +8 -0
- package/src/permissions/permissions.native.ts +177 -0
- package/src/permissions/permissions.web.ts +96 -0
- package/src/picker/FilePicker.native.ts +407 -0
- package/src/picker/FilePicker.web.ts +366 -0
- package/src/picker/index.native.ts +2 -0
- package/src/picker/index.ts +2 -0
- package/src/picker/index.web.ts +2 -0
- package/src/types.ts +990 -0
- package/src/uploader/ChunkedUploader.ts +312 -0
- package/src/uploader/FileUploader.native.ts +435 -0
- package/src/uploader/FileUploader.web.ts +350 -0
- package/src/uploader/UploadQueue.ts +519 -0
- package/src/uploader/index.native.ts +4 -0
- package/src/uploader/index.ts +4 -0
- package/src/uploader/index.web.ts +4 -0
- package/src/utils.ts +586 -0
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
IFileUploader,
|
|
3
|
+
PickedFile,
|
|
4
|
+
UploadConfig,
|
|
5
|
+
UploadProgressInfo,
|
|
6
|
+
QueueStatus,
|
|
7
|
+
UploadResult,
|
|
8
|
+
UploadError,
|
|
9
|
+
CreateFileUploaderOptions,
|
|
10
|
+
} from '../types';
|
|
11
|
+
import { DEFAULT_UPLOAD_CONFIG } from '../constants';
|
|
12
|
+
import {
|
|
13
|
+
createUploadError,
|
|
14
|
+
shouldUseChunkedUpload,
|
|
15
|
+
generateId,
|
|
16
|
+
} from '../utils';
|
|
17
|
+
import { UploadQueue } from './UploadQueue';
|
|
18
|
+
import { ChunkedUploader } from './ChunkedUploader';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Web implementation of IFileUploader using XMLHttpRequest.
|
|
22
|
+
*/
|
|
23
|
+
export class WebFileUploader implements IFileUploader {
|
|
24
|
+
private _queue: UploadQueue;
|
|
25
|
+
private _abortControllers = new Map<string, AbortController>();
|
|
26
|
+
private _xhrs = new Map<string, XMLHttpRequest>();
|
|
27
|
+
private _chunkedUploaders = new Map<string, ChunkedUploader>();
|
|
28
|
+
private _disposed = false;
|
|
29
|
+
|
|
30
|
+
constructor(options?: CreateFileUploaderOptions) {
|
|
31
|
+
this._queue = new UploadQueue(options?.concurrency || 3);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
get queueStatus(): QueueStatus {
|
|
35
|
+
return this._queue.queueStatus;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
get uploads(): Map<string, UploadProgressInfo> {
|
|
39
|
+
return this._queue.uploads;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
add(files: PickedFile | PickedFile[], config: UploadConfig): string[] {
|
|
43
|
+
const finalConfig = { ...DEFAULT_UPLOAD_CONFIG, ...config } as UploadConfig;
|
|
44
|
+
return this._queue.add(files, finalConfig);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
start(): void {
|
|
48
|
+
this._queue.start();
|
|
49
|
+
this._processQueue();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
pause(): void {
|
|
53
|
+
this._queue.pause();
|
|
54
|
+
|
|
55
|
+
// Abort active uploads
|
|
56
|
+
for (const [id] of this._xhrs) {
|
|
57
|
+
this._abortUpload(id);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
resume(): void {
|
|
62
|
+
this._queue.resume();
|
|
63
|
+
this._processQueue();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
cancel(uploadId: string): void {
|
|
67
|
+
this._abortUpload(uploadId);
|
|
68
|
+
this._queue.cancel(uploadId);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
cancelAll(): void {
|
|
72
|
+
for (const [id] of this._xhrs) {
|
|
73
|
+
this._abortUpload(id);
|
|
74
|
+
}
|
|
75
|
+
this._queue.cancelAll();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
retry(uploadId: string): void {
|
|
79
|
+
this._queue.retry(uploadId);
|
|
80
|
+
this._processQueue();
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
retryAll(): void {
|
|
84
|
+
this._queue.retryAll();
|
|
85
|
+
this._processQueue();
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
remove(uploadId: string): void {
|
|
89
|
+
this._abortUpload(uploadId);
|
|
90
|
+
this._queue.remove(uploadId);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
clearCompleted(): void {
|
|
94
|
+
this._queue.clearCompleted();
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
getUpload(uploadId: string): UploadProgressInfo | undefined {
|
|
98
|
+
return this._queue.getUpload(uploadId);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
onQueueChange(callback: (status: QueueStatus) => void): () => void {
|
|
102
|
+
return this._queue.onQueueChange(callback);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
onProgress(uploadId: string, callback: (progress: UploadProgressInfo) => void): () => void {
|
|
106
|
+
return this._queue.onProgress(uploadId, callback);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
onComplete(callback: (result: UploadResult) => void): () => void {
|
|
110
|
+
return this._queue.onComplete(callback);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
onError(callback: (error: UploadError, uploadId: string) => void): () => void {
|
|
114
|
+
return this._queue.onError(callback);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
dispose(): void {
|
|
118
|
+
this._disposed = true;
|
|
119
|
+
this.cancelAll();
|
|
120
|
+
this._queue.dispose();
|
|
121
|
+
this._abortControllers.clear();
|
|
122
|
+
this._xhrs.clear();
|
|
123
|
+
this._chunkedUploaders.clear();
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// ============================================
|
|
127
|
+
// PRIVATE METHODS
|
|
128
|
+
// ============================================
|
|
129
|
+
|
|
130
|
+
private _processQueue(): void {
|
|
131
|
+
if (this._disposed) return;
|
|
132
|
+
|
|
133
|
+
let nextUpload = this._queue.getNextUpload();
|
|
134
|
+
|
|
135
|
+
while (nextUpload) {
|
|
136
|
+
this._startUpload(nextUpload);
|
|
137
|
+
nextUpload = this._queue.getNextUpload();
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
private async _startUpload(upload: UploadProgressInfo): Promise<void> {
|
|
142
|
+
const { id, file, config } = upload;
|
|
143
|
+
|
|
144
|
+
this._queue.markStarted(id);
|
|
145
|
+
|
|
146
|
+
// Check if we should use chunked upload
|
|
147
|
+
if (shouldUseChunkedUpload(file.size, config)) {
|
|
148
|
+
await this._startChunkedUpload(upload);
|
|
149
|
+
} else {
|
|
150
|
+
await this._startSimpleUpload(upload);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
private async _startSimpleUpload(upload: UploadProgressInfo): Promise<void> {
|
|
155
|
+
const { id, file, config } = upload;
|
|
156
|
+
|
|
157
|
+
const xhr = new XMLHttpRequest();
|
|
158
|
+
this._xhrs.set(id, xhr);
|
|
159
|
+
|
|
160
|
+
const abortController = new AbortController();
|
|
161
|
+
this._abortControllers.set(id, abortController);
|
|
162
|
+
|
|
163
|
+
return new Promise((resolve) => {
|
|
164
|
+
// Track upload progress
|
|
165
|
+
xhr.upload.onprogress = (event) => {
|
|
166
|
+
if (event.lengthComputable) {
|
|
167
|
+
this._queue.updateProgress(id, event.loaded);
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
// Handle completion
|
|
172
|
+
xhr.onload = async () => {
|
|
173
|
+
this._cleanup(id);
|
|
174
|
+
|
|
175
|
+
if (xhr.status >= 200 && xhr.status < 300) {
|
|
176
|
+
let response: unknown;
|
|
177
|
+
try {
|
|
178
|
+
response = JSON.parse(xhr.responseText);
|
|
179
|
+
} catch {
|
|
180
|
+
response = xhr.responseText;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
this._queue.markCompleted(id, response, xhr.status);
|
|
184
|
+
} else {
|
|
185
|
+
this._queue.markFailed(
|
|
186
|
+
id,
|
|
187
|
+
createUploadError('SERVER_ERROR', `Server returned ${xhr.status}`, xhr.status)
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
this._processQueue();
|
|
192
|
+
resolve();
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
// Handle errors
|
|
196
|
+
xhr.onerror = () => {
|
|
197
|
+
this._cleanup(id);
|
|
198
|
+
this._queue.markFailed(id, createUploadError('NETWORK_ERROR', 'Network error occurred'));
|
|
199
|
+
this._processQueue();
|
|
200
|
+
resolve();
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
xhr.ontimeout = () => {
|
|
204
|
+
this._cleanup(id);
|
|
205
|
+
this._queue.markFailed(id, createUploadError('TIMEOUT', 'Upload timed out'));
|
|
206
|
+
this._processQueue();
|
|
207
|
+
resolve();
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
xhr.onabort = () => {
|
|
211
|
+
this._cleanup(id);
|
|
212
|
+
// Don't mark as failed - already handled by cancel/pause
|
|
213
|
+
resolve();
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
// Open connection
|
|
217
|
+
xhr.open(config.method, config.url, true);
|
|
218
|
+
xhr.timeout = config.timeout;
|
|
219
|
+
|
|
220
|
+
// Set headers
|
|
221
|
+
if (config.headers) {
|
|
222
|
+
for (const [key, value] of Object.entries(config.headers)) {
|
|
223
|
+
xhr.setRequestHeader(key, value);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Build and send request
|
|
228
|
+
this._buildAndSendRequest(xhr, file, config);
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
private async _buildAndSendRequest(
|
|
233
|
+
xhr: XMLHttpRequest,
|
|
234
|
+
file: PickedFile,
|
|
235
|
+
config: UploadConfig
|
|
236
|
+
): Promise<void> {
|
|
237
|
+
if (config.multipart) {
|
|
238
|
+
const formData = new FormData();
|
|
239
|
+
|
|
240
|
+
// Get file data
|
|
241
|
+
const data = await file.getData();
|
|
242
|
+
const blob = data instanceof Blob ? data : this._base64ToBlob(data as string, file.type);
|
|
243
|
+
|
|
244
|
+
formData.append(config.fieldName, blob, file.name);
|
|
245
|
+
|
|
246
|
+
// Add additional form data
|
|
247
|
+
if (config.formData) {
|
|
248
|
+
for (const [key, value] of Object.entries(config.formData)) {
|
|
249
|
+
formData.append(key, String(value));
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Apply custom transform if provided
|
|
254
|
+
if (config.transformRequest) {
|
|
255
|
+
const transformed = await config.transformRequest(file, formData);
|
|
256
|
+
xhr.send(transformed);
|
|
257
|
+
} else {
|
|
258
|
+
xhr.send(formData);
|
|
259
|
+
}
|
|
260
|
+
} else {
|
|
261
|
+
// Send raw file data
|
|
262
|
+
const data = await file.getData();
|
|
263
|
+
const blob = data instanceof Blob ? data : this._base64ToBlob(data as string, file.type);
|
|
264
|
+
|
|
265
|
+
xhr.setRequestHeader('Content-Type', file.type);
|
|
266
|
+
xhr.send(blob);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
private async _startChunkedUpload(upload: UploadProgressInfo): Promise<void> {
|
|
271
|
+
const { id, file, config } = upload;
|
|
272
|
+
|
|
273
|
+
const chunkedUploader = new ChunkedUploader(config.chunkSize);
|
|
274
|
+
this._chunkedUploaders.set(id, chunkedUploader);
|
|
275
|
+
|
|
276
|
+
const abortController = new AbortController();
|
|
277
|
+
this._abortControllers.set(id, abortController);
|
|
278
|
+
|
|
279
|
+
const result = await chunkedUploader.uploadFile(
|
|
280
|
+
file,
|
|
281
|
+
{
|
|
282
|
+
...config,
|
|
283
|
+
fileId: generateId('file'),
|
|
284
|
+
},
|
|
285
|
+
{
|
|
286
|
+
onProgress: (progress) => {
|
|
287
|
+
this._queue.updateProgress(id, progress.bytesUploaded, {
|
|
288
|
+
currentChunk: progress.currentChunk,
|
|
289
|
+
totalChunks: progress.totalChunks,
|
|
290
|
+
});
|
|
291
|
+
},
|
|
292
|
+
abortSignal: abortController.signal,
|
|
293
|
+
}
|
|
294
|
+
);
|
|
295
|
+
|
|
296
|
+
this._cleanup(id);
|
|
297
|
+
|
|
298
|
+
if (result.success) {
|
|
299
|
+
this._queue.markCompleted(id, result.response);
|
|
300
|
+
} else if (result.error) {
|
|
301
|
+
this._queue.markFailed(id, result.error);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
this._processQueue();
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
private _abortUpload(uploadId: string): void {
|
|
308
|
+
// Abort XHR
|
|
309
|
+
const xhr = this._xhrs.get(uploadId);
|
|
310
|
+
if (xhr) {
|
|
311
|
+
xhr.abort();
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// Abort controller
|
|
315
|
+
const controller = this._abortControllers.get(uploadId);
|
|
316
|
+
if (controller) {
|
|
317
|
+
controller.abort();
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Abort chunked uploader
|
|
321
|
+
const chunkedUploader = this._chunkedUploaders.get(uploadId);
|
|
322
|
+
if (chunkedUploader) {
|
|
323
|
+
chunkedUploader.abort();
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
private _cleanup(uploadId: string): void {
|
|
328
|
+
this._xhrs.delete(uploadId);
|
|
329
|
+
this._abortControllers.delete(uploadId);
|
|
330
|
+
this._chunkedUploaders.delete(uploadId);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
private _base64ToBlob(base64: string, type: string): Blob {
|
|
334
|
+
const binaryString = atob(base64);
|
|
335
|
+
const bytes = new Uint8Array(binaryString.length);
|
|
336
|
+
|
|
337
|
+
for (let i = 0; i < binaryString.length; i++) {
|
|
338
|
+
bytes[i] = binaryString.charCodeAt(i);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
return new Blob([bytes], { type });
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Create a new WebFileUploader instance.
|
|
347
|
+
*/
|
|
348
|
+
export function createFileUploader(options?: CreateFileUploaderOptions): IFileUploader {
|
|
349
|
+
return new WebFileUploader(options);
|
|
350
|
+
}
|