@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.
Files changed (42) hide show
  1. package/package.json +94 -0
  2. package/src/components/DropZone.native.tsx +96 -0
  3. package/src/components/DropZone.styles.tsx +99 -0
  4. package/src/components/DropZone.web.tsx +178 -0
  5. package/src/components/FilePickerButton.native.tsx +82 -0
  6. package/src/components/FilePickerButton.styles.tsx +112 -0
  7. package/src/components/FilePickerButton.web.tsx +84 -0
  8. package/src/components/UploadProgress.native.tsx +203 -0
  9. package/src/components/UploadProgress.styles.tsx +90 -0
  10. package/src/components/UploadProgress.web.tsx +201 -0
  11. package/src/components/index.native.ts +8 -0
  12. package/src/components/index.ts +6 -0
  13. package/src/components/index.web.ts +8 -0
  14. package/src/constants.ts +336 -0
  15. package/src/examples/index.ts +181 -0
  16. package/src/hooks/createUseFilePickerHook.ts +169 -0
  17. package/src/hooks/createUseFileUploadHook.ts +173 -0
  18. package/src/hooks/index.native.ts +12 -0
  19. package/src/hooks/index.ts +12 -0
  20. package/src/hooks/index.web.ts +12 -0
  21. package/src/index.native.ts +142 -0
  22. package/src/index.ts +139 -0
  23. package/src/index.web.ts +142 -0
  24. package/src/permissions/index.native.ts +8 -0
  25. package/src/permissions/index.ts +8 -0
  26. package/src/permissions/index.web.ts +8 -0
  27. package/src/permissions/permissions.native.ts +177 -0
  28. package/src/permissions/permissions.web.ts +96 -0
  29. package/src/picker/FilePicker.native.ts +407 -0
  30. package/src/picker/FilePicker.web.ts +366 -0
  31. package/src/picker/index.native.ts +2 -0
  32. package/src/picker/index.ts +2 -0
  33. package/src/picker/index.web.ts +2 -0
  34. package/src/types.ts +990 -0
  35. package/src/uploader/ChunkedUploader.ts +312 -0
  36. package/src/uploader/FileUploader.native.ts +435 -0
  37. package/src/uploader/FileUploader.web.ts +350 -0
  38. package/src/uploader/UploadQueue.ts +519 -0
  39. package/src/uploader/index.native.ts +4 -0
  40. package/src/uploader/index.ts +4 -0
  41. package/src/uploader/index.web.ts +4 -0
  42. 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
+ }