@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,312 @@
1
+ import type {
2
+ PickedFile,
3
+ UploadConfig,
4
+ ChunkProgress,
5
+ ChunkUploadConfig,
6
+ UploadError,
7
+ } from '../types';
8
+ import {
9
+ generateId,
10
+ calculateChunkCount,
11
+ getChunkBoundaries,
12
+ createUploadError,
13
+ } from '../utils';
14
+ import { SIZE_LIMITS } from '../constants';
15
+
16
+ export interface ChunkedUploadOptions {
17
+ onProgress?: (progress: ChunkProgress) => void;
18
+ onChunkComplete?: (chunkIndex: number, totalChunks: number) => void;
19
+ abortSignal?: AbortSignal;
20
+ }
21
+
22
+ /**
23
+ * Handles chunked uploads for large files.
24
+ * Supports resume by tracking uploaded chunks.
25
+ */
26
+ export class ChunkedUploader {
27
+ private _chunkSize: number;
28
+ private _uploadedChunks = new Set<number>();
29
+ private _aborted = false;
30
+
31
+ constructor(chunkSize: number = SIZE_LIMITS.DEFAULT_CHUNK_SIZE) {
32
+ this._chunkSize = Math.max(
33
+ SIZE_LIMITS.MIN_CHUNK_SIZE,
34
+ Math.min(chunkSize, SIZE_LIMITS.MAX_CHUNK_SIZE)
35
+ );
36
+ }
37
+
38
+ /**
39
+ * Upload a file in chunks.
40
+ */
41
+ async uploadFile(
42
+ file: PickedFile,
43
+ config: ChunkUploadConfig,
44
+ options: ChunkedUploadOptions = {}
45
+ ): Promise<{ success: boolean; response?: unknown; error?: UploadError }> {
46
+ const { onProgress, onChunkComplete, abortSignal } = options;
47
+
48
+ this._aborted = false;
49
+ this._uploadedChunks.clear();
50
+
51
+ const totalChunks = calculateChunkCount(file.size, this._chunkSize);
52
+ const fileId = config.fileId || generateId('chunked');
53
+
54
+ let uploadedBytes = 0;
55
+
56
+ // Listen for abort
57
+ if (abortSignal) {
58
+ abortSignal.addEventListener('abort', () => {
59
+ this._aborted = true;
60
+ });
61
+ }
62
+
63
+ try {
64
+ // Upload each chunk sequentially
65
+ for (let chunkIndex = 0; chunkIndex < totalChunks; chunkIndex++) {
66
+ if (this._aborted) {
67
+ return {
68
+ success: false,
69
+ error: createUploadError('ABORTED', 'Upload was cancelled'),
70
+ };
71
+ }
72
+
73
+ // Skip already uploaded chunks (for resume)
74
+ if (this._uploadedChunks.has(chunkIndex)) {
75
+ const { end, start } = getChunkBoundaries(file.size, this._chunkSize, chunkIndex);
76
+ uploadedBytes += end - start;
77
+ continue;
78
+ }
79
+
80
+ const result = await this._uploadChunk(
81
+ file,
82
+ chunkIndex,
83
+ totalChunks,
84
+ fileId,
85
+ config
86
+ );
87
+
88
+ if (!result.success) {
89
+ return {
90
+ success: false,
91
+ error: result.error,
92
+ };
93
+ }
94
+
95
+ this._uploadedChunks.add(chunkIndex);
96
+ const { end, start } = getChunkBoundaries(file.size, this._chunkSize, chunkIndex);
97
+ uploadedBytes += end - start;
98
+
99
+ // Report progress
100
+ if (onProgress) {
101
+ onProgress({
102
+ currentChunk: chunkIndex + 1,
103
+ totalChunks,
104
+ bytesUploaded: uploadedBytes,
105
+ bytesTotal: file.size,
106
+ percentage: (uploadedBytes / file.size) * 100,
107
+ });
108
+ }
109
+
110
+ if (onChunkComplete) {
111
+ onChunkComplete(chunkIndex, totalChunks);
112
+ }
113
+ }
114
+
115
+ // Finalize the upload if a finalize URL is provided
116
+ if (config.finalizeUrl) {
117
+ return await this._finalizeUpload(fileId, file, config);
118
+ }
119
+
120
+ return { success: true };
121
+ } catch (error) {
122
+ return {
123
+ success: false,
124
+ error: createUploadError(
125
+ 'CHUNK_FAILED',
126
+ error instanceof Error ? error.message : 'Chunk upload failed',
127
+ undefined,
128
+ error instanceof Error ? error : undefined
129
+ ),
130
+ };
131
+ }
132
+ }
133
+
134
+ /**
135
+ * Get uploaded chunk indices (for resume).
136
+ */
137
+ getUploadedChunks(): number[] {
138
+ return Array.from(this._uploadedChunks);
139
+ }
140
+
141
+ /**
142
+ * Set uploaded chunks (for resume from saved state).
143
+ */
144
+ setUploadedChunks(chunks: number[]): void {
145
+ this._uploadedChunks = new Set(chunks);
146
+ }
147
+
148
+ /**
149
+ * Abort the current upload.
150
+ */
151
+ abort(): void {
152
+ this._aborted = true;
153
+ }
154
+
155
+ /**
156
+ * Reset the uploader state.
157
+ */
158
+ reset(): void {
159
+ this._aborted = false;
160
+ this._uploadedChunks.clear();
161
+ }
162
+
163
+ // ============================================
164
+ // PRIVATE METHODS
165
+ // ============================================
166
+
167
+ private async _uploadChunk(
168
+ file: PickedFile,
169
+ chunkIndex: number,
170
+ totalChunks: number,
171
+ fileId: string,
172
+ config: ChunkUploadConfig
173
+ ): Promise<{ success: boolean; error?: UploadError }> {
174
+ const { start, end } = getChunkBoundaries(file.size, this._chunkSize, chunkIndex);
175
+
176
+ // Get chunk data
177
+ const chunk = await this._getChunkData(file, start, end);
178
+
179
+ // Build form data
180
+ const formData = new FormData();
181
+ formData.append(config.fieldName, chunk, file.name);
182
+ formData.append('fileId', fileId);
183
+ formData.append('chunkIndex', String(chunkIndex));
184
+ formData.append('totalChunks', String(totalChunks));
185
+ formData.append('fileName', file.name);
186
+ formData.append('fileSize', String(file.size));
187
+ formData.append('chunkSize', String(this._chunkSize));
188
+
189
+ // Add additional form data
190
+ if (config.formData) {
191
+ for (const [key, value] of Object.entries(config.formData)) {
192
+ formData.append(key, String(value));
193
+ }
194
+ }
195
+
196
+ // Upload the chunk
197
+ try {
198
+ const response = await fetch(config.url, {
199
+ method: config.method,
200
+ headers: config.headers,
201
+ body: formData,
202
+ signal: this._aborted ? AbortSignal.abort() : undefined,
203
+ });
204
+
205
+ if (!response.ok) {
206
+ return {
207
+ success: false,
208
+ error: createUploadError(
209
+ 'SERVER_ERROR',
210
+ `Server returned ${response.status}`,
211
+ response.status
212
+ ),
213
+ };
214
+ }
215
+
216
+ return { success: true };
217
+ } catch (error) {
218
+ if (error instanceof DOMException && error.name === 'AbortError') {
219
+ return {
220
+ success: false,
221
+ error: createUploadError('ABORTED', 'Chunk upload was cancelled'),
222
+ };
223
+ }
224
+
225
+ return {
226
+ success: false,
227
+ error: createUploadError(
228
+ 'NETWORK_ERROR',
229
+ error instanceof Error ? error.message : 'Network error',
230
+ undefined,
231
+ error instanceof Error ? error : undefined
232
+ ),
233
+ };
234
+ }
235
+ }
236
+
237
+ private async _getChunkData(file: PickedFile, start: number, end: number): Promise<Blob> {
238
+ const data = await file.getData();
239
+
240
+ if (data instanceof Blob) {
241
+ return data.slice(start, end);
242
+ }
243
+
244
+ // For native (base64), convert to Blob
245
+ const base64 = data as string;
246
+ const binaryString = atob(base64);
247
+
248
+ // Slice the relevant portion
249
+ const chunkBinaryString = binaryString.slice(start, end);
250
+ const bytes = new Uint8Array(chunkBinaryString.length);
251
+
252
+ for (let i = 0; i < chunkBinaryString.length; i++) {
253
+ bytes[i] = chunkBinaryString.charCodeAt(i);
254
+ }
255
+
256
+ return new Blob([bytes], { type: file.type });
257
+ }
258
+
259
+ private async _finalizeUpload(
260
+ fileId: string,
261
+ file: PickedFile,
262
+ config: ChunkUploadConfig
263
+ ): Promise<{ success: boolean; response?: unknown; error?: UploadError }> {
264
+ try {
265
+ const response = await fetch(config.finalizeUrl!, {
266
+ method: 'POST',
267
+ headers: {
268
+ 'Content-Type': 'application/json',
269
+ ...config.headers,
270
+ },
271
+ body: JSON.stringify({
272
+ fileId,
273
+ fileName: file.name,
274
+ fileSize: file.size,
275
+ fileType: file.type,
276
+ totalChunks: calculateChunkCount(file.size, this._chunkSize),
277
+ }),
278
+ });
279
+
280
+ if (!response.ok) {
281
+ return {
282
+ success: false,
283
+ error: createUploadError(
284
+ 'SERVER_ERROR',
285
+ `Failed to finalize upload: ${response.status}`,
286
+ response.status
287
+ ),
288
+ };
289
+ }
290
+
291
+ const result = await response.json();
292
+ return { success: true, response: result };
293
+ } catch (error) {
294
+ return {
295
+ success: false,
296
+ error: createUploadError(
297
+ 'NETWORK_ERROR',
298
+ 'Failed to finalize upload',
299
+ undefined,
300
+ error instanceof Error ? error : undefined
301
+ ),
302
+ };
303
+ }
304
+ }
305
+ }
306
+
307
+ /**
308
+ * Create a new ChunkedUploader instance.
309
+ */
310
+ export function createChunkedUploader(chunkSize?: number): ChunkedUploader {
311
+ return new ChunkedUploader(chunkSize);
312
+ }