@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,435 @@
|
|
|
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
|
+
// Lazy load react-native-blob-util
|
|
21
|
+
let BlobUtil: typeof import('react-native-blob-util') | null = null;
|
|
22
|
+
|
|
23
|
+
async function getBlobUtil() {
|
|
24
|
+
if (!BlobUtil) {
|
|
25
|
+
BlobUtil = await import('react-native-blob-util');
|
|
26
|
+
}
|
|
27
|
+
return BlobUtil.default;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface UploadTask {
|
|
31
|
+
cancel: () => void;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Native implementation of IFileUploader using react-native-blob-util.
|
|
36
|
+
*/
|
|
37
|
+
export class NativeFileUploader implements IFileUploader {
|
|
38
|
+
private _queue: UploadQueue;
|
|
39
|
+
private _uploadTasks = new Map<string, UploadTask>();
|
|
40
|
+
private _chunkedUploaders = new Map<string, ChunkedUploader>();
|
|
41
|
+
private _abortControllers = new Map<string, AbortController>();
|
|
42
|
+
private _disposed = false;
|
|
43
|
+
|
|
44
|
+
constructor(options?: CreateFileUploaderOptions) {
|
|
45
|
+
this._queue = new UploadQueue(options?.concurrency || 3);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
get queueStatus(): QueueStatus {
|
|
49
|
+
return this._queue.queueStatus;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
get uploads(): Map<string, UploadProgressInfo> {
|
|
53
|
+
return this._queue.uploads;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
add(files: PickedFile | PickedFile[], config: UploadConfig): string[] {
|
|
57
|
+
const finalConfig = { ...DEFAULT_UPLOAD_CONFIG, ...config } as UploadConfig;
|
|
58
|
+
return this._queue.add(files, finalConfig);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
start(): void {
|
|
62
|
+
this._queue.start();
|
|
63
|
+
this._processQueue();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
pause(): void {
|
|
67
|
+
this._queue.pause();
|
|
68
|
+
|
|
69
|
+
// Cancel active uploads (they'll be retried on resume)
|
|
70
|
+
for (const [id, task] of this._uploadTasks) {
|
|
71
|
+
task.cancel();
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
resume(): void {
|
|
76
|
+
this._queue.resume();
|
|
77
|
+
this._processQueue();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
cancel(uploadId: string): void {
|
|
81
|
+
this._cancelUpload(uploadId);
|
|
82
|
+
this._queue.cancel(uploadId);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
cancelAll(): void {
|
|
86
|
+
for (const [id] of this._uploadTasks) {
|
|
87
|
+
this._cancelUpload(id);
|
|
88
|
+
}
|
|
89
|
+
this._queue.cancelAll();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
retry(uploadId: string): void {
|
|
93
|
+
this._queue.retry(uploadId);
|
|
94
|
+
this._processQueue();
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
retryAll(): void {
|
|
98
|
+
this._queue.retryAll();
|
|
99
|
+
this._processQueue();
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
remove(uploadId: string): void {
|
|
103
|
+
this._cancelUpload(uploadId);
|
|
104
|
+
this._queue.remove(uploadId);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
clearCompleted(): void {
|
|
108
|
+
this._queue.clearCompleted();
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
getUpload(uploadId: string): UploadProgressInfo | undefined {
|
|
112
|
+
return this._queue.getUpload(uploadId);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
onQueueChange(callback: (status: QueueStatus) => void): () => void {
|
|
116
|
+
return this._queue.onQueueChange(callback);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
onProgress(uploadId: string, callback: (progress: UploadProgressInfo) => void): () => void {
|
|
120
|
+
return this._queue.onProgress(uploadId, callback);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
onComplete(callback: (result: UploadResult) => void): () => void {
|
|
124
|
+
return this._queue.onComplete(callback);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
onError(callback: (error: UploadError, uploadId: string) => void): () => void {
|
|
128
|
+
return this._queue.onError(callback);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
dispose(): void {
|
|
132
|
+
this._disposed = true;
|
|
133
|
+
this.cancelAll();
|
|
134
|
+
this._queue.dispose();
|
|
135
|
+
this._uploadTasks.clear();
|
|
136
|
+
this._chunkedUploaders.clear();
|
|
137
|
+
this._abortControllers.clear();
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// ============================================
|
|
141
|
+
// PRIVATE METHODS
|
|
142
|
+
// ============================================
|
|
143
|
+
|
|
144
|
+
private _processQueue(): void {
|
|
145
|
+
if (this._disposed) return;
|
|
146
|
+
|
|
147
|
+
let nextUpload = this._queue.getNextUpload();
|
|
148
|
+
|
|
149
|
+
while (nextUpload) {
|
|
150
|
+
this._startUpload(nextUpload);
|
|
151
|
+
nextUpload = this._queue.getNextUpload();
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
private async _startUpload(upload: UploadProgressInfo): Promise<void> {
|
|
156
|
+
const { id, file, config } = upload;
|
|
157
|
+
|
|
158
|
+
this._queue.markStarted(id);
|
|
159
|
+
|
|
160
|
+
// Check if we should use chunked upload
|
|
161
|
+
if (shouldUseChunkedUpload(file.size, config)) {
|
|
162
|
+
await this._startChunkedUpload(upload);
|
|
163
|
+
} else if (config.backgroundUpload) {
|
|
164
|
+
await this._startBackgroundUpload(upload);
|
|
165
|
+
} else {
|
|
166
|
+
await this._startSimpleUpload(upload);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
private async _startSimpleUpload(upload: UploadProgressInfo): Promise<void> {
|
|
171
|
+
const { id, file, config } = upload;
|
|
172
|
+
|
|
173
|
+
try {
|
|
174
|
+
const RNBlobUtil = await getBlobUtil();
|
|
175
|
+
|
|
176
|
+
// Get file path
|
|
177
|
+
const filePath = file.uri.replace('file://', '');
|
|
178
|
+
|
|
179
|
+
// Build multipart data
|
|
180
|
+
const multipartData: Array<{ name: string; filename?: string; type?: string; data: string }> = [
|
|
181
|
+
{
|
|
182
|
+
name: config.fieldName,
|
|
183
|
+
filename: file.name,
|
|
184
|
+
type: file.type,
|
|
185
|
+
data: RNBlobUtil.wrap(filePath),
|
|
186
|
+
},
|
|
187
|
+
];
|
|
188
|
+
|
|
189
|
+
// Add additional form data
|
|
190
|
+
if (config.formData) {
|
|
191
|
+
for (const [key, value] of Object.entries(config.formData)) {
|
|
192
|
+
multipartData.push({
|
|
193
|
+
name: key,
|
|
194
|
+
data: String(value),
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Create upload task
|
|
200
|
+
const task = RNBlobUtil.fetch(
|
|
201
|
+
config.method,
|
|
202
|
+
config.url,
|
|
203
|
+
{
|
|
204
|
+
...config.headers,
|
|
205
|
+
'Content-Type': 'multipart/form-data',
|
|
206
|
+
},
|
|
207
|
+
multipartData
|
|
208
|
+
);
|
|
209
|
+
|
|
210
|
+
// Store task for cancellation
|
|
211
|
+
this._uploadTasks.set(id, {
|
|
212
|
+
cancel: () => task.cancel(),
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
// Track progress
|
|
216
|
+
task.uploadProgress((written: number, total: number) => {
|
|
217
|
+
this._queue.updateProgress(id, written);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
// Wait for completion
|
|
221
|
+
const response = await task;
|
|
222
|
+
const statusCode = response.respInfo.status;
|
|
223
|
+
|
|
224
|
+
this._cleanup(id);
|
|
225
|
+
|
|
226
|
+
if (statusCode >= 200 && statusCode < 300) {
|
|
227
|
+
let responseData: unknown;
|
|
228
|
+
try {
|
|
229
|
+
responseData = response.json();
|
|
230
|
+
} catch {
|
|
231
|
+
responseData = response.text();
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
this._queue.markCompleted(id, responseData, statusCode);
|
|
235
|
+
} else {
|
|
236
|
+
this._queue.markFailed(
|
|
237
|
+
id,
|
|
238
|
+
createUploadError('SERVER_ERROR', `Server returned ${statusCode}`, statusCode)
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
} catch (error) {
|
|
242
|
+
this._cleanup(id);
|
|
243
|
+
|
|
244
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
245
|
+
|
|
246
|
+
if (errorMessage.includes('cancel')) {
|
|
247
|
+
// Upload was cancelled, don't mark as failed
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
this._queue.markFailed(
|
|
252
|
+
id,
|
|
253
|
+
createUploadError(
|
|
254
|
+
'NETWORK_ERROR',
|
|
255
|
+
errorMessage,
|
|
256
|
+
undefined,
|
|
257
|
+
error instanceof Error ? error : undefined
|
|
258
|
+
)
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
this._processQueue();
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
private async _startBackgroundUpload(upload: UploadProgressInfo): Promise<void> {
|
|
266
|
+
const { id, file, config } = upload;
|
|
267
|
+
|
|
268
|
+
try {
|
|
269
|
+
const RNBlobUtil = await getBlobUtil();
|
|
270
|
+
|
|
271
|
+
// Get file path
|
|
272
|
+
const filePath = file.uri.replace('file://', '');
|
|
273
|
+
|
|
274
|
+
// Configure for background upload
|
|
275
|
+
const sessionConfig = RNBlobUtil.config({
|
|
276
|
+
IOSBackgroundTask: true,
|
|
277
|
+
indicator: true,
|
|
278
|
+
timeout: config.timeout,
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
// Build multipart data
|
|
282
|
+
const multipartData: Array<{ name: string; filename?: string; type?: string; data: string }> = [
|
|
283
|
+
{
|
|
284
|
+
name: config.fieldName,
|
|
285
|
+
filename: file.name,
|
|
286
|
+
type: file.type,
|
|
287
|
+
data: RNBlobUtil.wrap(filePath),
|
|
288
|
+
},
|
|
289
|
+
];
|
|
290
|
+
|
|
291
|
+
// Add additional form data
|
|
292
|
+
if (config.formData) {
|
|
293
|
+
for (const [key, value] of Object.entries(config.formData)) {
|
|
294
|
+
multipartData.push({
|
|
295
|
+
name: key,
|
|
296
|
+
data: String(value),
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Create upload task with background support
|
|
302
|
+
const task = sessionConfig.fetch(
|
|
303
|
+
config.method,
|
|
304
|
+
config.url,
|
|
305
|
+
{
|
|
306
|
+
...config.headers,
|
|
307
|
+
'Content-Type': 'multipart/form-data',
|
|
308
|
+
},
|
|
309
|
+
multipartData
|
|
310
|
+
);
|
|
311
|
+
|
|
312
|
+
// Store task for cancellation
|
|
313
|
+
this._uploadTasks.set(id, {
|
|
314
|
+
cancel: () => task.cancel(),
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
// Track progress (may be delayed until app returns to foreground)
|
|
318
|
+
task.uploadProgress((written: number, total: number) => {
|
|
319
|
+
this._queue.updateProgress(id, written);
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
// Wait for completion
|
|
323
|
+
const response = await task;
|
|
324
|
+
const statusCode = response.respInfo.status;
|
|
325
|
+
|
|
326
|
+
this._cleanup(id);
|
|
327
|
+
|
|
328
|
+
if (statusCode >= 200 && statusCode < 300) {
|
|
329
|
+
let responseData: unknown;
|
|
330
|
+
try {
|
|
331
|
+
responseData = response.json();
|
|
332
|
+
} catch {
|
|
333
|
+
responseData = response.text();
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
this._queue.markCompleted(id, responseData, statusCode);
|
|
337
|
+
} else {
|
|
338
|
+
this._queue.markFailed(
|
|
339
|
+
id,
|
|
340
|
+
createUploadError('SERVER_ERROR', `Server returned ${statusCode}`, statusCode)
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
} catch (error) {
|
|
344
|
+
this._cleanup(id);
|
|
345
|
+
|
|
346
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
347
|
+
|
|
348
|
+
if (errorMessage.includes('cancel')) {
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
this._queue.markFailed(
|
|
353
|
+
id,
|
|
354
|
+
createUploadError(
|
|
355
|
+
'NETWORK_ERROR',
|
|
356
|
+
errorMessage,
|
|
357
|
+
undefined,
|
|
358
|
+
error instanceof Error ? error : undefined
|
|
359
|
+
)
|
|
360
|
+
);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
this._processQueue();
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
private async _startChunkedUpload(upload: UploadProgressInfo): Promise<void> {
|
|
367
|
+
const { id, file, config } = upload;
|
|
368
|
+
|
|
369
|
+
const chunkedUploader = new ChunkedUploader(config.chunkSize);
|
|
370
|
+
this._chunkedUploaders.set(id, chunkedUploader);
|
|
371
|
+
|
|
372
|
+
const abortController = new AbortController();
|
|
373
|
+
this._abortControllers.set(id, abortController);
|
|
374
|
+
|
|
375
|
+
const result = await chunkedUploader.uploadFile(
|
|
376
|
+
file,
|
|
377
|
+
{
|
|
378
|
+
...config,
|
|
379
|
+
fileId: generateId('file'),
|
|
380
|
+
},
|
|
381
|
+
{
|
|
382
|
+
onProgress: (progress) => {
|
|
383
|
+
this._queue.updateProgress(id, progress.bytesUploaded, {
|
|
384
|
+
currentChunk: progress.currentChunk,
|
|
385
|
+
totalChunks: progress.totalChunks,
|
|
386
|
+
});
|
|
387
|
+
},
|
|
388
|
+
abortSignal: abortController.signal,
|
|
389
|
+
}
|
|
390
|
+
);
|
|
391
|
+
|
|
392
|
+
this._cleanup(id);
|
|
393
|
+
|
|
394
|
+
if (result.success) {
|
|
395
|
+
this._queue.markCompleted(id, result.response);
|
|
396
|
+
} else if (result.error) {
|
|
397
|
+
this._queue.markFailed(id, result.error);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
this._processQueue();
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
private _cancelUpload(uploadId: string): void {
|
|
404
|
+
// Cancel upload task
|
|
405
|
+
const task = this._uploadTasks.get(uploadId);
|
|
406
|
+
if (task) {
|
|
407
|
+
task.cancel();
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// Abort controller for chunked uploads
|
|
411
|
+
const controller = this._abortControllers.get(uploadId);
|
|
412
|
+
if (controller) {
|
|
413
|
+
controller.abort();
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// Abort chunked uploader
|
|
417
|
+
const chunkedUploader = this._chunkedUploaders.get(uploadId);
|
|
418
|
+
if (chunkedUploader) {
|
|
419
|
+
chunkedUploader.abort();
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
private _cleanup(uploadId: string): void {
|
|
424
|
+
this._uploadTasks.delete(uploadId);
|
|
425
|
+
this._abortControllers.delete(uploadId);
|
|
426
|
+
this._chunkedUploaders.delete(uploadId);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* Create a new NativeFileUploader instance.
|
|
432
|
+
*/
|
|
433
|
+
export function createFileUploader(options?: CreateFileUploaderOptions): IFileUploader {
|
|
434
|
+
return new NativeFileUploader(options);
|
|
435
|
+
}
|