rn-file-toolkit 1.0.1 → 1.0.3
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/README.md +152 -432
- package/android/src/main/java/com/filetoolkit/FileToolkitModule.kt +142 -12
- package/ios/FileToolkit.mm +219 -110
- package/lib/commonjs/index.js +147 -562
- package/lib/commonjs/index.js.map +1 -1
- package/lib/module/index.js +147 -562
- package/lib/module/index.js.map +1 -1
- package/lib/typescript/commonjs/src/index.d.ts +27 -443
- package/lib/typescript/commonjs/src/index.d.ts.map +1 -1
- package/lib/typescript/module/src/index.d.ts +27 -443
- package/lib/typescript/module/src/index.d.ts.map +1 -1
- package/package.json +10 -3
- package/src/index.tsx +177 -783
package/src/index.tsx
CHANGED
|
@@ -5,131 +5,60 @@ import FileToolkitSpec from './NativeFileToolkit';
|
|
|
5
5
|
const FileToolkitModule = NativeModules.FileToolkit || FileToolkitSpec;
|
|
6
6
|
const eventEmitter = new NativeEventEmitter(FileToolkitModule);
|
|
7
7
|
|
|
8
|
-
// ─── Types ────────────────────────────────────────────────────────────────────
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Rich progress information emitted during a download.
|
|
12
|
-
* Replaces the plain `number` percent from earlier versions.
|
|
13
|
-
*/
|
|
14
8
|
export interface ProgressInfo {
|
|
15
|
-
/** Download progress as a percentage (0–100) */
|
|
16
9
|
percent: number;
|
|
17
|
-
/** Number of bytes downloaded so far */
|
|
18
10
|
bytesDownloaded: number;
|
|
19
|
-
/** Total file size in bytes (0 if unknown) */
|
|
20
11
|
totalBytes: number;
|
|
21
|
-
/** Current download speed in bytes per second */
|
|
22
12
|
speedBps: number;
|
|
23
|
-
/** Estimated seconds remaining (0 if unknown) */
|
|
24
13
|
etaSeconds: number;
|
|
25
14
|
}
|
|
26
15
|
|
|
27
16
|
export interface DownloadOptions {
|
|
28
|
-
/** Remote URL to download from */
|
|
29
17
|
url: string;
|
|
30
|
-
/** Optional file name. Auto-detected from URL if not provided */
|
|
31
18
|
fileName?: string;
|
|
32
|
-
/**
|
|
33
|
-
* Run as a background download.
|
|
34
|
-
* - iOS: uses NSURLSession background configuration
|
|
35
|
-
* - Android: uses system DownloadManager
|
|
36
|
-
* Background downloads survive app suspension. Listen to `onDownloadComplete`
|
|
37
|
-
* and `onDownloadError` events instead of awaiting the promise.
|
|
38
|
-
*/
|
|
39
19
|
background?: boolean;
|
|
40
|
-
/** Custom request headers (e.g. Authorization) */
|
|
41
20
|
headers?: Record<string, string>;
|
|
42
|
-
/**
|
|
43
|
-
* Destination directory for the downloaded file.
|
|
44
|
-
* - 'downloads': Public Downloads folder (default)
|
|
45
|
-
* - 'cache': App-private cache directory (cleared by OS when space is low)
|
|
46
|
-
* - 'documents': App-private documents directory (persisted)
|
|
47
|
-
*/
|
|
48
21
|
destination?: 'downloads' | 'cache' | 'documents';
|
|
49
|
-
/** Android only: custom title for the system download notification */
|
|
50
22
|
notificationTitle?: string;
|
|
51
|
-
/** Android only: custom description for the system download notification */
|
|
52
23
|
notificationDescription?: string;
|
|
53
|
-
/** Optional checksum verification after download completes */
|
|
54
24
|
checksum?: {
|
|
55
25
|
hash: string;
|
|
56
26
|
algorithm: 'md5' | 'sha1' | 'sha256';
|
|
57
27
|
};
|
|
58
|
-
/** Called with rich progress info during foreground downloads */
|
|
59
28
|
onProgress?: (info: ProgressInfo) => void;
|
|
60
|
-
/**
|
|
61
|
-
* Join the managed download queue instead of starting immediately.
|
|
62
|
-
* Respects the `maxConcurrent` limit set via `setQueueOptions()`.
|
|
63
|
-
* Defaults to `false`.
|
|
64
|
-
*/
|
|
65
29
|
queue?: boolean;
|
|
66
|
-
|
|
67
|
-
* Priority inside the queue.
|
|
68
|
-
* - `'high'`: inserted at the front of the pending queue.
|
|
69
|
-
* - `'normal'` (default): appended to the back.
|
|
70
|
-
* Has no effect when `queue` is `false`.
|
|
71
|
-
*/
|
|
30
|
+
downloadId?: string;
|
|
72
31
|
priority?: 'high' | 'normal';
|
|
73
|
-
/**
|
|
74
|
-
* Auto-retry on network failure with exponential backoff.
|
|
75
|
-
* Only retries on network errors (timeouts, connection drops).
|
|
76
|
-
* Server errors (4xx/5xx) and checksum mismatches are NOT retried.
|
|
77
|
-
*
|
|
78
|
-
* @example
|
|
79
|
-
* ```ts
|
|
80
|
-
* download({
|
|
81
|
-
* url: '...',
|
|
82
|
-
* retry: {
|
|
83
|
-
* attempts: 3,
|
|
84
|
-
* delay: 1000, // 1s → 2s → 4s (doubles each time, capped at 30s)
|
|
85
|
-
* onRetry: (attempt, error) => console.log(`Retry #${attempt}: ${error}`),
|
|
86
|
-
* }
|
|
87
|
-
* })
|
|
88
|
-
* ```
|
|
89
|
-
*/
|
|
90
32
|
retry?: {
|
|
91
|
-
/** Maximum number of retry attempts (default: 0 = no retry) */
|
|
92
33
|
attempts: number;
|
|
93
|
-
/** Base delay in ms between retries. Doubles each attempt (default: 1000) */
|
|
94
34
|
delay?: number;
|
|
95
|
-
/** Called just before each retry attempt */
|
|
96
35
|
onRetry?: (attempt: number, error: string) => void;
|
|
97
36
|
};
|
|
98
37
|
}
|
|
99
38
|
|
|
100
39
|
export interface UploadOptions {
|
|
101
|
-
/** Remote URL to upload to */
|
|
102
40
|
url: string;
|
|
103
|
-
/** Absolute local path of the file to upload */
|
|
104
41
|
filePath: string;
|
|
105
|
-
/** Multi-part field name for the file (default: 'file') */
|
|
106
42
|
fieldName?: string;
|
|
107
|
-
/** Custom request headers */
|
|
108
43
|
headers?: Record<string, string>;
|
|
109
|
-
/** Additional text parameters for the multi-part request */
|
|
110
44
|
parameters?: Record<string, string>;
|
|
111
|
-
/** Called with progress 0–100 during upload */
|
|
112
45
|
onProgress?: (percent: number) => void;
|
|
46
|
+
uploadId?: string;
|
|
113
47
|
}
|
|
114
48
|
|
|
115
49
|
export interface DownloadResult {
|
|
116
50
|
success: boolean;
|
|
117
|
-
/** Local path of the saved file */
|
|
118
51
|
filePath?: string;
|
|
119
|
-
/** Unique ID for this download — use with pause/resume/cancel */
|
|
120
52
|
downloadId?: string;
|
|
121
|
-
/** Error message if success is false */
|
|
122
53
|
error?: string;
|
|
123
54
|
}
|
|
124
55
|
|
|
125
56
|
export interface UploadResult {
|
|
126
57
|
success: boolean;
|
|
127
|
-
/** HTTP response status code */
|
|
128
58
|
status?: number;
|
|
129
|
-
/** HTTP response body as string (if any) */
|
|
130
59
|
data?: string;
|
|
131
|
-
/** Error message if success is false */
|
|
132
60
|
error?: string;
|
|
61
|
+
uploadId?: string;
|
|
133
62
|
}
|
|
134
63
|
|
|
135
64
|
export interface ActionResult {
|
|
@@ -140,9 +69,7 @@ export interface ActionResult {
|
|
|
140
69
|
export interface CachedFile {
|
|
141
70
|
fileName: string;
|
|
142
71
|
filePath: string;
|
|
143
|
-
/** File size in bytes */
|
|
144
72
|
size: number;
|
|
145
|
-
/** Last-modified timestamp in milliseconds */
|
|
146
73
|
modifiedAt: number;
|
|
147
74
|
}
|
|
148
75
|
|
|
@@ -153,91 +80,59 @@ export interface CacheResult {
|
|
|
153
80
|
}
|
|
154
81
|
|
|
155
82
|
export interface SaveBase64Options {
|
|
156
|
-
/**
|
|
157
|
-
* Base64 string or data URI (e.g., "data:image/png;base64,iVBORw0...")
|
|
158
|
-
* If a data URI is provided, the base64 portion will be extracted automatically.
|
|
159
|
-
*/
|
|
160
83
|
base64Data: string;
|
|
161
|
-
/** Optional file name. Auto-generated if not provided */
|
|
162
84
|
fileName?: string;
|
|
163
|
-
/**
|
|
164
|
-
* Destination directory for the saved file.
|
|
165
|
-
* - 'downloads': Public Downloads folder (default)
|
|
166
|
-
* - 'cache': App-private cache directory
|
|
167
|
-
* - 'documents': App-private documents directory
|
|
168
|
-
*/
|
|
169
85
|
destination?: 'downloads' | 'cache' | 'documents';
|
|
170
86
|
}
|
|
171
87
|
|
|
172
88
|
export interface SaveBase64Result {
|
|
173
89
|
success: boolean;
|
|
174
|
-
/** Local path of the saved file */
|
|
175
90
|
filePath?: string;
|
|
176
|
-
/** Error message if success is false */
|
|
177
91
|
error?: string;
|
|
178
92
|
}
|
|
179
93
|
|
|
180
94
|
export interface UrlToBase64Options {
|
|
181
|
-
/** URL of the file to convert to base64 (image, video, gif, etc.) */
|
|
182
95
|
url: string;
|
|
183
|
-
/** Optional custom headers (e.g., Authorization) */
|
|
184
96
|
headers?: Record<string, string>;
|
|
185
97
|
}
|
|
186
98
|
|
|
187
99
|
export interface UrlToBase64Result {
|
|
188
100
|
success: boolean;
|
|
189
|
-
/** Base64-encoded string */
|
|
190
101
|
base64?: string;
|
|
191
|
-
/** MIME type detected from response (e.g., 'image/png') */
|
|
192
102
|
mimeType?: string;
|
|
193
|
-
/** Complete data URI (e.g., 'data:image/png;base64,...') */
|
|
194
103
|
dataUri?: string;
|
|
195
|
-
/** Error message if success is false */
|
|
196
104
|
error?: string;
|
|
197
105
|
}
|
|
198
106
|
|
|
199
107
|
export interface ShareFileOptions {
|
|
200
|
-
/** Absolute path to the file to share */
|
|
201
108
|
filePath: string;
|
|
202
|
-
/** Optional title for the share dialog (Android only) */
|
|
203
109
|
title?: string;
|
|
204
|
-
/** Optional subject for email sharing (Android only) */
|
|
205
110
|
subject?: string;
|
|
206
111
|
}
|
|
207
112
|
|
|
208
113
|
export interface OpenFileOptions {
|
|
209
|
-
/** Absolute path to the file to open */
|
|
210
114
|
filePath: string;
|
|
211
|
-
/** MIME type of the file (e.g., 'application/pdf', 'image/jpeg'). Auto-detected if not provided. */
|
|
212
115
|
mimeType?: string;
|
|
213
116
|
}
|
|
214
117
|
|
|
215
118
|
export interface ShareFileResult {
|
|
216
119
|
success: boolean;
|
|
217
|
-
/** Whether user completed the share action (iOS only) */
|
|
218
120
|
completed?: boolean;
|
|
219
|
-
/** Error message if success is false */
|
|
220
121
|
error?: string;
|
|
221
122
|
}
|
|
222
123
|
|
|
223
124
|
export interface OpenFileResult {
|
|
224
125
|
success: boolean;
|
|
225
|
-
/** Error message if success is false */
|
|
226
126
|
error?: string;
|
|
227
127
|
}
|
|
228
128
|
|
|
229
129
|
export type FsEncoding = 'utf8' | 'base64';
|
|
230
130
|
|
|
231
131
|
export interface FsStat {
|
|
232
|
-
/** Absolute path */
|
|
233
132
|
path: string;
|
|
234
|
-
/** Basename */
|
|
235
133
|
name: string;
|
|
236
|
-
/** Size in bytes (0 for directories) */
|
|
237
134
|
size: number;
|
|
238
|
-
/** Last modified timestamp in milliseconds */
|
|
239
135
|
modified: number;
|
|
240
|
-
/** Whether path is a directory */
|
|
241
136
|
isDir: boolean;
|
|
242
137
|
}
|
|
243
138
|
|
|
@@ -257,22 +152,13 @@ export interface FsApi {
|
|
|
257
152
|
ls: (dirPath: string) => Promise<string[]>;
|
|
258
153
|
}
|
|
259
154
|
|
|
260
|
-
// ─── Queue ───────────────────────────────────────────────────────────────────
|
|
261
|
-
|
|
262
155
|
export interface QueueOptions {
|
|
263
|
-
/**
|
|
264
|
-
* Maximum number of simultaneous downloads when using the managed queue.
|
|
265
|
-
* Defaults to `3`.
|
|
266
|
-
*/
|
|
267
156
|
maxConcurrent?: number;
|
|
268
157
|
}
|
|
269
158
|
|
|
270
159
|
export interface QueueStatus {
|
|
271
|
-
/** Number of downloads actively running right now */
|
|
272
160
|
active: number;
|
|
273
|
-
/** Number of downloads waiting in the queue */
|
|
274
161
|
pending: number;
|
|
275
|
-
/** Current `maxConcurrent` setting */
|
|
276
162
|
maxConcurrent: number;
|
|
277
163
|
}
|
|
278
164
|
|
|
@@ -290,7 +176,6 @@ class DownloadQueue {
|
|
|
290
176
|
setOptions(opts: QueueOptions): void {
|
|
291
177
|
if (opts.maxConcurrent != null && opts.maxConcurrent > 0) {
|
|
292
178
|
this._maxConcurrent = opts.maxConcurrent;
|
|
293
|
-
// Kick off any slots that just opened.
|
|
294
179
|
this._flush();
|
|
295
180
|
}
|
|
296
181
|
}
|
|
@@ -319,7 +204,6 @@ class DownloadQueue {
|
|
|
319
204
|
while (this._active < this._maxConcurrent && this._queue.length > 0) {
|
|
320
205
|
const item = this._queue.shift()!;
|
|
321
206
|
this._active++;
|
|
322
|
-
// Strip queue-specific fields before passing to the native layer.
|
|
323
207
|
const nativeOptions = { ...item.options };
|
|
324
208
|
delete nativeOptions.queue;
|
|
325
209
|
delete nativeOptions.priority;
|
|
@@ -340,123 +224,71 @@ class DownloadQueue {
|
|
|
340
224
|
|
|
341
225
|
const _globalQueue = new DownloadQueue();
|
|
342
226
|
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
*
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
227
|
+
function _generateId(): string {
|
|
228
|
+
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
|
|
229
|
+
const r = (Math.random() * 16) | 0;
|
|
230
|
+
const v = c === 'x' ? r : (r & 0x3) | 0x8;
|
|
231
|
+
return v.toString(16);
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
|
|
351
235
|
export function setQueueOptions(options: QueueOptions): void {
|
|
352
236
|
_globalQueue.setOptions(options);
|
|
353
237
|
}
|
|
354
238
|
|
|
355
|
-
/**
|
|
356
|
-
* Get the current state of the managed download queue.
|
|
357
|
-
*
|
|
358
|
-
* @example
|
|
359
|
-
* ```ts
|
|
360
|
-
* const { active, pending, maxConcurrent } = getQueueStatus();
|
|
361
|
-
* ```
|
|
362
|
-
*/
|
|
363
239
|
export function getQueueStatus(): QueueStatus {
|
|
364
240
|
return _globalQueue.getStatus();
|
|
365
241
|
}
|
|
366
242
|
|
|
367
|
-
// ─── Core download ────────────────────────────────────────────────────────────
|
|
368
|
-
|
|
369
|
-
/**
|
|
370
|
-
* Core native download executor — used internally by `download()` and the queue.
|
|
371
|
-
* Never routes through the queue.
|
|
372
|
-
*/
|
|
373
243
|
async function _executeDownload(
|
|
374
244
|
options: DownloadOptions
|
|
375
245
|
): Promise<DownloadResult> {
|
|
376
|
-
let progressSubscription:
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
// downloadId is resolved immediately on Android foreground, allowing
|
|
382
|
-
// ID-based event filtering. On iOS it only arrives on completion, so we
|
|
383
|
-
// fall back to URL-based filtering in that case.
|
|
384
|
-
let knownDownloadId: string | null = null;
|
|
385
|
-
|
|
386
|
-
// Speed / ETA tracking (JS-side, works regardless of native platform)
|
|
246
|
+
let progressSubscription: any = null;
|
|
247
|
+
let retrySubscription: any = null;
|
|
248
|
+
const downloadId = options.downloadId || _generateId();
|
|
249
|
+
const knownDownloadId: string = downloadId;
|
|
387
250
|
let _lastProgressTs: number | null = null;
|
|
388
251
|
let _lastProgressBytes: number = 0;
|
|
389
|
-
// Smoothed speed using exponential moving average (α = 0.3)
|
|
390
252
|
let _smoothedSpeedBps: number = 0;
|
|
391
253
|
|
|
392
254
|
if (options.onProgress) {
|
|
393
255
|
progressSubscription = eventEmitter.addListener(
|
|
394
256
|
'onDownloadProgress',
|
|
395
257
|
(event: any) => {
|
|
396
|
-
const matchesId =
|
|
397
|
-
|
|
398
|
-
const matchesUrl = !knownDownloadId && event.url === options.url;
|
|
258
|
+
const matchesId = event.downloadId === knownDownloadId;
|
|
259
|
+
const matchesUrl = !event.downloadId && event.url === options.url;
|
|
399
260
|
if ((matchesId || matchesUrl) && options.onProgress) {
|
|
400
261
|
const now = Date.now();
|
|
401
|
-
const percent
|
|
402
|
-
const bytesDownloaded
|
|
403
|
-
const totalBytes
|
|
404
|
-
|
|
262
|
+
const percent = event.progress ?? 0;
|
|
263
|
+
const bytesDownloaded = event.bytesDownloaded ?? 0;
|
|
264
|
+
const totalBytes = event.totalBytes ?? 0;
|
|
405
265
|
let speedBps = 0;
|
|
406
266
|
let etaSeconds = 0;
|
|
407
|
-
|
|
408
267
|
if (_lastProgressTs !== null) {
|
|
409
268
|
const dtSec = (now - _lastProgressTs) / 1000;
|
|
410
269
|
if (dtSec > 0) {
|
|
411
270
|
const bytesDelta = bytesDownloaded - _lastProgressBytes;
|
|
412
|
-
// Only update speed if we have byte-level data from native
|
|
413
271
|
if (bytesDelta > 0 && totalBytes > 0) {
|
|
414
272
|
const instantSpeed = bytesDelta / dtSec;
|
|
415
|
-
// Exponential moving average for smoother readings
|
|
416
273
|
_smoothedSpeedBps =
|
|
417
274
|
_smoothedSpeedBps === 0
|
|
418
275
|
? instantSpeed
|
|
419
276
|
: 0.3 * instantSpeed + 0.7 * _smoothedSpeedBps;
|
|
420
277
|
speedBps = _smoothedSpeedBps;
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
} else if (percent > 0 && percent < 100) {
|
|
424
|
-
// Fallback: estimate from percent when bytes aren't available
|
|
425
|
-
const estimatedTotalBytes =
|
|
426
|
-
totalBytes > 0
|
|
427
|
-
? totalBytes
|
|
428
|
-
: _lastProgressBytes / (percent / 100);
|
|
429
|
-
const percentDelta =
|
|
430
|
-
percent - (_lastProgressBytes / estimatedTotalBytes) * 100;
|
|
431
|
-
if (percentDelta > 0) {
|
|
432
|
-
const estimatedByteDelta =
|
|
433
|
-
(percentDelta / 100) * estimatedTotalBytes;
|
|
434
|
-
const instantSpeed = estimatedByteDelta / dtSec;
|
|
435
|
-
_smoothedSpeedBps =
|
|
436
|
-
_smoothedSpeedBps === 0
|
|
437
|
-
? instantSpeed
|
|
438
|
-
: 0.3 * instantSpeed + 0.7 * _smoothedSpeedBps;
|
|
439
|
-
speedBps = _smoothedSpeedBps;
|
|
440
|
-
const remainingPct = 100 - percent;
|
|
441
|
-
const estimatedRemaining =
|
|
442
|
-
(remainingPct / 100) * estimatedTotalBytes;
|
|
443
|
-
etaSeconds = speedBps > 0 ? estimatedRemaining / speedBps : 0;
|
|
444
|
-
}
|
|
278
|
+
etaSeconds =
|
|
279
|
+
speedBps > 0 ? (totalBytes - bytesDownloaded) / speedBps : 0;
|
|
445
280
|
}
|
|
446
281
|
}
|
|
447
282
|
}
|
|
448
|
-
|
|
449
283
|
_lastProgressTs = now;
|
|
450
284
|
_lastProgressBytes = bytesDownloaded;
|
|
451
|
-
|
|
452
|
-
const info: ProgressInfo = {
|
|
285
|
+
options.onProgress!({
|
|
453
286
|
percent,
|
|
454
287
|
bytesDownloaded,
|
|
455
288
|
totalBytes,
|
|
456
289
|
speedBps,
|
|
457
290
|
etaSeconds,
|
|
458
|
-
};
|
|
459
|
-
options.onProgress(info);
|
|
291
|
+
});
|
|
460
292
|
}
|
|
461
293
|
}
|
|
462
294
|
);
|
|
@@ -466,9 +298,8 @@ async function _executeDownload(
|
|
|
466
298
|
retrySubscription = eventEmitter.addListener(
|
|
467
299
|
'onDownloadRetry',
|
|
468
300
|
(event: any) => {
|
|
469
|
-
const matchesId =
|
|
470
|
-
|
|
471
|
-
const matchesUrl = !knownDownloadId && event.url === options.url;
|
|
301
|
+
const matchesId = event.downloadId === knownDownloadId;
|
|
302
|
+
const matchesUrl = !event.downloadId && event.url === options.url;
|
|
472
303
|
if ((matchesId || matchesUrl) && options.retry?.onRetry) {
|
|
473
304
|
options.retry.onRetry(event.attempt, event.error ?? '');
|
|
474
305
|
}
|
|
@@ -482,35 +313,13 @@ async function _executeDownload(
|
|
|
482
313
|
};
|
|
483
314
|
|
|
484
315
|
try {
|
|
485
|
-
const
|
|
486
|
-
|
|
487
|
-
|
|
316
|
+
const result = await (FileToolkitModule as any).download({
|
|
317
|
+
...options,
|
|
318
|
+
downloadId,
|
|
488
319
|
background: options.background ?? false,
|
|
489
320
|
headers: options.headers ?? {},
|
|
490
321
|
destination: options.destination ?? 'downloads',
|
|
491
|
-
notificationTitle: options.notificationTitle,
|
|
492
|
-
notificationDescription: options.notificationDescription,
|
|
493
|
-
checksum: options.checksum,
|
|
494
|
-
retry: options.retry
|
|
495
|
-
? {
|
|
496
|
-
attempts: options.retry.attempts,
|
|
497
|
-
delay: options.retry.delay ?? 1000,
|
|
498
|
-
}
|
|
499
|
-
: undefined,
|
|
500
|
-
});
|
|
501
|
-
|
|
502
|
-
// On Android foreground the promise resolves immediately with downloadId,
|
|
503
|
-
// giving us a precise ID to filter events by before download completes.
|
|
504
|
-
// On iOS it only resolves on completion, so knownDownloadId stays null
|
|
505
|
-
// and URL-based filtering is used as the fallback.
|
|
506
|
-
resultPromise.then?.((partial: any) => {
|
|
507
|
-
if (partial?.downloadId && !partial?.filePath) {
|
|
508
|
-
// Android early-resolve: has downloadId but no filePath yet
|
|
509
|
-
knownDownloadId = partial.downloadId;
|
|
510
|
-
}
|
|
511
322
|
});
|
|
512
|
-
|
|
513
|
-
const result = await resultPromise;
|
|
514
323
|
cleanup();
|
|
515
324
|
return result as DownloadResult;
|
|
516
325
|
} catch (error: any) {
|
|
@@ -519,277 +328,146 @@ async function _executeDownload(
|
|
|
519
328
|
}
|
|
520
329
|
}
|
|
521
330
|
|
|
522
|
-
/**
|
|
523
|
-
* Download a file.
|
|
524
|
-
*
|
|
525
|
-
* For **foreground** downloads the promise resolves when the file is saved.
|
|
526
|
-
* For **background** downloads the promise resolves immediately with a
|
|
527
|
-
* `downloadId`; listen to the `onDownloadComplete` / `onDownloadError`
|
|
528
|
-
* events for the final result.
|
|
529
|
-
*
|
|
530
|
-
* Pass `queue: true` to join the managed queue and respect the `maxConcurrent`
|
|
531
|
-
* limit configured via `setQueueOptions()`. Use `priority: 'high'` to jump
|
|
532
|
-
* ahead of other pending items in the queue.
|
|
533
|
-
*
|
|
534
|
-
* @example
|
|
535
|
-
* ```ts
|
|
536
|
-
* // Direct (unqueued) download
|
|
537
|
-
* const result = await download({ url: 'https://...' });
|
|
538
|
-
*
|
|
539
|
-
* // Queued download with high priority
|
|
540
|
-
* setQueueOptions({ maxConcurrent: 3 });
|
|
541
|
-
* const result = await download({
|
|
542
|
-
* url: 'https://...',
|
|
543
|
-
* queue: true,
|
|
544
|
-
* priority: 'high',
|
|
545
|
-
* });
|
|
546
|
-
* ```
|
|
547
|
-
*/
|
|
548
331
|
export function download(options: DownloadOptions): Promise<DownloadResult> {
|
|
549
|
-
if (options.queue)
|
|
550
|
-
return _globalQueue.enqueue(options);
|
|
551
|
-
}
|
|
332
|
+
if (options.queue) return _globalQueue.enqueue(options);
|
|
552
333
|
return _executeDownload(options);
|
|
553
334
|
}
|
|
554
335
|
|
|
555
|
-
// ─── Core upload ───────────────────────────────────────────────────────────────
|
|
556
|
-
|
|
557
|
-
/**
|
|
558
|
-
* Upload a file using multipart/form-data.
|
|
559
|
-
*/
|
|
560
336
|
export async function upload(options: UploadOptions): Promise<UploadResult> {
|
|
561
|
-
let
|
|
562
|
-
|
|
563
|
-
|
|
337
|
+
let sub: any = null;
|
|
338
|
+
const uploadId = options.uploadId || _generateId();
|
|
564
339
|
if (options.onProgress) {
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
if (event.url === options.url && options.onProgress) {
|
|
569
|
-
options.onProgress(event.progress);
|
|
570
|
-
}
|
|
571
|
-
}
|
|
572
|
-
);
|
|
340
|
+
sub = eventEmitter.addListener('onUploadProgress', (e: any) => {
|
|
341
|
+
if (e.uploadId === uploadId) options.onProgress!(e.progress);
|
|
342
|
+
});
|
|
573
343
|
}
|
|
574
|
-
|
|
575
344
|
try {
|
|
576
|
-
const
|
|
577
|
-
|
|
578
|
-
|
|
345
|
+
const res = await (FileToolkitModule as any).upload({
|
|
346
|
+
...options,
|
|
347
|
+
uploadId,
|
|
579
348
|
fieldName: options.fieldName ?? 'file',
|
|
580
349
|
headers: options.headers ?? {},
|
|
581
350
|
parameters: options.parameters ?? {},
|
|
582
351
|
});
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
return result as UploadResult;
|
|
589
|
-
} catch (error: any) {
|
|
590
|
-
if (progressSubscription) {
|
|
591
|
-
progressSubscription.remove();
|
|
592
|
-
}
|
|
593
|
-
return { success: false, error: error?.message || 'UNKNOWN_ERROR' };
|
|
352
|
+
sub?.remove();
|
|
353
|
+
return { ...res, uploadId } as UploadResult;
|
|
354
|
+
} catch (err: any) {
|
|
355
|
+
sub?.remove();
|
|
356
|
+
return { success: false, error: err?.message || 'UNKNOWN_ERROR', uploadId };
|
|
594
357
|
}
|
|
595
358
|
}
|
|
596
359
|
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
/**
|
|
600
|
-
* Pause an active foreground download.
|
|
601
|
-
* On iOS the partial data is stored so it can be resumed.
|
|
602
|
-
* On Android the download thread is suspended.
|
|
603
|
-
*/
|
|
604
|
-
export async function pauseDownload(downloadId: string): Promise<ActionResult> {
|
|
360
|
+
export async function pauseDownload(id: string): Promise<ActionResult> {
|
|
605
361
|
try {
|
|
606
|
-
return
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
} catch (error: any) {
|
|
610
|
-
return { success: false, error: error?.message || 'UNKNOWN_ERROR' };
|
|
362
|
+
return await (FileToolkitModule as any).pauseDownload(id);
|
|
363
|
+
} catch (err: any) {
|
|
364
|
+
return { success: false, error: err.message || 'UNKNOWN_ERROR' };
|
|
611
365
|
}
|
|
612
366
|
}
|
|
613
367
|
|
|
614
|
-
|
|
615
|
-
* Resume a previously paused download.
|
|
616
|
-
* On iOS resumes from partial data (HTTP Range). On Android unblocks the thread.
|
|
617
|
-
*/
|
|
618
|
-
export async function resumeDownload(
|
|
619
|
-
downloadId: string
|
|
620
|
-
): Promise<ActionResult> {
|
|
368
|
+
export async function resumeDownload(id: string): Promise<ActionResult> {
|
|
621
369
|
try {
|
|
622
|
-
return
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
} catch (error: any) {
|
|
626
|
-
return { success: false, error: error?.message || 'UNKNOWN_ERROR' };
|
|
370
|
+
return await (FileToolkitModule as any).resumeDownload(id);
|
|
371
|
+
} catch (err: any) {
|
|
372
|
+
return { success: false, error: err.message || 'UNKNOWN_ERROR' };
|
|
627
373
|
}
|
|
628
374
|
}
|
|
629
375
|
|
|
630
|
-
|
|
631
|
-
* Cancel and discard a download (foreground or background).
|
|
632
|
-
* Any partially-downloaded file is deleted.
|
|
633
|
-
*/
|
|
634
|
-
export async function cancelDownload(
|
|
635
|
-
downloadId: string
|
|
636
|
-
): Promise<ActionResult> {
|
|
376
|
+
export async function cancelDownload(id: string): Promise<ActionResult> {
|
|
637
377
|
try {
|
|
638
|
-
return
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
} catch (error: any) {
|
|
642
|
-
return { success: false, error: error?.message || 'UNKNOWN_ERROR' };
|
|
378
|
+
return await (FileToolkitModule as any).cancelDownload(id);
|
|
379
|
+
} catch (err: any) {
|
|
380
|
+
return { success: false, error: err.message || 'UNKNOWN_ERROR' };
|
|
643
381
|
}
|
|
644
382
|
}
|
|
645
383
|
|
|
646
|
-
// ─── Cache management ─────────────────────────────────────────────────────────
|
|
647
|
-
|
|
648
|
-
/**
|
|
649
|
-
* List all files in the app's cache directory.
|
|
650
|
-
*/
|
|
651
384
|
export async function getCachedFiles(): Promise<CacheResult> {
|
|
652
385
|
try {
|
|
653
|
-
return
|
|
654
|
-
} catch (
|
|
655
|
-
return { success: false, error:
|
|
386
|
+
return await (FileToolkitModule as any).getCachedFiles();
|
|
387
|
+
} catch (err: any) {
|
|
388
|
+
return { success: false, error: err.message || 'UNKNOWN_ERROR' };
|
|
656
389
|
}
|
|
657
390
|
}
|
|
658
391
|
|
|
659
|
-
|
|
660
|
-
* Delete a single file by its absolute path.
|
|
661
|
-
*/
|
|
662
|
-
export async function deleteFile(filePath: string): Promise<ActionResult> {
|
|
392
|
+
export async function deleteFile(path: string): Promise<ActionResult> {
|
|
663
393
|
try {
|
|
664
|
-
return
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
} catch (error: any) {
|
|
668
|
-
return { success: false, error: error?.message || 'UNKNOWN_ERROR' };
|
|
394
|
+
return await (FileToolkitModule as any).deleteFile(path);
|
|
395
|
+
} catch (err: any) {
|
|
396
|
+
return { success: false, error: err.message || 'UNKNOWN_ERROR' };
|
|
669
397
|
}
|
|
670
398
|
}
|
|
671
399
|
|
|
672
|
-
/**
|
|
673
|
-
* Delete all files in the app's cache directory.
|
|
674
|
-
*/
|
|
675
400
|
export async function clearCache(): Promise<ActionResult> {
|
|
676
401
|
try {
|
|
677
|
-
return
|
|
678
|
-
} catch (
|
|
679
|
-
return { success: false, error:
|
|
402
|
+
return await (FileToolkitModule as any).clearCache();
|
|
403
|
+
} catch (err: any) {
|
|
404
|
+
return { success: false, error: err.message || 'UNKNOWN_ERROR' };
|
|
680
405
|
}
|
|
681
406
|
}
|
|
682
407
|
|
|
683
|
-
|
|
684
|
-
* Get all active background downloads.
|
|
685
|
-
* Use this after app restart to "re-attach" to ongoing downloads.
|
|
686
|
-
*/
|
|
687
|
-
export async function getBackgroundDownloads(): Promise<{
|
|
688
|
-
success: boolean;
|
|
689
|
-
downloads?: Array<{
|
|
690
|
-
downloadId: string;
|
|
691
|
-
url: string;
|
|
692
|
-
status: number;
|
|
693
|
-
progress: number;
|
|
694
|
-
}>;
|
|
695
|
-
error?: string;
|
|
696
|
-
}> {
|
|
408
|
+
export async function getBackgroundDownloads(): Promise<any> {
|
|
697
409
|
try {
|
|
698
|
-
return
|
|
699
|
-
} catch (
|
|
700
|
-
return { success: false, error:
|
|
410
|
+
return await (FileToolkitModule as any).getBackgroundDownloads();
|
|
411
|
+
} catch (err: any) {
|
|
412
|
+
return { success: false, error: err.message || 'UNKNOWN_ERROR' };
|
|
701
413
|
}
|
|
702
414
|
}
|
|
703
415
|
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
function _ensureFsSuccess(result: any, fallback: string): void {
|
|
707
|
-
if (!result?.success) {
|
|
708
|
-
throw new Error(result?.error || fallback);
|
|
709
|
-
}
|
|
416
|
+
function _ensure(res: any, msg: string) {
|
|
417
|
+
if (!res?.success) throw new Error(res?.error || msg);
|
|
710
418
|
}
|
|
711
419
|
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
return !!result.exists;
|
|
420
|
+
export async function exists(path: string): Promise<boolean> {
|
|
421
|
+
const res = await (FileToolkitModule as any).exists(path);
|
|
422
|
+
_ensure(res, 'EXISTS_ERROR');
|
|
423
|
+
return !!res.exists;
|
|
717
424
|
}
|
|
718
425
|
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
return result.stat as FsStat;
|
|
426
|
+
export async function stat(path: string): Promise<FsStat> {
|
|
427
|
+
const res = await (FileToolkitModule as any).stat(path);
|
|
428
|
+
_ensure(res, 'STAT_ERROR');
|
|
429
|
+
return res.stat;
|
|
724
430
|
}
|
|
725
431
|
|
|
726
|
-
/** Read a file as utf8 (default) or base64 string. */
|
|
727
432
|
export async function readFile(
|
|
728
|
-
|
|
729
|
-
|
|
433
|
+
path: string,
|
|
434
|
+
enc: FsEncoding = 'utf8'
|
|
730
435
|
): Promise<string> {
|
|
731
|
-
const
|
|
732
|
-
|
|
733
|
-
return
|
|
436
|
+
const res = await (FileToolkitModule as any).readFile(path, enc);
|
|
437
|
+
_ensure(res, 'READ_ERROR');
|
|
438
|
+
return res.data ?? '';
|
|
734
439
|
}
|
|
735
440
|
|
|
736
|
-
/** Write utf8 (default) or base64 data to a file. */
|
|
737
441
|
export async function writeFile(
|
|
738
|
-
|
|
442
|
+
path: string,
|
|
739
443
|
data: string,
|
|
740
|
-
|
|
444
|
+
enc: FsEncoding = 'utf8'
|
|
741
445
|
): Promise<void> {
|
|
742
|
-
const
|
|
743
|
-
|
|
744
|
-
data,
|
|
745
|
-
encoding
|
|
746
|
-
);
|
|
747
|
-
_ensureFsSuccess(result, 'WRITE_FILE_ERROR');
|
|
446
|
+
const res = await (FileToolkitModule as any).writeFile(path, data, enc);
|
|
447
|
+
_ensure(res, 'WRITE_ERROR');
|
|
748
448
|
}
|
|
749
449
|
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
toPath: string
|
|
754
|
-
): Promise<void> {
|
|
755
|
-
const result = await (FileToolkitSpec as any).copyFile(fromPath, toPath);
|
|
756
|
-
_ensureFsSuccess(result, 'COPY_FILE_ERROR');
|
|
450
|
+
export async function copyFile(from: string, to: string): Promise<void> {
|
|
451
|
+
const res = await (FileToolkitModule as any).copyFile(from, to);
|
|
452
|
+
_ensure(res, 'COPY_ERROR');
|
|
757
453
|
}
|
|
758
454
|
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
toPath: string
|
|
763
|
-
): Promise<void> {
|
|
764
|
-
const result = await (FileToolkitSpec as any).moveFile(fromPath, toPath);
|
|
765
|
-
_ensureFsSuccess(result, 'MOVE_FILE_ERROR');
|
|
455
|
+
export async function moveFile(from: string, to: string): Promise<void> {
|
|
456
|
+
const res = await (FileToolkitModule as any).moveFile(from, to);
|
|
457
|
+
_ensure(res, 'MOVE_ERROR');
|
|
766
458
|
}
|
|
767
459
|
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
_ensureFsSuccess(result, 'MKDIR_ERROR');
|
|
460
|
+
export async function mkdir(path: string): Promise<void> {
|
|
461
|
+
const res = await (FileToolkitModule as any).mkdir(path);
|
|
462
|
+
_ensure(res, 'MKDIR_ERROR');
|
|
772
463
|
}
|
|
773
464
|
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
return (result.entries || []) as string[];
|
|
465
|
+
export async function ls(path: string): Promise<string[]> {
|
|
466
|
+
const res = await (FileToolkitModule as any).ls(path);
|
|
467
|
+
_ensure(res, 'LS_ERROR');
|
|
468
|
+
return res.entries || [];
|
|
779
469
|
}
|
|
780
470
|
|
|
781
|
-
/**
|
|
782
|
-
* File system API namespace.
|
|
783
|
-
*
|
|
784
|
-
* @example
|
|
785
|
-
* ```ts
|
|
786
|
-
* import { fs } from 'rn-file-toolkit';
|
|
787
|
-
*
|
|
788
|
-
* const ok = await fs.exists('/path/to/file.pdf');
|
|
789
|
-
* const meta = await fs.stat('/path/to/file.pdf');
|
|
790
|
-
* const text = await fs.readFile('/path/to/file.txt');
|
|
791
|
-
* ```
|
|
792
|
-
*/
|
|
793
471
|
export const fs: FsApi = {
|
|
794
472
|
exists,
|
|
795
473
|
stat,
|
|
@@ -797,454 +475,170 @@ export const fs: FsApi = {
|
|
|
797
475
|
writeFile,
|
|
798
476
|
copyFile,
|
|
799
477
|
moveFile,
|
|
800
|
-
deleteFile: async (
|
|
801
|
-
|
|
802
|
-
_ensureFsSuccess(result, 'DELETE_FILE_ERROR');
|
|
478
|
+
deleteFile: async (p) => {
|
|
479
|
+
_ensure(await deleteFile(p), 'DEL_ERROR');
|
|
803
480
|
},
|
|
804
481
|
mkdir,
|
|
805
482
|
ls,
|
|
806
483
|
};
|
|
807
484
|
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
* Subscribe to background download completion events.
|
|
812
|
-
* Returns an unsubscribe function.
|
|
813
|
-
*/
|
|
814
|
-
export function onDownloadComplete(
|
|
815
|
-
callback: (result: DownloadResult) => void
|
|
816
|
-
): () => void {
|
|
817
|
-
const sub = eventEmitter.addListener('onDownloadComplete', callback as any);
|
|
818
|
-
return () => sub.remove();
|
|
485
|
+
export function onDownloadComplete(cb: any) {
|
|
486
|
+
const s = eventEmitter.addListener('onDownloadComplete', cb);
|
|
487
|
+
return () => s.remove();
|
|
819
488
|
}
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
* Returns an unsubscribe function.
|
|
824
|
-
*/
|
|
825
|
-
export function onDownloadError(
|
|
826
|
-
callback: (result: DownloadResult) => void
|
|
827
|
-
): () => void {
|
|
828
|
-
const sub = eventEmitter.addListener('onDownloadError', callback as any);
|
|
829
|
-
return () => sub.remove();
|
|
489
|
+
export function onDownloadError(cb: any) {
|
|
490
|
+
const s = eventEmitter.addListener('onDownloadError', cb);
|
|
491
|
+
return () => s.remove();
|
|
830
492
|
}
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
* Returns an unsubscribe function.
|
|
835
|
-
*/
|
|
836
|
-
export function onUploadProgress(
|
|
837
|
-
callback: (result: { url: string; progress: number }) => void
|
|
838
|
-
): () => void {
|
|
839
|
-
const sub = eventEmitter.addListener('onUploadProgress', callback as any);
|
|
840
|
-
return () => sub.remove();
|
|
493
|
+
export function onUploadProgress(cb: any) {
|
|
494
|
+
const s = eventEmitter.addListener('onUploadProgress', cb);
|
|
495
|
+
return () => s.remove();
|
|
841
496
|
}
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
* Returns an unsubscribe function.
|
|
846
|
-
*/
|
|
847
|
-
export function onDownloadRetry(
|
|
848
|
-
callback: (result: {
|
|
849
|
-
downloadId: string;
|
|
850
|
-
url: string;
|
|
851
|
-
attempt: number;
|
|
852
|
-
}) => void
|
|
853
|
-
): () => void {
|
|
854
|
-
const sub = eventEmitter.addListener('onDownloadRetry', callback as any);
|
|
855
|
-
return () => sub.remove();
|
|
497
|
+
export function onDownloadRetry(cb: any) {
|
|
498
|
+
const s = eventEmitter.addListener('onDownloadRetry', cb);
|
|
499
|
+
return () => s.remove();
|
|
856
500
|
}
|
|
857
501
|
|
|
858
|
-
// ─── saveBase64AsFile ─────────────────────────────────────────────────────────
|
|
859
|
-
|
|
860
|
-
/**
|
|
861
|
-
* Save a base64 string or data URI as a file.
|
|
862
|
-
* Supports data URIs (e.g., "data:image/png;base64,iVBORw0...") and plain base64 strings.
|
|
863
|
-
*
|
|
864
|
-
* @example
|
|
865
|
-
* ```typescript
|
|
866
|
-
* // From data URI
|
|
867
|
-
* const result = await saveBase64AsFile({
|
|
868
|
-
* base64Data: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUg...',
|
|
869
|
-
* fileName: 'photo.png',
|
|
870
|
-
* destination: 'documents'
|
|
871
|
-
* });
|
|
872
|
-
*
|
|
873
|
-
* // From plain base64
|
|
874
|
-
* const result = await saveBase64AsFile({
|
|
875
|
-
* base64Data: 'SGVsbG8gV29ybGQ=',
|
|
876
|
-
* fileName: 'hello.txt',
|
|
877
|
-
* destination: 'cache'
|
|
878
|
-
* });
|
|
879
|
-
* ```
|
|
880
|
-
*/
|
|
881
502
|
export async function saveBase64AsFile(
|
|
882
|
-
|
|
503
|
+
opts: SaveBase64Options
|
|
883
504
|
): Promise<SaveBase64Result> {
|
|
884
505
|
try {
|
|
885
|
-
let { base64Data, fileName, destination } =
|
|
886
|
-
|
|
887
|
-
// Parse data URI if present (e.g., "data:image/png;base64,iVBORw0...")
|
|
506
|
+
let { base64Data, fileName, destination } = opts;
|
|
888
507
|
if (base64Data.startsWith('data:')) {
|
|
889
|
-
const
|
|
890
|
-
if (
|
|
891
|
-
base64Data =
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
if (!fileName && matches[1]) {
|
|
895
|
-
const mimeType = matches[1];
|
|
896
|
-
const ext = mimeType.split('/')[1] || 'bin';
|
|
897
|
-
fileName = `file_${Date.now()}.${ext}`;
|
|
898
|
-
}
|
|
899
|
-
} else {
|
|
900
|
-
return {
|
|
901
|
-
success: false,
|
|
902
|
-
error:
|
|
903
|
-
'Invalid data URI format. Expected: data:<mimetype>;base64,<data>',
|
|
904
|
-
};
|
|
508
|
+
const m = base64Data.match(/^data:([^;]+);base64,(.+)$/);
|
|
509
|
+
if (m && m[2]) {
|
|
510
|
+
base64Data = m[2];
|
|
511
|
+
if (!fileName && m[1])
|
|
512
|
+
fileName = `file_${Date.now()}.${m[1].split('/')[1] || 'bin'}`;
|
|
905
513
|
}
|
|
906
514
|
}
|
|
907
|
-
|
|
908
|
-
const result = await FileToolkitModule.saveBase64AsFile({
|
|
515
|
+
return await FileToolkitModule.saveBase64AsFile({
|
|
909
516
|
base64Data,
|
|
910
517
|
fileName,
|
|
911
518
|
destination,
|
|
912
519
|
});
|
|
913
|
-
|
|
914
|
-
return
|
|
915
|
-
} catch (error: any) {
|
|
916
|
-
return { success: false, error: error?.message || 'UNKNOWN_ERROR' };
|
|
520
|
+
} catch (err: any) {
|
|
521
|
+
return { success: false, error: err.message || 'UNKNOWN_ERROR' };
|
|
917
522
|
}
|
|
918
523
|
}
|
|
919
524
|
|
|
920
|
-
// ─── urlToBase64 ──────────────────────────────────────────────────────────────
|
|
921
|
-
|
|
922
|
-
/**
|
|
923
|
-
* Convert a web URL (image, video, gif, etc.) to base64 string.
|
|
924
|
-
* Downloads the file and returns both the base64 string and data URI format.
|
|
925
|
-
*
|
|
926
|
-
* @example
|
|
927
|
-
* ```typescript
|
|
928
|
-
* // Convert image to base64
|
|
929
|
-
* const result = await urlToBase64({
|
|
930
|
-
* url: 'https://example.com/photo.jpg',
|
|
931
|
-
* headers: { Authorization: 'Bearer token' }
|
|
932
|
-
* });
|
|
933
|
-
*
|
|
934
|
-
* if (result.success) {
|
|
935
|
-
* console.log('Base64:', result.base64);
|
|
936
|
-
* console.log('Data URI:', result.dataUri);
|
|
937
|
-
* console.log('MIME Type:', result.mimeType);
|
|
938
|
-
* }
|
|
939
|
-
* ```
|
|
940
|
-
*/
|
|
941
525
|
export async function urlToBase64(
|
|
942
|
-
|
|
526
|
+
opts: UrlToBase64Options
|
|
943
527
|
): Promise<UrlToBase64Result> {
|
|
944
528
|
try {
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
});
|
|
949
|
-
|
|
950
|
-
return result as UrlToBase64Result;
|
|
951
|
-
} catch (error: any) {
|
|
952
|
-
return { success: false, error: error?.message || 'UNKNOWN_ERROR' };
|
|
529
|
+
return await FileToolkitModule.urlToBase64(opts);
|
|
530
|
+
} catch (err: any) {
|
|
531
|
+
return { success: false, error: err.message || 'UNKNOWN_ERROR' };
|
|
953
532
|
}
|
|
954
533
|
}
|
|
955
534
|
|
|
956
|
-
// ─── shareFile ────────────────────────────────────────────────────────────────
|
|
957
|
-
|
|
958
|
-
/**
|
|
959
|
-
* Share a file with other apps using the native share dialog.
|
|
960
|
-
* Opens the system share sheet on iOS and share chooser on Android.
|
|
961
|
-
*
|
|
962
|
-
* @example
|
|
963
|
-
* ```typescript
|
|
964
|
-
* const result = await shareFile({
|
|
965
|
-
* filePath: '/path/to/document.pdf',
|
|
966
|
-
* title: 'Share Document',
|
|
967
|
-
* subject: 'Check out this file'
|
|
968
|
-
* });
|
|
969
|
-
*
|
|
970
|
-
* if (result.success) {
|
|
971
|
-
* console.log('File shared successfully');
|
|
972
|
-
* }
|
|
973
|
-
* ```
|
|
974
|
-
*/
|
|
975
535
|
export async function shareFile(
|
|
976
|
-
|
|
536
|
+
opts: ShareFileOptions
|
|
977
537
|
): Promise<ShareFileResult> {
|
|
978
538
|
try {
|
|
979
|
-
|
|
980
|
-
title:
|
|
981
|
-
subject:
|
|
539
|
+
return await FileToolkitModule.shareFile(opts.filePath, {
|
|
540
|
+
title: opts.title,
|
|
541
|
+
subject: opts.subject,
|
|
982
542
|
});
|
|
983
|
-
|
|
984
|
-
return
|
|
985
|
-
} catch (error: any) {
|
|
986
|
-
return { success: false, error: error?.message || 'UNKNOWN_ERROR' };
|
|
543
|
+
} catch (err: any) {
|
|
544
|
+
return { success: false, error: err.message || 'UNKNOWN_ERROR' };
|
|
987
545
|
}
|
|
988
546
|
}
|
|
989
547
|
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
/**
|
|
993
|
-
* Open a file with the default app or app chooser.
|
|
994
|
-
* On iOS, displays a preview or shows apps that can handle the file.
|
|
995
|
-
* On Android, opens with the default app or shows app chooser.
|
|
996
|
-
*
|
|
997
|
-
* @example
|
|
998
|
-
* ```typescript
|
|
999
|
-
* const result = await openFile({
|
|
1000
|
-
* filePath: '/path/to/document.pdf',
|
|
1001
|
-
* mimeType: 'application/pdf' // optional, auto-detected if not provided
|
|
1002
|
-
* });
|
|
1003
|
-
*
|
|
1004
|
-
* if (result.success) {
|
|
1005
|
-
* console.log('File opened successfully');
|
|
1006
|
-
* }
|
|
1007
|
-
* ```
|
|
1008
|
-
*/
|
|
1009
|
-
export async function openFile(
|
|
1010
|
-
options: OpenFileOptions
|
|
1011
|
-
): Promise<OpenFileResult> {
|
|
548
|
+
export async function openFile(opts: OpenFileOptions): Promise<OpenFileResult> {
|
|
1012
549
|
try {
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
);
|
|
1017
|
-
|
|
1018
|
-
return result as OpenFileResult;
|
|
1019
|
-
} catch (error: any) {
|
|
1020
|
-
return { success: false, error: error?.message || 'UNKNOWN_ERROR' };
|
|
550
|
+
return await FileToolkitModule.openFile(opts.filePath, opts.mimeType || '');
|
|
551
|
+
} catch (err: any) {
|
|
552
|
+
return { success: false, error: err.message || 'UNKNOWN_ERROR' };
|
|
1021
553
|
}
|
|
1022
554
|
}
|
|
1023
555
|
|
|
1024
|
-
// ─── Zip / Unzip ─────────────────────────────────────────────────────────────
|
|
1025
|
-
|
|
1026
556
|
export interface UnzipResult {
|
|
1027
557
|
success: boolean;
|
|
1028
|
-
/** Absolute path of the destination directory */
|
|
1029
558
|
destDir?: string;
|
|
1030
|
-
/** List of absolute paths of all extracted files */
|
|
1031
559
|
files?: string[];
|
|
1032
|
-
/** Error message if success is false */
|
|
1033
560
|
error?: string;
|
|
1034
561
|
}
|
|
1035
562
|
|
|
1036
563
|
export interface ZipResult {
|
|
1037
564
|
success: boolean;
|
|
1038
|
-
/** Absolute path of the created zip archive */
|
|
1039
565
|
zipPath?: string;
|
|
1040
|
-
/** Error message if success is false */
|
|
1041
566
|
error?: string;
|
|
1042
567
|
}
|
|
1043
568
|
|
|
1044
|
-
|
|
1045
|
-
* Extract a ZIP archive to a destination directory.
|
|
1046
|
-
*
|
|
1047
|
-
* Uses `java.util.zip` on Android and zlib (system framework) on iOS.
|
|
1048
|
-
* No third-party dependency required.
|
|
1049
|
-
*
|
|
1050
|
-
* @param sourcePath Absolute path to the `.zip` file
|
|
1051
|
-
* @param destDir Absolute path to the directory where files will be extracted.
|
|
1052
|
-
* The directory is created automatically if it does not exist.
|
|
1053
|
-
*
|
|
1054
|
-
* @example
|
|
1055
|
-
* ```ts
|
|
1056
|
-
* const result = await unzip(
|
|
1057
|
-
* '/path/to/archive.zip',
|
|
1058
|
-
* '/path/to/output-folder'
|
|
1059
|
-
* );
|
|
1060
|
-
*
|
|
1061
|
-
* if (result.success) {
|
|
1062
|
-
* console.log('Extracted files:', result.files);
|
|
1063
|
-
* }
|
|
1064
|
-
* ```
|
|
1065
|
-
*/
|
|
1066
|
-
export async function unzip(
|
|
1067
|
-
sourcePath: string,
|
|
1068
|
-
destDir: string
|
|
1069
|
-
): Promise<UnzipResult> {
|
|
569
|
+
export async function unzip(s: string, d: string): Promise<UnzipResult> {
|
|
1070
570
|
try {
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
return { success: false, error: error?.message || 'UNZIP_ERROR' };
|
|
571
|
+
return await (FileToolkitModule as any).unzip(s, d);
|
|
572
|
+
} catch {
|
|
573
|
+
return { success: false, error: 'UNZIP_ERROR' };
|
|
1075
574
|
}
|
|
1076
575
|
}
|
|
1077
576
|
|
|
1078
|
-
|
|
1079
|
-
* Create a ZIP archive from a file or directory.
|
|
1080
|
-
*
|
|
1081
|
-
* Uses `java.util.zip` on Android and zlib (system framework) on iOS.
|
|
1082
|
-
* No third-party dependency required.
|
|
1083
|
-
*
|
|
1084
|
-
* @param sourcePath Absolute path to the file or directory to compress
|
|
1085
|
-
* @param destPath Absolute path for the output `.zip` file.
|
|
1086
|
-
* Parent directory is created automatically if needed.
|
|
1087
|
-
*
|
|
1088
|
-
* @example
|
|
1089
|
-
* ```ts
|
|
1090
|
-
* // Zip a single file
|
|
1091
|
-
* const result = await zip(
|
|
1092
|
-
* '/path/to/document.pdf',
|
|
1093
|
-
* '/path/to/document.zip'
|
|
1094
|
-
* );
|
|
1095
|
-
*
|
|
1096
|
-
* // Zip an entire directory
|
|
1097
|
-
* const result = await zip(
|
|
1098
|
-
* '/path/to/my-folder',
|
|
1099
|
-
* '/path/to/my-folder.zip'
|
|
1100
|
-
* );
|
|
1101
|
-
*
|
|
1102
|
-
* if (result.success) {
|
|
1103
|
-
* console.log('Archive created at:', result.zipPath);
|
|
1104
|
-
* }
|
|
1105
|
-
* ```
|
|
1106
|
-
*/
|
|
1107
|
-
export async function zip(
|
|
1108
|
-
sourcePath: string,
|
|
1109
|
-
destPath: string
|
|
1110
|
-
): Promise<ZipResult> {
|
|
577
|
+
export async function zip(s: string, d: string): Promise<ZipResult> {
|
|
1111
578
|
try {
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
return { success: false, error: error?.message || 'ZIP_ERROR' };
|
|
579
|
+
return await (FileToolkitModule as any).zip(s, d);
|
|
580
|
+
} catch {
|
|
581
|
+
return { success: false, error: 'ZIP_ERROR' };
|
|
1116
582
|
}
|
|
1117
583
|
}
|
|
1118
584
|
|
|
1119
|
-
// ─── useDownload hook ─────────────────────────────────────────────────────────
|
|
1120
|
-
|
|
1121
|
-
export type DownloadStatus =
|
|
1122
|
-
| 'idle'
|
|
1123
|
-
| 'downloading'
|
|
1124
|
-
| 'paused'
|
|
1125
|
-
| 'done'
|
|
1126
|
-
| 'error';
|
|
1127
|
-
|
|
1128
585
|
export interface UseDownloadReturn {
|
|
1129
|
-
/** Start a download. Resolves with the final result. */
|
|
1130
586
|
start: (options: DownloadOptions) => Promise<DownloadResult>;
|
|
1131
|
-
/** Pause the current download */
|
|
1132
587
|
pause: () => Promise<void>;
|
|
1133
|
-
/** Resume the current download */
|
|
1134
588
|
resume: () => Promise<void>;
|
|
1135
|
-
/** Cancel the current download */
|
|
1136
589
|
cancel: () => Promise<void>;
|
|
1137
|
-
|
|
1138
|
-
status: DownloadStatus;
|
|
1139
|
-
/** Rich progress information (null until first progress event) */
|
|
590
|
+
status: 'idle' | 'downloading' | 'paused' | 'done' | 'error';
|
|
1140
591
|
progress: ProgressInfo | null;
|
|
1141
|
-
/** Final result once download completes or fails */
|
|
1142
592
|
result: DownloadResult | null;
|
|
1143
|
-
/** The active download ID (available after download starts) */
|
|
1144
593
|
downloadId: string | null;
|
|
1145
594
|
}
|
|
1146
595
|
|
|
1147
|
-
/**
|
|
1148
|
-
* React hook for managing a single download with built-in state.
|
|
1149
|
-
*
|
|
1150
|
-
* Tracks status, rich progress (percent, speed, ETA), and the final result.
|
|
1151
|
-
* Exposes `pause`, `resume`, and `cancel` controls tied to the active download.
|
|
1152
|
-
*
|
|
1153
|
-
* @example
|
|
1154
|
-
* ```tsx
|
|
1155
|
-
* function DownloadButton() {
|
|
1156
|
-
* const { start, pause, resume, cancel, status, progress, result } = useDownload();
|
|
1157
|
-
*
|
|
1158
|
-
* return (
|
|
1159
|
-
* <View>
|
|
1160
|
-
* <Button
|
|
1161
|
-
* title="Download"
|
|
1162
|
-
* onPress={() => start({ url: 'https://example.com/file.zip' })}
|
|
1163
|
-
* />
|
|
1164
|
-
*
|
|
1165
|
-
* {status === 'downloading' && progress && (
|
|
1166
|
-
* <View>
|
|
1167
|
-
* <Text>{progress.percent.toFixed(1)}%</Text>
|
|
1168
|
-
* <Text>Speed: {(progress.speedBps / 1024).toFixed(1)} KB/s</Text>
|
|
1169
|
-
* <Text>ETA: {progress.etaSeconds.toFixed(0)}s</Text>
|
|
1170
|
-
* <ProgressBar value={progress.percent / 100} />
|
|
1171
|
-
* <Button title="Pause" onPress={pause} />
|
|
1172
|
-
* </View>
|
|
1173
|
-
* )}
|
|
1174
|
-
*
|
|
1175
|
-
* {status === 'paused' && (
|
|
1176
|
-
* <Button title="Resume" onPress={resume} />
|
|
1177
|
-
* )}
|
|
1178
|
-
*
|
|
1179
|
-
* {status === 'done' && result?.success && (
|
|
1180
|
-
* <Text>Saved to: {result.filePath}</Text>
|
|
1181
|
-
* )}
|
|
1182
|
-
*
|
|
1183
|
-
* {status === 'error' && (
|
|
1184
|
-
* <Text>Error: {result?.error}</Text>
|
|
1185
|
-
* )}
|
|
1186
|
-
* </View>
|
|
1187
|
-
* );
|
|
1188
|
-
* }
|
|
1189
|
-
* ```
|
|
1190
|
-
*/
|
|
1191
596
|
export function useDownload(): UseDownloadReturn {
|
|
1192
|
-
const [status, setStatus] = useState<
|
|
597
|
+
const [status, setStatus] = useState<any>('idle');
|
|
1193
598
|
const [progress, setProgress] = useState<ProgressInfo | null>(null);
|
|
1194
599
|
const [result, setResult] = useState<DownloadResult | null>(null);
|
|
1195
600
|
const [downloadId, setDownloadId] = useState<string | null>(null);
|
|
1196
601
|
const downloadIdRef = useRef<string | null>(null);
|
|
1197
602
|
|
|
1198
|
-
const start = useCallback(
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
603
|
+
const start = useCallback(async (opts: DownloadOptions) => {
|
|
604
|
+
const id = opts.downloadId || _generateId();
|
|
605
|
+
setStatus('downloading');
|
|
606
|
+
setProgress(null);
|
|
607
|
+
setResult(null);
|
|
608
|
+
setDownloadId(id);
|
|
609
|
+
downloadIdRef.current = id;
|
|
610
|
+
const res = await download({
|
|
611
|
+
...opts,
|
|
612
|
+
downloadId: id,
|
|
613
|
+
onProgress: (p) => {
|
|
614
|
+
setProgress(p);
|
|
615
|
+
opts.onProgress?.(p);
|
|
616
|
+
},
|
|
617
|
+
});
|
|
618
|
+
setResult(res);
|
|
619
|
+
setStatus(
|
|
620
|
+
!!opts.background && !!res.success && !!res.downloadId && !res.filePath
|
|
621
|
+
? 'downloading'
|
|
622
|
+
: res.success
|
|
623
|
+
? 'done'
|
|
624
|
+
: 'error'
|
|
625
|
+
);
|
|
626
|
+
return res;
|
|
627
|
+
}, []);
|
|
1218
628
|
|
|
1219
|
-
|
|
1220
|
-
const isBackgroundPending =
|
|
1221
|
-
!!options.background &&
|
|
1222
|
-
!!res.success &&
|
|
1223
|
-
!!res.downloadId &&
|
|
1224
|
-
!res.filePath;
|
|
1225
|
-
setStatus(
|
|
1226
|
-
isBackgroundPending ? 'downloading' : res.success ? 'done' : 'error'
|
|
1227
|
-
);
|
|
1228
|
-
return res;
|
|
1229
|
-
},
|
|
1230
|
-
[]
|
|
1231
|
-
);
|
|
1232
|
-
|
|
1233
|
-
const pause = useCallback(async (): Promise<void> => {
|
|
629
|
+
const pause = useCallback(async () => {
|
|
1234
630
|
if (downloadIdRef.current) {
|
|
1235
631
|
await pauseDownload(downloadIdRef.current);
|
|
1236
632
|
setStatus('paused');
|
|
1237
633
|
}
|
|
1238
634
|
}, []);
|
|
1239
|
-
|
|
1240
|
-
const resume = useCallback(async (): Promise<void> => {
|
|
635
|
+
const resume = useCallback(async () => {
|
|
1241
636
|
if (downloadIdRef.current) {
|
|
1242
637
|
await resumeDownload(downloadIdRef.current);
|
|
1243
638
|
setStatus('downloading');
|
|
1244
639
|
}
|
|
1245
640
|
}, []);
|
|
1246
|
-
|
|
1247
|
-
const cancel = useCallback(async (): Promise<void> => {
|
|
641
|
+
const cancel = useCallback(async () => {
|
|
1248
642
|
if (downloadIdRef.current) {
|
|
1249
643
|
await cancelDownload(downloadIdRef.current);
|
|
1250
644
|
setStatus('idle');
|