@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,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
|
+
}
|