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/lib/module/index.js
CHANGED
|
@@ -5,16 +5,6 @@ import { NativeEventEmitter, NativeModules } from 'react-native';
|
|
|
5
5
|
import FileToolkitSpec from "./NativeFileToolkit.js";
|
|
6
6
|
const FileToolkitModule = NativeModules.FileToolkit || FileToolkitSpec;
|
|
7
7
|
const eventEmitter = new NativeEventEmitter(FileToolkitModule);
|
|
8
|
-
|
|
9
|
-
// ─── Types ────────────────────────────────────────────────────────────────────
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Rich progress information emitted during a download.
|
|
13
|
-
* Replaces the plain `number` percent from earlier versions.
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
// ─── Queue ───────────────────────────────────────────────────────────────────
|
|
17
|
-
|
|
18
8
|
class DownloadQueue {
|
|
19
9
|
_maxConcurrent = 3;
|
|
20
10
|
_active = 0;
|
|
@@ -22,7 +12,6 @@ class DownloadQueue {
|
|
|
22
12
|
setOptions(opts) {
|
|
23
13
|
if (opts.maxConcurrent != null && opts.maxConcurrent > 0) {
|
|
24
14
|
this._maxConcurrent = opts.maxConcurrent;
|
|
25
|
-
// Kick off any slots that just opened.
|
|
26
15
|
this._flush();
|
|
27
16
|
}
|
|
28
17
|
}
|
|
@@ -52,7 +41,6 @@ class DownloadQueue {
|
|
|
52
41
|
while (this._active < this._maxConcurrent && this._queue.length > 0) {
|
|
53
42
|
const item = this._queue.shift();
|
|
54
43
|
this._active++;
|
|
55
|
-
// Strip queue-specific fields before passing to the native layer.
|
|
56
44
|
const nativeOptions = {
|
|
57
45
|
...item.options
|
|
58
46
|
};
|
|
@@ -70,55 +58,31 @@ class DownloadQueue {
|
|
|
70
58
|
}
|
|
71
59
|
}
|
|
72
60
|
const _globalQueue = new DownloadQueue();
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
* ```
|
|
81
|
-
*/
|
|
61
|
+
function _generateId() {
|
|
62
|
+
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => {
|
|
63
|
+
const r = Math.random() * 16 | 0;
|
|
64
|
+
const v = c === 'x' ? r : r & 0x3 | 0x8;
|
|
65
|
+
return v.toString(16);
|
|
66
|
+
});
|
|
67
|
+
}
|
|
82
68
|
export function setQueueOptions(options) {
|
|
83
69
|
_globalQueue.setOptions(options);
|
|
84
70
|
}
|
|
85
|
-
|
|
86
|
-
/**
|
|
87
|
-
* Get the current state of the managed download queue.
|
|
88
|
-
*
|
|
89
|
-
* @example
|
|
90
|
-
* ```ts
|
|
91
|
-
* const { active, pending, maxConcurrent } = getQueueStatus();
|
|
92
|
-
* ```
|
|
93
|
-
*/
|
|
94
71
|
export function getQueueStatus() {
|
|
95
72
|
return _globalQueue.getStatus();
|
|
96
73
|
}
|
|
97
|
-
|
|
98
|
-
// ─── Core download ────────────────────────────────────────────────────────────
|
|
99
|
-
|
|
100
|
-
/**
|
|
101
|
-
* Core native download executor — used internally by `download()` and the queue.
|
|
102
|
-
* Never routes through the queue.
|
|
103
|
-
*/
|
|
104
74
|
async function _executeDownload(options) {
|
|
105
75
|
let progressSubscription = null;
|
|
106
76
|
let retrySubscription = null;
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
// ID-based event filtering. On iOS it only arrives on completion, so we
|
|
110
|
-
// fall back to URL-based filtering in that case.
|
|
111
|
-
let knownDownloadId = null;
|
|
112
|
-
|
|
113
|
-
// Speed / ETA tracking (JS-side, works regardless of native platform)
|
|
77
|
+
const downloadId = options.downloadId || _generateId();
|
|
78
|
+
const knownDownloadId = downloadId;
|
|
114
79
|
let _lastProgressTs = null;
|
|
115
80
|
let _lastProgressBytes = 0;
|
|
116
|
-
// Smoothed speed using exponential moving average (α = 0.3)
|
|
117
81
|
let _smoothedSpeedBps = 0;
|
|
118
82
|
if (options.onProgress) {
|
|
119
83
|
progressSubscription = eventEmitter.addListener('onDownloadProgress', event => {
|
|
120
|
-
const matchesId =
|
|
121
|
-
const matchesUrl = !
|
|
84
|
+
const matchesId = event.downloadId === knownDownloadId;
|
|
85
|
+
const matchesUrl = !event.downloadId && event.url === options.url;
|
|
122
86
|
if ((matchesId || matchesUrl) && options.onProgress) {
|
|
123
87
|
const now = Date.now();
|
|
124
88
|
const percent = event.progress ?? 0;
|
|
@@ -130,47 +94,30 @@ async function _executeDownload(options) {
|
|
|
130
94
|
const dtSec = (now - _lastProgressTs) / 1000;
|
|
131
95
|
if (dtSec > 0) {
|
|
132
96
|
const bytesDelta = bytesDownloaded - _lastProgressBytes;
|
|
133
|
-
// Only update speed if we have byte-level data from native
|
|
134
97
|
if (bytesDelta > 0 && totalBytes > 0) {
|
|
135
98
|
const instantSpeed = bytesDelta / dtSec;
|
|
136
|
-
// Exponential moving average for smoother readings
|
|
137
99
|
_smoothedSpeedBps = _smoothedSpeedBps === 0 ? instantSpeed : 0.3 * instantSpeed + 0.7 * _smoothedSpeedBps;
|
|
138
100
|
speedBps = _smoothedSpeedBps;
|
|
139
|
-
|
|
140
|
-
etaSeconds = speedBps > 0 ? remaining / speedBps : 0;
|
|
141
|
-
} else if (percent > 0 && percent < 100) {
|
|
142
|
-
// Fallback: estimate from percent when bytes aren't available
|
|
143
|
-
const estimatedTotalBytes = totalBytes > 0 ? totalBytes : _lastProgressBytes / (percent / 100);
|
|
144
|
-
const percentDelta = percent - _lastProgressBytes / estimatedTotalBytes * 100;
|
|
145
|
-
if (percentDelta > 0) {
|
|
146
|
-
const estimatedByteDelta = percentDelta / 100 * estimatedTotalBytes;
|
|
147
|
-
const instantSpeed = estimatedByteDelta / dtSec;
|
|
148
|
-
_smoothedSpeedBps = _smoothedSpeedBps === 0 ? instantSpeed : 0.3 * instantSpeed + 0.7 * _smoothedSpeedBps;
|
|
149
|
-
speedBps = _smoothedSpeedBps;
|
|
150
|
-
const remainingPct = 100 - percent;
|
|
151
|
-
const estimatedRemaining = remainingPct / 100 * estimatedTotalBytes;
|
|
152
|
-
etaSeconds = speedBps > 0 ? estimatedRemaining / speedBps : 0;
|
|
153
|
-
}
|
|
101
|
+
etaSeconds = speedBps > 0 ? (totalBytes - bytesDownloaded) / speedBps : 0;
|
|
154
102
|
}
|
|
155
103
|
}
|
|
156
104
|
}
|
|
157
105
|
_lastProgressTs = now;
|
|
158
106
|
_lastProgressBytes = bytesDownloaded;
|
|
159
|
-
|
|
107
|
+
options.onProgress({
|
|
160
108
|
percent,
|
|
161
109
|
bytesDownloaded,
|
|
162
110
|
totalBytes,
|
|
163
111
|
speedBps,
|
|
164
112
|
etaSeconds
|
|
165
|
-
};
|
|
166
|
-
options.onProgress(info);
|
|
113
|
+
});
|
|
167
114
|
}
|
|
168
115
|
});
|
|
169
116
|
}
|
|
170
117
|
if (options.retry?.onRetry) {
|
|
171
118
|
retrySubscription = eventEmitter.addListener('onDownloadRetry', event => {
|
|
172
|
-
const matchesId =
|
|
173
|
-
const matchesUrl = !
|
|
119
|
+
const matchesId = event.downloadId === knownDownloadId;
|
|
120
|
+
const matchesUrl = !event.downloadId && event.url === options.url;
|
|
174
121
|
if ((matchesId || matchesUrl) && options.retry?.onRetry) {
|
|
175
122
|
options.retry.onRetry(event.attempt, event.error ?? '');
|
|
176
123
|
}
|
|
@@ -181,32 +128,13 @@ async function _executeDownload(options) {
|
|
|
181
128
|
retrySubscription?.remove();
|
|
182
129
|
};
|
|
183
130
|
try {
|
|
184
|
-
const
|
|
185
|
-
|
|
186
|
-
|
|
131
|
+
const result = await FileToolkitModule.download({
|
|
132
|
+
...options,
|
|
133
|
+
downloadId,
|
|
187
134
|
background: options.background ?? false,
|
|
188
135
|
headers: options.headers ?? {},
|
|
189
|
-
destination: options.destination ?? 'downloads'
|
|
190
|
-
notificationTitle: options.notificationTitle,
|
|
191
|
-
notificationDescription: options.notificationDescription,
|
|
192
|
-
checksum: options.checksum,
|
|
193
|
-
retry: options.retry ? {
|
|
194
|
-
attempts: options.retry.attempts,
|
|
195
|
-
delay: options.retry.delay ?? 1000
|
|
196
|
-
} : undefined
|
|
136
|
+
destination: options.destination ?? 'downloads'
|
|
197
137
|
});
|
|
198
|
-
|
|
199
|
-
// On Android foreground the promise resolves immediately with downloadId,
|
|
200
|
-
// giving us a precise ID to filter events by before download completes.
|
|
201
|
-
// On iOS it only resolves on completion, so knownDownloadId stays null
|
|
202
|
-
// and URL-based filtering is used as the fallback.
|
|
203
|
-
resultPromise.then?.(partial => {
|
|
204
|
-
if (partial?.downloadId && !partial?.filePath) {
|
|
205
|
-
// Android early-resolve: has downloadId but no filePath yet
|
|
206
|
-
knownDownloadId = partial.downloadId;
|
|
207
|
-
}
|
|
208
|
-
});
|
|
209
|
-
const result = await resultPromise;
|
|
210
138
|
cleanup();
|
|
211
139
|
return result;
|
|
212
140
|
} catch (error) {
|
|
@@ -217,256 +145,149 @@ async function _executeDownload(options) {
|
|
|
217
145
|
};
|
|
218
146
|
}
|
|
219
147
|
}
|
|
220
|
-
|
|
221
|
-
/**
|
|
222
|
-
* Download a file.
|
|
223
|
-
*
|
|
224
|
-
* For **foreground** downloads the promise resolves when the file is saved.
|
|
225
|
-
* For **background** downloads the promise resolves immediately with a
|
|
226
|
-
* `downloadId`; listen to the `onDownloadComplete` / `onDownloadError`
|
|
227
|
-
* events for the final result.
|
|
228
|
-
*
|
|
229
|
-
* Pass `queue: true` to join the managed queue and respect the `maxConcurrent`
|
|
230
|
-
* limit configured via `setQueueOptions()`. Use `priority: 'high'` to jump
|
|
231
|
-
* ahead of other pending items in the queue.
|
|
232
|
-
*
|
|
233
|
-
* @example
|
|
234
|
-
* ```ts
|
|
235
|
-
* // Direct (unqueued) download
|
|
236
|
-
* const result = await download({ url: 'https://...' });
|
|
237
|
-
*
|
|
238
|
-
* // Queued download with high priority
|
|
239
|
-
* setQueueOptions({ maxConcurrent: 3 });
|
|
240
|
-
* const result = await download({
|
|
241
|
-
* url: 'https://...',
|
|
242
|
-
* queue: true,
|
|
243
|
-
* priority: 'high',
|
|
244
|
-
* });
|
|
245
|
-
* ```
|
|
246
|
-
*/
|
|
247
148
|
export function download(options) {
|
|
248
|
-
if (options.queue)
|
|
249
|
-
return _globalQueue.enqueue(options);
|
|
250
|
-
}
|
|
149
|
+
if (options.queue) return _globalQueue.enqueue(options);
|
|
251
150
|
return _executeDownload(options);
|
|
252
151
|
}
|
|
253
|
-
|
|
254
|
-
// ─── Core upload ───────────────────────────────────────────────────────────────
|
|
255
|
-
|
|
256
|
-
/**
|
|
257
|
-
* Upload a file using multipart/form-data.
|
|
258
|
-
*/
|
|
259
152
|
export async function upload(options) {
|
|
260
|
-
let
|
|
153
|
+
let sub = null;
|
|
154
|
+
const uploadId = options.uploadId || _generateId();
|
|
261
155
|
if (options.onProgress) {
|
|
262
|
-
|
|
263
|
-
if (
|
|
264
|
-
options.onProgress(event.progress);
|
|
265
|
-
}
|
|
156
|
+
sub = eventEmitter.addListener('onUploadProgress', e => {
|
|
157
|
+
if (e.uploadId === uploadId) options.onProgress(e.progress);
|
|
266
158
|
});
|
|
267
159
|
}
|
|
268
160
|
try {
|
|
269
|
-
const
|
|
270
|
-
|
|
271
|
-
|
|
161
|
+
const res = await FileToolkitModule.upload({
|
|
162
|
+
...options,
|
|
163
|
+
uploadId,
|
|
272
164
|
fieldName: options.fieldName ?? 'file',
|
|
273
165
|
headers: options.headers ?? {},
|
|
274
166
|
parameters: options.parameters ?? {}
|
|
275
167
|
});
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
}
|
|
168
|
+
sub?.remove();
|
|
169
|
+
return {
|
|
170
|
+
...res,
|
|
171
|
+
uploadId
|
|
172
|
+
};
|
|
173
|
+
} catch (err) {
|
|
174
|
+
sub?.remove();
|
|
284
175
|
return {
|
|
285
176
|
success: false,
|
|
286
|
-
error:
|
|
177
|
+
error: err?.message || 'UNKNOWN_ERROR',
|
|
178
|
+
uploadId
|
|
287
179
|
};
|
|
288
180
|
}
|
|
289
181
|
}
|
|
290
|
-
|
|
291
|
-
// ─── Pause / Resume / Cancel ──────────────────────────────────────────────────
|
|
292
|
-
|
|
293
|
-
/**
|
|
294
|
-
* Pause an active foreground download.
|
|
295
|
-
* On iOS the partial data is stored so it can be resumed.
|
|
296
|
-
* On Android the download thread is suspended.
|
|
297
|
-
*/
|
|
298
|
-
export async function pauseDownload(downloadId) {
|
|
182
|
+
export async function pauseDownload(id) {
|
|
299
183
|
try {
|
|
300
|
-
return await
|
|
301
|
-
} catch (
|
|
184
|
+
return await FileToolkitModule.pauseDownload(id);
|
|
185
|
+
} catch (err) {
|
|
302
186
|
return {
|
|
303
187
|
success: false,
|
|
304
|
-
error:
|
|
188
|
+
error: err.message || 'UNKNOWN_ERROR'
|
|
305
189
|
};
|
|
306
190
|
}
|
|
307
191
|
}
|
|
308
|
-
|
|
309
|
-
/**
|
|
310
|
-
* Resume a previously paused download.
|
|
311
|
-
* On iOS resumes from partial data (HTTP Range). On Android unblocks the thread.
|
|
312
|
-
*/
|
|
313
|
-
export async function resumeDownload(downloadId) {
|
|
192
|
+
export async function resumeDownload(id) {
|
|
314
193
|
try {
|
|
315
|
-
return await
|
|
316
|
-
} catch (
|
|
194
|
+
return await FileToolkitModule.resumeDownload(id);
|
|
195
|
+
} catch (err) {
|
|
317
196
|
return {
|
|
318
197
|
success: false,
|
|
319
|
-
error:
|
|
198
|
+
error: err.message || 'UNKNOWN_ERROR'
|
|
320
199
|
};
|
|
321
200
|
}
|
|
322
201
|
}
|
|
323
|
-
|
|
324
|
-
/**
|
|
325
|
-
* Cancel and discard a download (foreground or background).
|
|
326
|
-
* Any partially-downloaded file is deleted.
|
|
327
|
-
*/
|
|
328
|
-
export async function cancelDownload(downloadId) {
|
|
202
|
+
export async function cancelDownload(id) {
|
|
329
203
|
try {
|
|
330
|
-
return await
|
|
331
|
-
} catch (
|
|
204
|
+
return await FileToolkitModule.cancelDownload(id);
|
|
205
|
+
} catch (err) {
|
|
332
206
|
return {
|
|
333
207
|
success: false,
|
|
334
|
-
error:
|
|
208
|
+
error: err.message || 'UNKNOWN_ERROR'
|
|
335
209
|
};
|
|
336
210
|
}
|
|
337
211
|
}
|
|
338
|
-
|
|
339
|
-
// ─── Cache management ─────────────────────────────────────────────────────────
|
|
340
|
-
|
|
341
|
-
/**
|
|
342
|
-
* List all files in the app's cache directory.
|
|
343
|
-
*/
|
|
344
212
|
export async function getCachedFiles() {
|
|
345
213
|
try {
|
|
346
|
-
return await
|
|
347
|
-
} catch (
|
|
214
|
+
return await FileToolkitModule.getCachedFiles();
|
|
215
|
+
} catch (err) {
|
|
348
216
|
return {
|
|
349
217
|
success: false,
|
|
350
|
-
error:
|
|
218
|
+
error: err.message || 'UNKNOWN_ERROR'
|
|
351
219
|
};
|
|
352
220
|
}
|
|
353
221
|
}
|
|
354
|
-
|
|
355
|
-
/**
|
|
356
|
-
* Delete a single file by its absolute path.
|
|
357
|
-
*/
|
|
358
|
-
export async function deleteFile(filePath) {
|
|
222
|
+
export async function deleteFile(path) {
|
|
359
223
|
try {
|
|
360
|
-
return await
|
|
361
|
-
} catch (
|
|
224
|
+
return await FileToolkitModule.deleteFile(path);
|
|
225
|
+
} catch (err) {
|
|
362
226
|
return {
|
|
363
227
|
success: false,
|
|
364
|
-
error:
|
|
228
|
+
error: err.message || 'UNKNOWN_ERROR'
|
|
365
229
|
};
|
|
366
230
|
}
|
|
367
231
|
}
|
|
368
|
-
|
|
369
|
-
/**
|
|
370
|
-
* Delete all files in the app's cache directory.
|
|
371
|
-
*/
|
|
372
232
|
export async function clearCache() {
|
|
373
233
|
try {
|
|
374
|
-
return await
|
|
375
|
-
} catch (
|
|
234
|
+
return await FileToolkitModule.clearCache();
|
|
235
|
+
} catch (err) {
|
|
376
236
|
return {
|
|
377
237
|
success: false,
|
|
378
|
-
error:
|
|
238
|
+
error: err.message || 'UNKNOWN_ERROR'
|
|
379
239
|
};
|
|
380
240
|
}
|
|
381
241
|
}
|
|
382
|
-
|
|
383
|
-
/**
|
|
384
|
-
* Get all active background downloads.
|
|
385
|
-
* Use this after app restart to "re-attach" to ongoing downloads.
|
|
386
|
-
*/
|
|
387
242
|
export async function getBackgroundDownloads() {
|
|
388
243
|
try {
|
|
389
|
-
return await
|
|
390
|
-
} catch (
|
|
244
|
+
return await FileToolkitModule.getBackgroundDownloads();
|
|
245
|
+
} catch (err) {
|
|
391
246
|
return {
|
|
392
247
|
success: false,
|
|
393
|
-
error:
|
|
248
|
+
error: err.message || 'UNKNOWN_ERROR'
|
|
394
249
|
};
|
|
395
250
|
}
|
|
396
251
|
}
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
function _ensureFsSuccess(result, fallback) {
|
|
401
|
-
if (!result?.success) {
|
|
402
|
-
throw new Error(result?.error || fallback);
|
|
403
|
-
}
|
|
252
|
+
function _ensure(res, msg) {
|
|
253
|
+
if (!res?.success) throw new Error(res?.error || msg);
|
|
404
254
|
}
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
_ensureFsSuccess(result, 'EXISTS_ERROR');
|
|
410
|
-
return !!result.exists;
|
|
255
|
+
export async function exists(path) {
|
|
256
|
+
const res = await FileToolkitModule.exists(path);
|
|
257
|
+
_ensure(res, 'EXISTS_ERROR');
|
|
258
|
+
return !!res.exists;
|
|
411
259
|
}
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
_ensureFsSuccess(result, 'STAT_ERROR');
|
|
417
|
-
return result.stat;
|
|
260
|
+
export async function stat(path) {
|
|
261
|
+
const res = await FileToolkitModule.stat(path);
|
|
262
|
+
_ensure(res, 'STAT_ERROR');
|
|
263
|
+
return res.stat;
|
|
418
264
|
}
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
_ensureFsSuccess(result, 'READ_FILE_ERROR');
|
|
424
|
-
return result.data ?? '';
|
|
265
|
+
export async function readFile(path, enc = 'utf8') {
|
|
266
|
+
const res = await FileToolkitModule.readFile(path, enc);
|
|
267
|
+
_ensure(res, 'READ_ERROR');
|
|
268
|
+
return res.data ?? '';
|
|
425
269
|
}
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
const result = await FileToolkitSpec.writeFile(filePath, data, encoding);
|
|
430
|
-
_ensureFsSuccess(result, 'WRITE_FILE_ERROR');
|
|
270
|
+
export async function writeFile(path, data, enc = 'utf8') {
|
|
271
|
+
const res = await FileToolkitModule.writeFile(path, data, enc);
|
|
272
|
+
_ensure(res, 'WRITE_ERROR');
|
|
431
273
|
}
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
const result = await FileToolkitSpec.copyFile(fromPath, toPath);
|
|
436
|
-
_ensureFsSuccess(result, 'COPY_FILE_ERROR');
|
|
274
|
+
export async function copyFile(from, to) {
|
|
275
|
+
const res = await FileToolkitModule.copyFile(from, to);
|
|
276
|
+
_ensure(res, 'COPY_ERROR');
|
|
437
277
|
}
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
const result = await FileToolkitSpec.moveFile(fromPath, toPath);
|
|
442
|
-
_ensureFsSuccess(result, 'MOVE_FILE_ERROR');
|
|
278
|
+
export async function moveFile(from, to) {
|
|
279
|
+
const res = await FileToolkitModule.moveFile(from, to);
|
|
280
|
+
_ensure(res, 'MOVE_ERROR');
|
|
443
281
|
}
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
const result = await FileToolkitSpec.mkdir(dirPath);
|
|
448
|
-
_ensureFsSuccess(result, 'MKDIR_ERROR');
|
|
282
|
+
export async function mkdir(path) {
|
|
283
|
+
const res = await FileToolkitModule.mkdir(path);
|
|
284
|
+
_ensure(res, 'MKDIR_ERROR');
|
|
449
285
|
}
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
_ensureFsSuccess(result, 'LS_ERROR');
|
|
455
|
-
return result.entries || [];
|
|
286
|
+
export async function ls(path) {
|
|
287
|
+
const res = await FileToolkitModule.ls(path);
|
|
288
|
+
_ensure(res, 'LS_ERROR');
|
|
289
|
+
return res.entries || [];
|
|
456
290
|
}
|
|
457
|
-
|
|
458
|
-
/**
|
|
459
|
-
* File system API namespace.
|
|
460
|
-
*
|
|
461
|
-
* @example
|
|
462
|
-
* ```ts
|
|
463
|
-
* import { fs } from 'rn-file-toolkit';
|
|
464
|
-
*
|
|
465
|
-
* const ok = await fs.exists('/path/to/file.pdf');
|
|
466
|
-
* const meta = await fs.stat('/path/to/file.pdf');
|
|
467
|
-
* const text = await fs.readFile('/path/to/file.txt');
|
|
468
|
-
* ```
|
|
469
|
-
*/
|
|
470
291
|
export const fs = {
|
|
471
292
|
exists,
|
|
472
293
|
stat,
|
|
@@ -474,366 +295,130 @@ export const fs = {
|
|
|
474
295
|
writeFile,
|
|
475
296
|
copyFile,
|
|
476
297
|
moveFile,
|
|
477
|
-
deleteFile: async
|
|
478
|
-
|
|
479
|
-
_ensureFsSuccess(result, 'DELETE_FILE_ERROR');
|
|
298
|
+
deleteFile: async p => {
|
|
299
|
+
_ensure(await deleteFile(p), 'DEL_ERROR');
|
|
480
300
|
},
|
|
481
301
|
mkdir,
|
|
482
302
|
ls
|
|
483
303
|
};
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
/**
|
|
488
|
-
* Subscribe to background download completion events.
|
|
489
|
-
* Returns an unsubscribe function.
|
|
490
|
-
*/
|
|
491
|
-
export function onDownloadComplete(callback) {
|
|
492
|
-
const sub = eventEmitter.addListener('onDownloadComplete', callback);
|
|
493
|
-
return () => sub.remove();
|
|
304
|
+
export function onDownloadComplete(cb) {
|
|
305
|
+
const s = eventEmitter.addListener('onDownloadComplete', cb);
|
|
306
|
+
return () => s.remove();
|
|
494
307
|
}
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
* Returns an unsubscribe function.
|
|
499
|
-
*/
|
|
500
|
-
export function onDownloadError(callback) {
|
|
501
|
-
const sub = eventEmitter.addListener('onDownloadError', callback);
|
|
502
|
-
return () => sub.remove();
|
|
308
|
+
export function onDownloadError(cb) {
|
|
309
|
+
const s = eventEmitter.addListener('onDownloadError', cb);
|
|
310
|
+
return () => s.remove();
|
|
503
311
|
}
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
* Returns an unsubscribe function.
|
|
508
|
-
*/
|
|
509
|
-
export function onUploadProgress(callback) {
|
|
510
|
-
const sub = eventEmitter.addListener('onUploadProgress', callback);
|
|
511
|
-
return () => sub.remove();
|
|
312
|
+
export function onUploadProgress(cb) {
|
|
313
|
+
const s = eventEmitter.addListener('onUploadProgress', cb);
|
|
314
|
+
return () => s.remove();
|
|
512
315
|
}
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
* Returns an unsubscribe function.
|
|
517
|
-
*/
|
|
518
|
-
export function onDownloadRetry(callback) {
|
|
519
|
-
const sub = eventEmitter.addListener('onDownloadRetry', callback);
|
|
520
|
-
return () => sub.remove();
|
|
316
|
+
export function onDownloadRetry(cb) {
|
|
317
|
+
const s = eventEmitter.addListener('onDownloadRetry', cb);
|
|
318
|
+
return () => s.remove();
|
|
521
319
|
}
|
|
522
|
-
|
|
523
|
-
// ─── saveBase64AsFile ─────────────────────────────────────────────────────────
|
|
524
|
-
|
|
525
|
-
/**
|
|
526
|
-
* Save a base64 string or data URI as a file.
|
|
527
|
-
* Supports data URIs (e.g., "data:image/png;base64,iVBORw0...") and plain base64 strings.
|
|
528
|
-
*
|
|
529
|
-
* @example
|
|
530
|
-
* ```typescript
|
|
531
|
-
* // From data URI
|
|
532
|
-
* const result = await saveBase64AsFile({
|
|
533
|
-
* base64Data: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUg...',
|
|
534
|
-
* fileName: 'photo.png',
|
|
535
|
-
* destination: 'documents'
|
|
536
|
-
* });
|
|
537
|
-
*
|
|
538
|
-
* // From plain base64
|
|
539
|
-
* const result = await saveBase64AsFile({
|
|
540
|
-
* base64Data: 'SGVsbG8gV29ybGQ=',
|
|
541
|
-
* fileName: 'hello.txt',
|
|
542
|
-
* destination: 'cache'
|
|
543
|
-
* });
|
|
544
|
-
* ```
|
|
545
|
-
*/
|
|
546
|
-
export async function saveBase64AsFile(options) {
|
|
320
|
+
export async function saveBase64AsFile(opts) {
|
|
547
321
|
try {
|
|
548
322
|
let {
|
|
549
323
|
base64Data,
|
|
550
324
|
fileName,
|
|
551
325
|
destination
|
|
552
|
-
} =
|
|
553
|
-
|
|
554
|
-
// Parse data URI if present (e.g., "data:image/png;base64,iVBORw0...")
|
|
326
|
+
} = opts;
|
|
555
327
|
if (base64Data.startsWith('data:')) {
|
|
556
|
-
const
|
|
557
|
-
if (
|
|
558
|
-
base64Data =
|
|
559
|
-
|
|
560
|
-
// Auto-detect file extension from MIME type if fileName not provided
|
|
561
|
-
if (!fileName && matches[1]) {
|
|
562
|
-
const mimeType = matches[1];
|
|
563
|
-
const ext = mimeType.split('/')[1] || 'bin';
|
|
564
|
-
fileName = `file_${Date.now()}.${ext}`;
|
|
565
|
-
}
|
|
566
|
-
} else {
|
|
567
|
-
return {
|
|
568
|
-
success: false,
|
|
569
|
-
error: 'Invalid data URI format. Expected: data:<mimetype>;base64,<data>'
|
|
570
|
-
};
|
|
328
|
+
const m = base64Data.match(/^data:([^;]+);base64,(.+)$/);
|
|
329
|
+
if (m && m[2]) {
|
|
330
|
+
base64Data = m[2];
|
|
331
|
+
if (!fileName && m[1]) fileName = `file_${Date.now()}.${m[1].split('/')[1] || 'bin'}`;
|
|
571
332
|
}
|
|
572
333
|
}
|
|
573
|
-
|
|
334
|
+
return await FileToolkitModule.saveBase64AsFile({
|
|
574
335
|
base64Data,
|
|
575
336
|
fileName,
|
|
576
337
|
destination
|
|
577
338
|
});
|
|
578
|
-
|
|
579
|
-
} catch (error) {
|
|
339
|
+
} catch (err) {
|
|
580
340
|
return {
|
|
581
341
|
success: false,
|
|
582
|
-
error:
|
|
342
|
+
error: err.message || 'UNKNOWN_ERROR'
|
|
583
343
|
};
|
|
584
344
|
}
|
|
585
345
|
}
|
|
586
|
-
|
|
587
|
-
// ─── urlToBase64 ──────────────────────────────────────────────────────────────
|
|
588
|
-
|
|
589
|
-
/**
|
|
590
|
-
* Convert a web URL (image, video, gif, etc.) to base64 string.
|
|
591
|
-
* Downloads the file and returns both the base64 string and data URI format.
|
|
592
|
-
*
|
|
593
|
-
* @example
|
|
594
|
-
* ```typescript
|
|
595
|
-
* // Convert image to base64
|
|
596
|
-
* const result = await urlToBase64({
|
|
597
|
-
* url: 'https://example.com/photo.jpg',
|
|
598
|
-
* headers: { Authorization: 'Bearer token' }
|
|
599
|
-
* });
|
|
600
|
-
*
|
|
601
|
-
* if (result.success) {
|
|
602
|
-
* console.log('Base64:', result.base64);
|
|
603
|
-
* console.log('Data URI:', result.dataUri);
|
|
604
|
-
* console.log('MIME Type:', result.mimeType);
|
|
605
|
-
* }
|
|
606
|
-
* ```
|
|
607
|
-
*/
|
|
608
|
-
export async function urlToBase64(options) {
|
|
346
|
+
export async function urlToBase64(opts) {
|
|
609
347
|
try {
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
headers: options.headers
|
|
613
|
-
});
|
|
614
|
-
return result;
|
|
615
|
-
} catch (error) {
|
|
348
|
+
return await FileToolkitModule.urlToBase64(opts);
|
|
349
|
+
} catch (err) {
|
|
616
350
|
return {
|
|
617
351
|
success: false,
|
|
618
|
-
error:
|
|
352
|
+
error: err.message || 'UNKNOWN_ERROR'
|
|
619
353
|
};
|
|
620
354
|
}
|
|
621
355
|
}
|
|
622
|
-
|
|
623
|
-
// ─── shareFile ────────────────────────────────────────────────────────────────
|
|
624
|
-
|
|
625
|
-
/**
|
|
626
|
-
* Share a file with other apps using the native share dialog.
|
|
627
|
-
* Opens the system share sheet on iOS and share chooser on Android.
|
|
628
|
-
*
|
|
629
|
-
* @example
|
|
630
|
-
* ```typescript
|
|
631
|
-
* const result = await shareFile({
|
|
632
|
-
* filePath: '/path/to/document.pdf',
|
|
633
|
-
* title: 'Share Document',
|
|
634
|
-
* subject: 'Check out this file'
|
|
635
|
-
* });
|
|
636
|
-
*
|
|
637
|
-
* if (result.success) {
|
|
638
|
-
* console.log('File shared successfully');
|
|
639
|
-
* }
|
|
640
|
-
* ```
|
|
641
|
-
*/
|
|
642
|
-
export async function shareFile(options) {
|
|
356
|
+
export async function shareFile(opts) {
|
|
643
357
|
try {
|
|
644
|
-
|
|
645
|
-
title:
|
|
646
|
-
subject:
|
|
358
|
+
return await FileToolkitModule.shareFile(opts.filePath, {
|
|
359
|
+
title: opts.title,
|
|
360
|
+
subject: opts.subject
|
|
647
361
|
});
|
|
648
|
-
|
|
649
|
-
} catch (error) {
|
|
362
|
+
} catch (err) {
|
|
650
363
|
return {
|
|
651
364
|
success: false,
|
|
652
|
-
error:
|
|
365
|
+
error: err.message || 'UNKNOWN_ERROR'
|
|
653
366
|
};
|
|
654
367
|
}
|
|
655
368
|
}
|
|
656
|
-
|
|
657
|
-
// ─── openFile ─────────────────────────────────────────────────────────────────
|
|
658
|
-
|
|
659
|
-
/**
|
|
660
|
-
* Open a file with the default app or app chooser.
|
|
661
|
-
* On iOS, displays a preview or shows apps that can handle the file.
|
|
662
|
-
* On Android, opens with the default app or shows app chooser.
|
|
663
|
-
*
|
|
664
|
-
* @example
|
|
665
|
-
* ```typescript
|
|
666
|
-
* const result = await openFile({
|
|
667
|
-
* filePath: '/path/to/document.pdf',
|
|
668
|
-
* mimeType: 'application/pdf' // optional, auto-detected if not provided
|
|
669
|
-
* });
|
|
670
|
-
*
|
|
671
|
-
* if (result.success) {
|
|
672
|
-
* console.log('File opened successfully');
|
|
673
|
-
* }
|
|
674
|
-
* ```
|
|
675
|
-
*/
|
|
676
|
-
export async function openFile(options) {
|
|
369
|
+
export async function openFile(opts) {
|
|
677
370
|
try {
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
} catch (error) {
|
|
371
|
+
return await FileToolkitModule.openFile(opts.filePath, opts.mimeType || '');
|
|
372
|
+
} catch (err) {
|
|
681
373
|
return {
|
|
682
374
|
success: false,
|
|
683
|
-
error:
|
|
375
|
+
error: err.message || 'UNKNOWN_ERROR'
|
|
684
376
|
};
|
|
685
377
|
}
|
|
686
378
|
}
|
|
687
|
-
|
|
688
|
-
// ─── Zip / Unzip ─────────────────────────────────────────────────────────────
|
|
689
|
-
|
|
690
|
-
/**
|
|
691
|
-
* Extract a ZIP archive to a destination directory.
|
|
692
|
-
*
|
|
693
|
-
* Uses `java.util.zip` on Android and zlib (system framework) on iOS.
|
|
694
|
-
* No third-party dependency required.
|
|
695
|
-
*
|
|
696
|
-
* @param sourcePath Absolute path to the `.zip` file
|
|
697
|
-
* @param destDir Absolute path to the directory where files will be extracted.
|
|
698
|
-
* The directory is created automatically if it does not exist.
|
|
699
|
-
*
|
|
700
|
-
* @example
|
|
701
|
-
* ```ts
|
|
702
|
-
* const result = await unzip(
|
|
703
|
-
* '/path/to/archive.zip',
|
|
704
|
-
* '/path/to/output-folder'
|
|
705
|
-
* );
|
|
706
|
-
*
|
|
707
|
-
* if (result.success) {
|
|
708
|
-
* console.log('Extracted files:', result.files);
|
|
709
|
-
* }
|
|
710
|
-
* ```
|
|
711
|
-
*/
|
|
712
|
-
export async function unzip(sourcePath, destDir) {
|
|
379
|
+
export async function unzip(s, d) {
|
|
713
380
|
try {
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
} catch (error) {
|
|
381
|
+
return await FileToolkitModule.unzip(s, d);
|
|
382
|
+
} catch {
|
|
717
383
|
return {
|
|
718
384
|
success: false,
|
|
719
|
-
error:
|
|
385
|
+
error: 'UNZIP_ERROR'
|
|
720
386
|
};
|
|
721
387
|
}
|
|
722
388
|
}
|
|
723
|
-
|
|
724
|
-
/**
|
|
725
|
-
* Create a ZIP archive from a file or directory.
|
|
726
|
-
*
|
|
727
|
-
* Uses `java.util.zip` on Android and zlib (system framework) on iOS.
|
|
728
|
-
* No third-party dependency required.
|
|
729
|
-
*
|
|
730
|
-
* @param sourcePath Absolute path to the file or directory to compress
|
|
731
|
-
* @param destPath Absolute path for the output `.zip` file.
|
|
732
|
-
* Parent directory is created automatically if needed.
|
|
733
|
-
*
|
|
734
|
-
* @example
|
|
735
|
-
* ```ts
|
|
736
|
-
* // Zip a single file
|
|
737
|
-
* const result = await zip(
|
|
738
|
-
* '/path/to/document.pdf',
|
|
739
|
-
* '/path/to/document.zip'
|
|
740
|
-
* );
|
|
741
|
-
*
|
|
742
|
-
* // Zip an entire directory
|
|
743
|
-
* const result = await zip(
|
|
744
|
-
* '/path/to/my-folder',
|
|
745
|
-
* '/path/to/my-folder.zip'
|
|
746
|
-
* );
|
|
747
|
-
*
|
|
748
|
-
* if (result.success) {
|
|
749
|
-
* console.log('Archive created at:', result.zipPath);
|
|
750
|
-
* }
|
|
751
|
-
* ```
|
|
752
|
-
*/
|
|
753
|
-
export async function zip(sourcePath, destPath) {
|
|
389
|
+
export async function zip(s, d) {
|
|
754
390
|
try {
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
} catch (error) {
|
|
391
|
+
return await FileToolkitModule.zip(s, d);
|
|
392
|
+
} catch {
|
|
758
393
|
return {
|
|
759
394
|
success: false,
|
|
760
|
-
error:
|
|
395
|
+
error: 'ZIP_ERROR'
|
|
761
396
|
};
|
|
762
397
|
}
|
|
763
398
|
}
|
|
764
|
-
|
|
765
|
-
// ─── useDownload hook ─────────────────────────────────────────────────────────
|
|
766
|
-
|
|
767
|
-
/**
|
|
768
|
-
* React hook for managing a single download with built-in state.
|
|
769
|
-
*
|
|
770
|
-
* Tracks status, rich progress (percent, speed, ETA), and the final result.
|
|
771
|
-
* Exposes `pause`, `resume`, and `cancel` controls tied to the active download.
|
|
772
|
-
*
|
|
773
|
-
* @example
|
|
774
|
-
* ```tsx
|
|
775
|
-
* function DownloadButton() {
|
|
776
|
-
* const { start, pause, resume, cancel, status, progress, result } = useDownload();
|
|
777
|
-
*
|
|
778
|
-
* return (
|
|
779
|
-
* <View>
|
|
780
|
-
* <Button
|
|
781
|
-
* title="Download"
|
|
782
|
-
* onPress={() => start({ url: 'https://example.com/file.zip' })}
|
|
783
|
-
* />
|
|
784
|
-
*
|
|
785
|
-
* {status === 'downloading' && progress && (
|
|
786
|
-
* <View>
|
|
787
|
-
* <Text>{progress.percent.toFixed(1)}%</Text>
|
|
788
|
-
* <Text>Speed: {(progress.speedBps / 1024).toFixed(1)} KB/s</Text>
|
|
789
|
-
* <Text>ETA: {progress.etaSeconds.toFixed(0)}s</Text>
|
|
790
|
-
* <ProgressBar value={progress.percent / 100} />
|
|
791
|
-
* <Button title="Pause" onPress={pause} />
|
|
792
|
-
* </View>
|
|
793
|
-
* )}
|
|
794
|
-
*
|
|
795
|
-
* {status === 'paused' && (
|
|
796
|
-
* <Button title="Resume" onPress={resume} />
|
|
797
|
-
* )}
|
|
798
|
-
*
|
|
799
|
-
* {status === 'done' && result?.success && (
|
|
800
|
-
* <Text>Saved to: {result.filePath}</Text>
|
|
801
|
-
* )}
|
|
802
|
-
*
|
|
803
|
-
* {status === 'error' && (
|
|
804
|
-
* <Text>Error: {result?.error}</Text>
|
|
805
|
-
* )}
|
|
806
|
-
* </View>
|
|
807
|
-
* );
|
|
808
|
-
* }
|
|
809
|
-
* ```
|
|
810
|
-
*/
|
|
811
399
|
export function useDownload() {
|
|
812
400
|
const [status, setStatus] = useState('idle');
|
|
813
401
|
const [progress, setProgress] = useState(null);
|
|
814
402
|
const [result, setResult] = useState(null);
|
|
815
403
|
const [downloadId, setDownloadId] = useState(null);
|
|
816
404
|
const downloadIdRef = useRef(null);
|
|
817
|
-
const start = useCallback(async
|
|
405
|
+
const start = useCallback(async opts => {
|
|
406
|
+
const id = opts.downloadId || _generateId();
|
|
818
407
|
setStatus('downloading');
|
|
819
408
|
setProgress(null);
|
|
820
409
|
setResult(null);
|
|
821
|
-
setDownloadId(
|
|
822
|
-
downloadIdRef.current =
|
|
410
|
+
setDownloadId(id);
|
|
411
|
+
downloadIdRef.current = id;
|
|
823
412
|
const res = await download({
|
|
824
|
-
...
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
413
|
+
...opts,
|
|
414
|
+
downloadId: id,
|
|
415
|
+
onProgress: p => {
|
|
416
|
+
setProgress(p);
|
|
417
|
+
opts.onProgress?.(p);
|
|
828
418
|
}
|
|
829
419
|
});
|
|
830
|
-
if (res.downloadId) {
|
|
831
|
-
downloadIdRef.current = res.downloadId;
|
|
832
|
-
setDownloadId(res.downloadId);
|
|
833
|
-
}
|
|
834
420
|
setResult(res);
|
|
835
|
-
|
|
836
|
-
setStatus(isBackgroundPending ? 'downloading' : res.success ? 'done' : 'error');
|
|
421
|
+
setStatus(!!opts.background && !!res.success && !!res.downloadId && !res.filePath ? 'downloading' : res.success ? 'done' : 'error');
|
|
837
422
|
return res;
|
|
838
423
|
}, []);
|
|
839
424
|
const pause = useCallback(async () => {
|