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