rn-file-toolkit 1.0.1
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/FileToolkit.podspec +22 -0
- package/LICENSE +20 -0
- package/README.md +522 -0
- package/android/build.gradle +61 -0
- package/android/src/main/AndroidManifest.xml +13 -0
- package/android/src/main/java/com/filetoolkit/FileToolkitModule.kt +1204 -0
- package/android/src/main/java/com/filetoolkit/FileToolkitPackage.kt +31 -0
- package/android/src/main/res/xml/file_provider_paths.xml +14 -0
- package/app.plugin.js +49 -0
- package/ios/FileToolkit.h +6 -0
- package/ios/FileToolkit.mm +1468 -0
- package/lib/commonjs/NativeFileToolkit.js +9 -0
- package/lib/commonjs/NativeFileToolkit.js.map +1 -0
- package/lib/commonjs/index.js +941 -0
- package/lib/commonjs/index.js.map +1 -0
- package/lib/commonjs/package.json +1 -0
- package/lib/module/NativeFileToolkit.js +5 -0
- package/lib/module/NativeFileToolkit.js.map +1 -0
- package/lib/module/index.js +905 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/typescript/commonjs/package.json +1 -0
- package/lib/typescript/commonjs/src/NativeFileToolkit.d.ts +29 -0
- package/lib/typescript/commonjs/src/NativeFileToolkit.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/index.d.ts +635 -0
- package/lib/typescript/commonjs/src/index.d.ts.map +1 -0
- package/lib/typescript/module/package.json +1 -0
- package/lib/typescript/module/src/NativeFileToolkit.d.ts +29 -0
- package/lib/typescript/module/src/NativeFileToolkit.d.ts.map +1 -0
- package/lib/typescript/module/src/index.d.ts +635 -0
- package/lib/typescript/module/src/index.d.ts.map +1 -0
- package/package.json +232 -0
- package/src/NativeFileToolkit.ts +29 -0
- package/src/index.tsx +1293 -0
|
@@ -0,0 +1,941 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.cancelDownload = cancelDownload;
|
|
7
|
+
exports.clearCache = clearCache;
|
|
8
|
+
exports.copyFile = copyFile;
|
|
9
|
+
exports.default = void 0;
|
|
10
|
+
exports.deleteFile = deleteFile;
|
|
11
|
+
exports.download = download;
|
|
12
|
+
exports.exists = exists;
|
|
13
|
+
exports.fs = void 0;
|
|
14
|
+
exports.getBackgroundDownloads = getBackgroundDownloads;
|
|
15
|
+
exports.getCachedFiles = getCachedFiles;
|
|
16
|
+
exports.getQueueStatus = getQueueStatus;
|
|
17
|
+
exports.ls = ls;
|
|
18
|
+
exports.mkdir = mkdir;
|
|
19
|
+
exports.moveFile = moveFile;
|
|
20
|
+
exports.onDownloadComplete = onDownloadComplete;
|
|
21
|
+
exports.onDownloadError = onDownloadError;
|
|
22
|
+
exports.onDownloadRetry = onDownloadRetry;
|
|
23
|
+
exports.onUploadProgress = onUploadProgress;
|
|
24
|
+
exports.openFile = openFile;
|
|
25
|
+
exports.pauseDownload = pauseDownload;
|
|
26
|
+
exports.readFile = readFile;
|
|
27
|
+
exports.resumeDownload = resumeDownload;
|
|
28
|
+
exports.saveBase64AsFile = saveBase64AsFile;
|
|
29
|
+
exports.setQueueOptions = setQueueOptions;
|
|
30
|
+
exports.shareFile = shareFile;
|
|
31
|
+
exports.stat = stat;
|
|
32
|
+
exports.unzip = unzip;
|
|
33
|
+
exports.upload = upload;
|
|
34
|
+
exports.urlToBase64 = urlToBase64;
|
|
35
|
+
exports.useDownload = useDownload;
|
|
36
|
+
exports.writeFile = writeFile;
|
|
37
|
+
exports.zip = zip;
|
|
38
|
+
var _react = require("react");
|
|
39
|
+
var _reactNative = require("react-native");
|
|
40
|
+
var _NativeFileToolkit = _interopRequireDefault(require("./NativeFileToolkit"));
|
|
41
|
+
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
|
|
42
|
+
const FileToolkitModule = _reactNative.NativeModules.FileToolkit || _NativeFileToolkit.default;
|
|
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
|
+
class DownloadQueue {
|
|
55
|
+
_maxConcurrent = 3;
|
|
56
|
+
_active = 0;
|
|
57
|
+
_queue = [];
|
|
58
|
+
setOptions(opts) {
|
|
59
|
+
if (opts.maxConcurrent != null && opts.maxConcurrent > 0) {
|
|
60
|
+
this._maxConcurrent = opts.maxConcurrent;
|
|
61
|
+
// Kick off any slots that just opened.
|
|
62
|
+
this._flush();
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
getStatus() {
|
|
66
|
+
return {
|
|
67
|
+
active: this._active,
|
|
68
|
+
pending: this._queue.length,
|
|
69
|
+
maxConcurrent: this._maxConcurrent
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
enqueue(options) {
|
|
73
|
+
return new Promise((resolve, reject) => {
|
|
74
|
+
const item = {
|
|
75
|
+
options,
|
|
76
|
+
resolve,
|
|
77
|
+
reject
|
|
78
|
+
};
|
|
79
|
+
if (options.priority === 'high') {
|
|
80
|
+
this._queue.unshift(item);
|
|
81
|
+
} else {
|
|
82
|
+
this._queue.push(item);
|
|
83
|
+
}
|
|
84
|
+
this._flush();
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
_flush() {
|
|
88
|
+
while (this._active < this._maxConcurrent && this._queue.length > 0) {
|
|
89
|
+
const item = this._queue.shift();
|
|
90
|
+
this._active++;
|
|
91
|
+
// Strip queue-specific fields before passing to the native layer.
|
|
92
|
+
const nativeOptions = {
|
|
93
|
+
...item.options
|
|
94
|
+
};
|
|
95
|
+
delete nativeOptions.queue;
|
|
96
|
+
delete nativeOptions.priority;
|
|
97
|
+
_executeDownload(nativeOptions).then(result => {
|
|
98
|
+
item.resolve(result);
|
|
99
|
+
}).catch(err => {
|
|
100
|
+
item.reject(err);
|
|
101
|
+
}).finally(() => {
|
|
102
|
+
this._active--;
|
|
103
|
+
this._flush();
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
const _globalQueue = new DownloadQueue();
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Configure the global managed download queue.
|
|
112
|
+
*
|
|
113
|
+
* @example
|
|
114
|
+
* ```ts
|
|
115
|
+
* setQueueOptions({ maxConcurrent: 3 });
|
|
116
|
+
* ```
|
|
117
|
+
*/
|
|
118
|
+
function setQueueOptions(options) {
|
|
119
|
+
_globalQueue.setOptions(options);
|
|
120
|
+
}
|
|
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
|
+
function getQueueStatus() {
|
|
131
|
+
return _globalQueue.getStatus();
|
|
132
|
+
}
|
|
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
|
+
async function _executeDownload(options) {
|
|
141
|
+
let progressSubscription = null;
|
|
142
|
+
let retrySubscription = null;
|
|
143
|
+
|
|
144
|
+
// downloadId is resolved immediately on Android foreground, allowing
|
|
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)
|
|
150
|
+
let _lastProgressTs = null;
|
|
151
|
+
let _lastProgressBytes = 0;
|
|
152
|
+
// Smoothed speed using exponential moving average (α = 0.3)
|
|
153
|
+
let _smoothedSpeedBps = 0;
|
|
154
|
+
if (options.onProgress) {
|
|
155
|
+
progressSubscription = eventEmitter.addListener('onDownloadProgress', event => {
|
|
156
|
+
const matchesId = knownDownloadId && event.downloadId === knownDownloadId;
|
|
157
|
+
const matchesUrl = !knownDownloadId && event.url === options.url;
|
|
158
|
+
if ((matchesId || matchesUrl) && options.onProgress) {
|
|
159
|
+
const now = Date.now();
|
|
160
|
+
const percent = event.progress ?? 0;
|
|
161
|
+
const bytesDownloaded = event.bytesDownloaded ?? 0;
|
|
162
|
+
const totalBytes = event.totalBytes ?? 0;
|
|
163
|
+
let speedBps = 0;
|
|
164
|
+
let etaSeconds = 0;
|
|
165
|
+
if (_lastProgressTs !== null) {
|
|
166
|
+
const dtSec = (now - _lastProgressTs) / 1000;
|
|
167
|
+
if (dtSec > 0) {
|
|
168
|
+
const bytesDelta = bytesDownloaded - _lastProgressBytes;
|
|
169
|
+
// Only update speed if we have byte-level data from native
|
|
170
|
+
if (bytesDelta > 0 && totalBytes > 0) {
|
|
171
|
+
const instantSpeed = bytesDelta / dtSec;
|
|
172
|
+
// Exponential moving average for smoother readings
|
|
173
|
+
_smoothedSpeedBps = _smoothedSpeedBps === 0 ? instantSpeed : 0.3 * instantSpeed + 0.7 * _smoothedSpeedBps;
|
|
174
|
+
speedBps = _smoothedSpeedBps;
|
|
175
|
+
const remaining = totalBytes - bytesDownloaded;
|
|
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
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
_lastProgressTs = now;
|
|
194
|
+
_lastProgressBytes = bytesDownloaded;
|
|
195
|
+
const info = {
|
|
196
|
+
percent,
|
|
197
|
+
bytesDownloaded,
|
|
198
|
+
totalBytes,
|
|
199
|
+
speedBps,
|
|
200
|
+
etaSeconds
|
|
201
|
+
};
|
|
202
|
+
options.onProgress(info);
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
if (options.retry?.onRetry) {
|
|
207
|
+
retrySubscription = eventEmitter.addListener('onDownloadRetry', event => {
|
|
208
|
+
const matchesId = knownDownloadId && event.downloadId === knownDownloadId;
|
|
209
|
+
const matchesUrl = !knownDownloadId && event.url === options.url;
|
|
210
|
+
if ((matchesId || matchesUrl) && options.retry?.onRetry) {
|
|
211
|
+
options.retry.onRetry(event.attempt, event.error ?? '');
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
const cleanup = () => {
|
|
216
|
+
progressSubscription?.remove();
|
|
217
|
+
retrySubscription?.remove();
|
|
218
|
+
};
|
|
219
|
+
try {
|
|
220
|
+
const resultPromise = _NativeFileToolkit.default.download({
|
|
221
|
+
url: options.url,
|
|
222
|
+
fileName: options.fileName,
|
|
223
|
+
background: options.background ?? false,
|
|
224
|
+
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
|
|
233
|
+
});
|
|
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
|
+
cleanup();
|
|
247
|
+
return result;
|
|
248
|
+
} catch (error) {
|
|
249
|
+
cleanup();
|
|
250
|
+
return {
|
|
251
|
+
success: false,
|
|
252
|
+
error: error?.message || 'UNKNOWN_ERROR'
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
}
|
|
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
|
+
function download(options) {
|
|
284
|
+
if (options.queue) {
|
|
285
|
+
return _globalQueue.enqueue(options);
|
|
286
|
+
}
|
|
287
|
+
return _executeDownload(options);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// ─── Core upload ───────────────────────────────────────────────────────────────
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Upload a file using multipart/form-data.
|
|
294
|
+
*/
|
|
295
|
+
async function upload(options) {
|
|
296
|
+
let progressSubscription = null;
|
|
297
|
+
if (options.onProgress) {
|
|
298
|
+
progressSubscription = eventEmitter.addListener('onUploadProgress', event => {
|
|
299
|
+
if (event.url === options.url && options.onProgress) {
|
|
300
|
+
options.onProgress(event.progress);
|
|
301
|
+
}
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
try {
|
|
305
|
+
const result = await _NativeFileToolkit.default.upload({
|
|
306
|
+
url: options.url,
|
|
307
|
+
filePath: options.filePath,
|
|
308
|
+
fieldName: options.fieldName ?? 'file',
|
|
309
|
+
headers: options.headers ?? {},
|
|
310
|
+
parameters: options.parameters ?? {}
|
|
311
|
+
});
|
|
312
|
+
if (progressSubscription) {
|
|
313
|
+
progressSubscription.remove();
|
|
314
|
+
}
|
|
315
|
+
return result;
|
|
316
|
+
} catch (error) {
|
|
317
|
+
if (progressSubscription) {
|
|
318
|
+
progressSubscription.remove();
|
|
319
|
+
}
|
|
320
|
+
return {
|
|
321
|
+
success: false,
|
|
322
|
+
error: error?.message || 'UNKNOWN_ERROR'
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
}
|
|
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) {
|
|
335
|
+
try {
|
|
336
|
+
return await _NativeFileToolkit.default.pauseDownload(downloadId);
|
|
337
|
+
} catch (error) {
|
|
338
|
+
return {
|
|
339
|
+
success: false,
|
|
340
|
+
error: error?.message || 'UNKNOWN_ERROR'
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
}
|
|
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) {
|
|
350
|
+
try {
|
|
351
|
+
return await _NativeFileToolkit.default.resumeDownload(downloadId);
|
|
352
|
+
} catch (error) {
|
|
353
|
+
return {
|
|
354
|
+
success: false,
|
|
355
|
+
error: error?.message || 'UNKNOWN_ERROR'
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Cancel and discard a download (foreground or background).
|
|
362
|
+
* Any partially-downloaded file is deleted.
|
|
363
|
+
*/
|
|
364
|
+
async function cancelDownload(downloadId) {
|
|
365
|
+
try {
|
|
366
|
+
return await _NativeFileToolkit.default.cancelDownload(downloadId);
|
|
367
|
+
} catch (error) {
|
|
368
|
+
return {
|
|
369
|
+
success: false,
|
|
370
|
+
error: error?.message || 'UNKNOWN_ERROR'
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// ─── Cache management ─────────────────────────────────────────────────────────
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* List all files in the app's cache directory.
|
|
379
|
+
*/
|
|
380
|
+
async function getCachedFiles() {
|
|
381
|
+
try {
|
|
382
|
+
return await _NativeFileToolkit.default.getCachedFiles();
|
|
383
|
+
} catch (error) {
|
|
384
|
+
return {
|
|
385
|
+
success: false,
|
|
386
|
+
error: error?.message || 'UNKNOWN_ERROR'
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* Delete a single file by its absolute path.
|
|
393
|
+
*/
|
|
394
|
+
async function deleteFile(filePath) {
|
|
395
|
+
try {
|
|
396
|
+
return await _NativeFileToolkit.default.deleteFile(filePath);
|
|
397
|
+
} catch (error) {
|
|
398
|
+
return {
|
|
399
|
+
success: false,
|
|
400
|
+
error: error?.message || 'UNKNOWN_ERROR'
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* Delete all files in the app's cache directory.
|
|
407
|
+
*/
|
|
408
|
+
async function clearCache() {
|
|
409
|
+
try {
|
|
410
|
+
return await _NativeFileToolkit.default.clearCache();
|
|
411
|
+
} catch (error) {
|
|
412
|
+
return {
|
|
413
|
+
success: false,
|
|
414
|
+
error: error?.message || 'UNKNOWN_ERROR'
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* Get all active background downloads.
|
|
421
|
+
* Use this after app restart to "re-attach" to ongoing downloads.
|
|
422
|
+
*/
|
|
423
|
+
async function getBackgroundDownloads() {
|
|
424
|
+
try {
|
|
425
|
+
return await _NativeFileToolkit.default.getBackgroundDownloads();
|
|
426
|
+
} catch (error) {
|
|
427
|
+
return {
|
|
428
|
+
success: false,
|
|
429
|
+
error: error?.message || 'UNKNOWN_ERROR'
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// ─── File system helpers ─────────────────────────────────────────────────────
|
|
435
|
+
|
|
436
|
+
function _ensureFsSuccess(result, fallback) {
|
|
437
|
+
if (!result?.success) {
|
|
438
|
+
throw new Error(result?.error || fallback);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/** Check whether a file or directory exists. */
|
|
443
|
+
async function exists(filePath) {
|
|
444
|
+
const result = await _NativeFileToolkit.default.exists(filePath);
|
|
445
|
+
_ensureFsSuccess(result, 'EXISTS_ERROR');
|
|
446
|
+
return !!result.exists;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/** Get file or directory metadata. */
|
|
450
|
+
async function stat(filePath) {
|
|
451
|
+
const result = await _NativeFileToolkit.default.stat(filePath);
|
|
452
|
+
_ensureFsSuccess(result, 'STAT_ERROR');
|
|
453
|
+
return result.stat;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/** Read a file as utf8 (default) or base64 string. */
|
|
457
|
+
async function readFile(filePath, encoding = 'utf8') {
|
|
458
|
+
const result = await _NativeFileToolkit.default.readFile(filePath, encoding);
|
|
459
|
+
_ensureFsSuccess(result, 'READ_FILE_ERROR');
|
|
460
|
+
return result.data ?? '';
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/** Write utf8 (default) or base64 data to a file. */
|
|
464
|
+
async function writeFile(filePath, data, encoding = 'utf8') {
|
|
465
|
+
const result = await _NativeFileToolkit.default.writeFile(filePath, data, encoding);
|
|
466
|
+
_ensureFsSuccess(result, 'WRITE_FILE_ERROR');
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/** Copy a file from source to destination. */
|
|
470
|
+
async function copyFile(fromPath, toPath) {
|
|
471
|
+
const result = await _NativeFileToolkit.default.copyFile(fromPath, toPath);
|
|
472
|
+
_ensureFsSuccess(result, 'COPY_FILE_ERROR');
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/** Move a file from source to destination. */
|
|
476
|
+
async function moveFile(fromPath, toPath) {
|
|
477
|
+
const result = await _NativeFileToolkit.default.moveFile(fromPath, toPath);
|
|
478
|
+
_ensureFsSuccess(result, 'MOVE_FILE_ERROR');
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/** Create a directory recursively. */
|
|
482
|
+
async function mkdir(dirPath) {
|
|
483
|
+
const result = await _NativeFileToolkit.default.mkdir(dirPath);
|
|
484
|
+
_ensureFsSuccess(result, 'MKDIR_ERROR');
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
/** List direct entries (names) in a directory. */
|
|
488
|
+
async function ls(dirPath) {
|
|
489
|
+
const result = await _NativeFileToolkit.default.ls(dirPath);
|
|
490
|
+
_ensureFsSuccess(result, 'LS_ERROR');
|
|
491
|
+
return result.entries || [];
|
|
492
|
+
}
|
|
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
|
+
const fs = exports.fs = {
|
|
507
|
+
exists,
|
|
508
|
+
stat,
|
|
509
|
+
readFile,
|
|
510
|
+
writeFile,
|
|
511
|
+
copyFile,
|
|
512
|
+
moveFile,
|
|
513
|
+
deleteFile: async filePath => {
|
|
514
|
+
const result = await deleteFile(filePath);
|
|
515
|
+
_ensureFsSuccess(result, 'DELETE_FILE_ERROR');
|
|
516
|
+
},
|
|
517
|
+
mkdir,
|
|
518
|
+
ls
|
|
519
|
+
};
|
|
520
|
+
|
|
521
|
+
// ─── Event helpers ────────────────────────────────────────────────────────────
|
|
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();
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
/**
|
|
533
|
+
* Subscribe to background download error events.
|
|
534
|
+
* Returns an unsubscribe function.
|
|
535
|
+
*/
|
|
536
|
+
function onDownloadError(callback) {
|
|
537
|
+
const sub = eventEmitter.addListener('onDownloadError', callback);
|
|
538
|
+
return () => sub.remove();
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
/**
|
|
542
|
+
* Subscribe to upload progress events.
|
|
543
|
+
* Returns an unsubscribe function.
|
|
544
|
+
*/
|
|
545
|
+
function onUploadProgress(callback) {
|
|
546
|
+
const sub = eventEmitter.addListener('onUploadProgress', callback);
|
|
547
|
+
return () => sub.remove();
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
/**
|
|
551
|
+
* Subscribe to download retry events (fired before each retry attempt).
|
|
552
|
+
* Returns an unsubscribe function.
|
|
553
|
+
*/
|
|
554
|
+
function onDownloadRetry(callback) {
|
|
555
|
+
const sub = eventEmitter.addListener('onDownloadRetry', callback);
|
|
556
|
+
return () => sub.remove();
|
|
557
|
+
}
|
|
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) {
|
|
583
|
+
try {
|
|
584
|
+
let {
|
|
585
|
+
base64Data,
|
|
586
|
+
fileName,
|
|
587
|
+
destination
|
|
588
|
+
} = options;
|
|
589
|
+
|
|
590
|
+
// Parse data URI if present (e.g., "data:image/png;base64,iVBORw0...")
|
|
591
|
+
if (base64Data.startsWith('data:')) {
|
|
592
|
+
const matches = base64Data.match(/^data:([^;]+);base64,(.+)$/);
|
|
593
|
+
if (matches && matches[2]) {
|
|
594
|
+
base64Data = matches[2];
|
|
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
|
+
};
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
const result = await FileToolkitModule.saveBase64AsFile({
|
|
610
|
+
base64Data,
|
|
611
|
+
fileName,
|
|
612
|
+
destination
|
|
613
|
+
});
|
|
614
|
+
return result;
|
|
615
|
+
} catch (error) {
|
|
616
|
+
return {
|
|
617
|
+
success: false,
|
|
618
|
+
error: error?.message || 'UNKNOWN_ERROR'
|
|
619
|
+
};
|
|
620
|
+
}
|
|
621
|
+
}
|
|
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) {
|
|
645
|
+
try {
|
|
646
|
+
const result = await FileToolkitModule.urlToBase64({
|
|
647
|
+
url: options.url,
|
|
648
|
+
headers: options.headers
|
|
649
|
+
});
|
|
650
|
+
return result;
|
|
651
|
+
} catch (error) {
|
|
652
|
+
return {
|
|
653
|
+
success: false,
|
|
654
|
+
error: error?.message || 'UNKNOWN_ERROR'
|
|
655
|
+
};
|
|
656
|
+
}
|
|
657
|
+
}
|
|
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) {
|
|
679
|
+
try {
|
|
680
|
+
const result = await FileToolkitModule.shareFile(options.filePath, {
|
|
681
|
+
title: options.title,
|
|
682
|
+
subject: options.subject
|
|
683
|
+
});
|
|
684
|
+
return result;
|
|
685
|
+
} catch (error) {
|
|
686
|
+
return {
|
|
687
|
+
success: false,
|
|
688
|
+
error: error?.message || 'UNKNOWN_ERROR'
|
|
689
|
+
};
|
|
690
|
+
}
|
|
691
|
+
}
|
|
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) {
|
|
713
|
+
try {
|
|
714
|
+
const result = await FileToolkitModule.openFile(options.filePath, options.mimeType || '');
|
|
715
|
+
return result;
|
|
716
|
+
} catch (error) {
|
|
717
|
+
return {
|
|
718
|
+
success: false,
|
|
719
|
+
error: error?.message || 'UNKNOWN_ERROR'
|
|
720
|
+
};
|
|
721
|
+
}
|
|
722
|
+
}
|
|
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) {
|
|
749
|
+
try {
|
|
750
|
+
const result = await _NativeFileToolkit.default.unzip(sourcePath, destDir);
|
|
751
|
+
return result;
|
|
752
|
+
} catch (error) {
|
|
753
|
+
return {
|
|
754
|
+
success: false,
|
|
755
|
+
error: error?.message || 'UNZIP_ERROR'
|
|
756
|
+
};
|
|
757
|
+
}
|
|
758
|
+
}
|
|
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) {
|
|
790
|
+
try {
|
|
791
|
+
const result = await _NativeFileToolkit.default.zip(sourcePath, destPath);
|
|
792
|
+
return result;
|
|
793
|
+
} catch (error) {
|
|
794
|
+
return {
|
|
795
|
+
success: false,
|
|
796
|
+
error: error?.message || 'ZIP_ERROR'
|
|
797
|
+
};
|
|
798
|
+
}
|
|
799
|
+
}
|
|
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
|
+
function useDownload() {
|
|
848
|
+
const [status, setStatus] = (0, _react.useState)('idle');
|
|
849
|
+
const [progress, setProgress] = (0, _react.useState)(null);
|
|
850
|
+
const [result, setResult] = (0, _react.useState)(null);
|
|
851
|
+
const [downloadId, setDownloadId] = (0, _react.useState)(null);
|
|
852
|
+
const downloadIdRef = (0, _react.useRef)(null);
|
|
853
|
+
const start = (0, _react.useCallback)(async options => {
|
|
854
|
+
setStatus('downloading');
|
|
855
|
+
setProgress(null);
|
|
856
|
+
setResult(null);
|
|
857
|
+
setDownloadId(null);
|
|
858
|
+
downloadIdRef.current = null;
|
|
859
|
+
const res = await download({
|
|
860
|
+
...options,
|
|
861
|
+
onProgress: info => {
|
|
862
|
+
setProgress(info);
|
|
863
|
+
options.onProgress?.(info);
|
|
864
|
+
}
|
|
865
|
+
});
|
|
866
|
+
if (res.downloadId) {
|
|
867
|
+
downloadIdRef.current = res.downloadId;
|
|
868
|
+
setDownloadId(res.downloadId);
|
|
869
|
+
}
|
|
870
|
+
setResult(res);
|
|
871
|
+
const isBackgroundPending = !!options.background && !!res.success && !!res.downloadId && !res.filePath;
|
|
872
|
+
setStatus(isBackgroundPending ? 'downloading' : res.success ? 'done' : 'error');
|
|
873
|
+
return res;
|
|
874
|
+
}, []);
|
|
875
|
+
const pause = (0, _react.useCallback)(async () => {
|
|
876
|
+
if (downloadIdRef.current) {
|
|
877
|
+
await pauseDownload(downloadIdRef.current);
|
|
878
|
+
setStatus('paused');
|
|
879
|
+
}
|
|
880
|
+
}, []);
|
|
881
|
+
const resume = (0, _react.useCallback)(async () => {
|
|
882
|
+
if (downloadIdRef.current) {
|
|
883
|
+
await resumeDownload(downloadIdRef.current);
|
|
884
|
+
setStatus('downloading');
|
|
885
|
+
}
|
|
886
|
+
}, []);
|
|
887
|
+
const cancel = (0, _react.useCallback)(async () => {
|
|
888
|
+
if (downloadIdRef.current) {
|
|
889
|
+
await cancelDownload(downloadIdRef.current);
|
|
890
|
+
setStatus('idle');
|
|
891
|
+
setProgress(null);
|
|
892
|
+
setResult(null);
|
|
893
|
+
downloadIdRef.current = null;
|
|
894
|
+
setDownloadId(null);
|
|
895
|
+
}
|
|
896
|
+
}, []);
|
|
897
|
+
return {
|
|
898
|
+
start,
|
|
899
|
+
pause,
|
|
900
|
+
resume,
|
|
901
|
+
cancel,
|
|
902
|
+
status,
|
|
903
|
+
progress,
|
|
904
|
+
result,
|
|
905
|
+
downloadId
|
|
906
|
+
};
|
|
907
|
+
}
|
|
908
|
+
var _default = exports.default = {
|
|
909
|
+
download,
|
|
910
|
+
upload,
|
|
911
|
+
pauseDownload,
|
|
912
|
+
resumeDownload,
|
|
913
|
+
cancelDownload,
|
|
914
|
+
getCachedFiles,
|
|
915
|
+
deleteFile,
|
|
916
|
+
clearCache,
|
|
917
|
+
getBackgroundDownloads,
|
|
918
|
+
saveBase64AsFile,
|
|
919
|
+
urlToBase64,
|
|
920
|
+
shareFile,
|
|
921
|
+
openFile,
|
|
922
|
+
onDownloadComplete,
|
|
923
|
+
onDownloadError,
|
|
924
|
+
onUploadProgress,
|
|
925
|
+
onDownloadRetry,
|
|
926
|
+
setQueueOptions,
|
|
927
|
+
getQueueStatus,
|
|
928
|
+
exists,
|
|
929
|
+
stat,
|
|
930
|
+
readFile,
|
|
931
|
+
writeFile,
|
|
932
|
+
copyFile,
|
|
933
|
+
moveFile,
|
|
934
|
+
mkdir,
|
|
935
|
+
ls,
|
|
936
|
+
fs,
|
|
937
|
+
useDownload,
|
|
938
|
+
unzip,
|
|
939
|
+
zip
|
|
940
|
+
};
|
|
941
|
+
//# sourceMappingURL=index.js.map
|