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
|
@@ -6,6 +6,12 @@ import android.content.Intent
|
|
|
6
6
|
import android.net.Uri
|
|
7
7
|
import android.os.Environment
|
|
8
8
|
import androidx.core.content.FileProvider
|
|
9
|
+
import android.content.BroadcastReceiver
|
|
10
|
+
import android.content.IntentFilter
|
|
11
|
+
import android.database.Cursor
|
|
12
|
+
import android.os.Handler
|
|
13
|
+
import android.os.Looper
|
|
14
|
+
import android.os.Build
|
|
9
15
|
import com.facebook.react.bridge.ReactApplicationContext
|
|
10
16
|
import com.facebook.react.bridge.ReadableMap
|
|
11
17
|
import com.facebook.react.bridge.Promise
|
|
@@ -48,6 +54,34 @@ class FileToolkitModule(private val reactContext: ReactApplicationContext) :
|
|
|
48
54
|
// Stored promises for foreground downloads — resolved on completion (not early)
|
|
49
55
|
private val foregroundPromises = ConcurrentHashMap<String, Promise>()
|
|
50
56
|
|
|
57
|
+
private val bgPollHandler = Handler(Looper.getMainLooper())
|
|
58
|
+
private val bgPollRunnable = object : Runnable {
|
|
59
|
+
override fun run() {
|
|
60
|
+
if (bgDownloadIds.isNotEmpty()) {
|
|
61
|
+
pollBackgroundDownloads()
|
|
62
|
+
}
|
|
63
|
+
bgPollHandler.postDelayed(this, 1500)
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
private val downloadReceiver = object : BroadcastReceiver() {
|
|
68
|
+
override fun onReceive(context: Context?, intent: Intent?) {
|
|
69
|
+
if (intent?.action == DownloadManager.ACTION_DOWNLOAD_COMPLETE) {
|
|
70
|
+
val id = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1L)
|
|
71
|
+
handleBackgroundDownloadComplete(id)
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
init {
|
|
77
|
+
val filter = IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE)
|
|
78
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
|
79
|
+
reactContext.registerReceiver(downloadReceiver, filter, Context.RECEIVER_EXPORTED)
|
|
80
|
+
} else {
|
|
81
|
+
reactContext.registerReceiver(downloadReceiver, filter)
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
51
85
|
// ─── Helpers ───────────────────────────────────────────────────────────────
|
|
52
86
|
|
|
53
87
|
private fun emit(event: String, map: com.facebook.react.bridge.WritableMap) {
|
|
@@ -114,7 +148,7 @@ class FileToolkitModule(private val reactContext: ReactApplicationContext) :
|
|
|
114
148
|
val isBackground = options.takeIf { it.hasKey("background") }?.getBoolean("background") ?: false
|
|
115
149
|
val rawFileName = if (options.hasKey("fileName")) options.getString("fileName") else null
|
|
116
150
|
val fileName = resolveFileName(urlString, rawFileName)
|
|
117
|
-
val downloadId = UUID.randomUUID().toString()
|
|
151
|
+
val downloadId = if (options.hasKey("downloadId")) options.getString("downloadId")!! else UUID.randomUUID().toString()
|
|
118
152
|
|
|
119
153
|
val headersMap = options.getMap("headers")
|
|
120
154
|
val destination = options.takeIf { it.hasKey("destination") }?.getString("destination")
|
|
@@ -154,6 +188,10 @@ class FileToolkitModule(private val reactContext: ReactApplicationContext) :
|
|
|
154
188
|
bgDownloadIds[downloadId] = bgId
|
|
155
189
|
activeDownloads.remove(downloadId)
|
|
156
190
|
|
|
191
|
+
// Start polling if not running
|
|
192
|
+
bgPollHandler.removeCallbacks(bgPollRunnable)
|
|
193
|
+
bgPollHandler.post(bgPollRunnable)
|
|
194
|
+
|
|
157
195
|
promise.resolve(Arguments.createMap().apply {
|
|
158
196
|
putBoolean("success", true)
|
|
159
197
|
putString("downloadId", downloadId)
|
|
@@ -353,7 +391,85 @@ class FileToolkitModule(private val reactContext: ReactApplicationContext) :
|
|
|
353
391
|
}
|
|
354
392
|
}
|
|
355
393
|
|
|
356
|
-
|
|
394
|
+
private fun pollBackgroundDownloads() {
|
|
395
|
+
val dm = reactContext.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
|
|
396
|
+
val query = DownloadManager.Query()
|
|
397
|
+
val cursor = dm.query(query) ?: return
|
|
398
|
+
|
|
399
|
+
val currentBgIds = bgDownloadIds.values.toSet()
|
|
400
|
+
|
|
401
|
+
if (cursor.moveToFirst()) {
|
|
402
|
+
val descIdx = cursor.getColumnIndex(DownloadManager.COLUMN_DESCRIPTION)
|
|
403
|
+
val idIdx = cursor.getColumnIndex(DownloadManager.COLUMN_ID)
|
|
404
|
+
val statusIdx = cursor.getColumnIndex(DownloadManager.COLUMN_STATUS)
|
|
405
|
+
val totalIdx = cursor.getColumnIndex(DownloadManager.COLUMN_TOTAL_SIZE_BYTES)
|
|
406
|
+
val currentIdx = cursor.getColumnIndex(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR)
|
|
407
|
+
|
|
408
|
+
do {
|
|
409
|
+
val bgId = cursor.getLong(idIdx)
|
|
410
|
+
if (!currentBgIds.contains(bgId)) continue
|
|
411
|
+
|
|
412
|
+
val description = cursor.getString(descIdx) ?: ""
|
|
413
|
+
if (description.startsWith("rn-file-toolkit-id:")) {
|
|
414
|
+
val downloadId = description.removePrefix("rn-file-toolkit-id:")
|
|
415
|
+
val status = cursor.getInt(statusIdx)
|
|
416
|
+
val total = cursor.getLong(totalIdx)
|
|
417
|
+
val current = cursor.getLong(currentIdx)
|
|
418
|
+
|
|
419
|
+
if (status == DownloadManager.STATUS_RUNNING || status == DownloadManager.STATUS_PENDING) {
|
|
420
|
+
val progress = if (total > 0) (current * 100 / total).toInt() else 0
|
|
421
|
+
val evt = Arguments.createMap().apply {
|
|
422
|
+
putString("downloadId", downloadId)
|
|
423
|
+
putInt("progress", progress)
|
|
424
|
+
putDouble("bytesDownloaded", current.toDouble())
|
|
425
|
+
putDouble("totalBytes", total.toDouble())
|
|
426
|
+
}
|
|
427
|
+
emit("onDownloadProgress", evt)
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
} while (cursor.moveToNext())
|
|
431
|
+
}
|
|
432
|
+
cursor.close()
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
private fun handleBackgroundDownloadComplete(bgId: Long) {
|
|
436
|
+
val dm = reactContext.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
|
|
437
|
+
val query = DownloadManager.Query().setFilterById(bgId)
|
|
438
|
+
val cursor = dm.query(query) ?: return
|
|
439
|
+
if (cursor.moveToFirst()) {
|
|
440
|
+
val descIdx = cursor.getColumnIndex(DownloadManager.COLUMN_DESCRIPTION)
|
|
441
|
+
val statusIdx = cursor.getColumnIndex(DownloadManager.COLUMN_STATUS)
|
|
442
|
+
val uriIdx = cursor.getColumnIndex(DownloadManager.COLUMN_LOCAL_URI)
|
|
443
|
+
val reasonIdx = cursor.getColumnIndex(DownloadManager.COLUMN_REASON)
|
|
444
|
+
|
|
445
|
+
val description = cursor.getString(descIdx) ?: ""
|
|
446
|
+
if (description.startsWith("rn-file-toolkit-id:")) {
|
|
447
|
+
val downloadId = description.removePrefix("rn-file-toolkit-id:")
|
|
448
|
+
val status = cursor.getInt(statusIdx)
|
|
449
|
+
bgDownloadIds.remove(downloadId) // cleanup
|
|
450
|
+
|
|
451
|
+
if (status == DownloadManager.STATUS_SUCCESSFUL) {
|
|
452
|
+
val localUri = cursor.getString(uriIdx)
|
|
453
|
+
val filePath = localUri?.removePrefix("file://") ?: ""
|
|
454
|
+
emit("onDownloadComplete", Arguments.createMap().apply {
|
|
455
|
+
putBoolean("success", true)
|
|
456
|
+
putString("downloadId", downloadId)
|
|
457
|
+
putString("filePath", filePath)
|
|
458
|
+
})
|
|
459
|
+
} else {
|
|
460
|
+
val reason = cursor.getInt(reasonIdx)
|
|
461
|
+
emit("onDownloadError", Arguments.createMap().apply {
|
|
462
|
+
putBoolean("success", false)
|
|
463
|
+
putString("downloadId", downloadId)
|
|
464
|
+
putString("error", "DownloadManager failed with reason/code: $reason")
|
|
465
|
+
})
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
cursor.close()
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// ─── executeDownload (Foreground) ─────────────────────────────────────────────────────────
|
|
357
473
|
|
|
358
474
|
override fun pauseDownload(downloadId: String, promise: Promise) {
|
|
359
475
|
val state = activeDownloads[downloadId]
|
|
@@ -482,7 +598,7 @@ class FileToolkitModule(private val reactContext: ReactApplicationContext) :
|
|
|
482
598
|
putBoolean("success", true)
|
|
483
599
|
putBoolean("exists", file.exists())
|
|
484
600
|
})
|
|
485
|
-
} catch (e:
|
|
601
|
+
} catch (e: Throwable) {
|
|
486
602
|
promise.resolve(Arguments.createMap().apply {
|
|
487
603
|
putBoolean("success", false)
|
|
488
604
|
putString("error", e.message ?: "EXISTS_ERROR")
|
|
@@ -513,7 +629,7 @@ class FileToolkitModule(private val reactContext: ReactApplicationContext) :
|
|
|
513
629
|
putDouble("modified", file.lastModified().toDouble())
|
|
514
630
|
})
|
|
515
631
|
})
|
|
516
|
-
} catch (e:
|
|
632
|
+
} catch (e: Throwable) {
|
|
517
633
|
promise.resolve(Arguments.createMap().apply {
|
|
518
634
|
putBoolean("success", false)
|
|
519
635
|
putString("error", e.message ?: "STAT_ERROR")
|
|
@@ -534,6 +650,15 @@ class FileToolkitModule(private val reactContext: ReactApplicationContext) :
|
|
|
534
650
|
return
|
|
535
651
|
}
|
|
536
652
|
|
|
653
|
+
// Safety check: reject files > 50MB to prevent crashing the RN bridge
|
|
654
|
+
if (file.length() > 50L * 1024 * 1024) {
|
|
655
|
+
promise.resolve(Arguments.createMap().apply {
|
|
656
|
+
putBoolean("success", false)
|
|
657
|
+
putString("error", "File exceeds 50MB limit for readFile. Use streaming or base64 encoding for large files.")
|
|
658
|
+
})
|
|
659
|
+
return
|
|
660
|
+
}
|
|
661
|
+
|
|
537
662
|
val data = if (encoding.equals("base64", ignoreCase = true)) {
|
|
538
663
|
android.util.Base64.encodeToString(file.readBytes(), android.util.Base64.NO_WRAP)
|
|
539
664
|
} else {
|
|
@@ -544,7 +669,7 @@ class FileToolkitModule(private val reactContext: ReactApplicationContext) :
|
|
|
544
669
|
putBoolean("success", true)
|
|
545
670
|
putString("data", data)
|
|
546
671
|
})
|
|
547
|
-
} catch (e:
|
|
672
|
+
} catch (e: Throwable) {
|
|
548
673
|
promise.resolve(Arguments.createMap().apply {
|
|
549
674
|
putBoolean("success", false)
|
|
550
675
|
putString("error", e.message ?: "READ_FILE_ERROR")
|
|
@@ -569,7 +694,7 @@ class FileToolkitModule(private val reactContext: ReactApplicationContext) :
|
|
|
569
694
|
promise.resolve(Arguments.createMap().apply {
|
|
570
695
|
putBoolean("success", true)
|
|
571
696
|
})
|
|
572
|
-
} catch (e:
|
|
697
|
+
} catch (e: Throwable) {
|
|
573
698
|
promise.resolve(Arguments.createMap().apply {
|
|
574
699
|
putBoolean("success", false)
|
|
575
700
|
putString("error", e.message ?: "WRITE_FILE_ERROR")
|
|
@@ -597,7 +722,7 @@ class FileToolkitModule(private val reactContext: ReactApplicationContext) :
|
|
|
597
722
|
promise.resolve(Arguments.createMap().apply {
|
|
598
723
|
putBoolean("success", true)
|
|
599
724
|
})
|
|
600
|
-
} catch (e:
|
|
725
|
+
} catch (e: Throwable) {
|
|
601
726
|
promise.resolve(Arguments.createMap().apply {
|
|
602
727
|
putBoolean("success", false)
|
|
603
728
|
putString("error", e.message ?: "COPY_FILE_ERROR")
|
|
@@ -637,7 +762,7 @@ class FileToolkitModule(private val reactContext: ReactApplicationContext) :
|
|
|
637
762
|
promise.resolve(Arguments.createMap().apply {
|
|
638
763
|
putBoolean("success", true)
|
|
639
764
|
})
|
|
640
|
-
} catch (e:
|
|
765
|
+
} catch (e: Throwable) {
|
|
641
766
|
promise.resolve(Arguments.createMap().apply {
|
|
642
767
|
putBoolean("success", false)
|
|
643
768
|
putString("error", e.message ?: "MOVE_FILE_ERROR")
|
|
@@ -655,7 +780,7 @@ class FileToolkitModule(private val reactContext: ReactApplicationContext) :
|
|
|
655
780
|
putBoolean("success", ok)
|
|
656
781
|
if (!ok) putString("error", "Could not create directory: $dirPath")
|
|
657
782
|
})
|
|
658
|
-
} catch (e:
|
|
783
|
+
} catch (e: Throwable) {
|
|
659
784
|
promise.resolve(Arguments.createMap().apply {
|
|
660
785
|
putBoolean("success", false)
|
|
661
786
|
putString("error", e.message ?: "MKDIR_ERROR")
|
|
@@ -683,7 +808,7 @@ class FileToolkitModule(private val reactContext: ReactApplicationContext) :
|
|
|
683
808
|
putBoolean("success", true)
|
|
684
809
|
putArray("entries", entries)
|
|
685
810
|
})
|
|
686
|
-
} catch (e:
|
|
811
|
+
} catch (e: Throwable) {
|
|
687
812
|
promise.resolve(Arguments.createMap().apply {
|
|
688
813
|
putBoolean("success", false)
|
|
689
814
|
putString("error", e.message ?: "LS_ERROR")
|
|
@@ -753,6 +878,7 @@ class FileToolkitModule(private val reactContext: ReactApplicationContext) :
|
|
|
753
878
|
}
|
|
754
879
|
|
|
755
880
|
val fieldName = if (options.hasKey("fieldName")) options.getString("fieldName") else "file"
|
|
881
|
+
val uploadId = if (options.hasKey("uploadId")) options.getString("uploadId") else UUID.randomUUID().toString()
|
|
756
882
|
val headersMap = options.getMap("headers")
|
|
757
883
|
val paramsMap = options.getMap("parameters")
|
|
758
884
|
|
|
@@ -778,6 +904,7 @@ class FileToolkitModule(private val reactContext: ReactApplicationContext) :
|
|
|
778
904
|
connection.requestMethod = "POST"
|
|
779
905
|
connection.setRequestProperty("Connection", "Keep-Alive")
|
|
780
906
|
connection.setRequestProperty("Content-Type", "multipart/form-data; boundary=$boundary")
|
|
907
|
+
connection.setChunkedStreamingMode(0)
|
|
781
908
|
|
|
782
909
|
// Add custom headers
|
|
783
910
|
headersMap?.toHashMap()?.forEach { (key, value) ->
|
|
@@ -819,6 +946,7 @@ class FileToolkitModule(private val reactContext: ReactApplicationContext) :
|
|
|
819
946
|
lastProgress = progress
|
|
820
947
|
val evt = Arguments.createMap().apply {
|
|
821
948
|
putString("url", urlString)
|
|
949
|
+
putString("uploadId", uploadId)
|
|
822
950
|
putInt("progress", progress)
|
|
823
951
|
}
|
|
824
952
|
emit("onUploadProgress", evt)
|
|
@@ -845,6 +973,7 @@ class FileToolkitModule(private val reactContext: ReactApplicationContext) :
|
|
|
845
973
|
putBoolean("success", responseCode in 200..299)
|
|
846
974
|
putInt("status", responseCode)
|
|
847
975
|
putString("data", responseBody)
|
|
976
|
+
putString("uploadId", uploadId)
|
|
848
977
|
if (responseCode !in 200..299) putString("error", "HTTP $responseCode")
|
|
849
978
|
})
|
|
850
979
|
|
|
@@ -852,6 +981,7 @@ class FileToolkitModule(private val reactContext: ReactApplicationContext) :
|
|
|
852
981
|
promise.resolve(Arguments.createMap().apply {
|
|
853
982
|
putBoolean("success", false)
|
|
854
983
|
putString("error", e.message ?: "UPLOAD_ERROR")
|
|
984
|
+
putString("uploadId", uploadId)
|
|
855
985
|
})
|
|
856
986
|
}
|
|
857
987
|
}
|
|
@@ -897,7 +1027,7 @@ class FileToolkitModule(private val reactContext: ReactApplicationContext) :
|
|
|
897
1027
|
putString("filePath", destFile.absolutePath)
|
|
898
1028
|
})
|
|
899
1029
|
|
|
900
|
-
} catch (e:
|
|
1030
|
+
} catch (e: Throwable) {
|
|
901
1031
|
promise.resolve(Arguments.createMap().apply {
|
|
902
1032
|
putBoolean("success", false)
|
|
903
1033
|
putString("error", e.message ?: "BASE64_SAVE_ERROR")
|
|
@@ -958,7 +1088,7 @@ class FileToolkitModule(private val reactContext: ReactApplicationContext) :
|
|
|
958
1088
|
putString("dataUri", "data:$mimeType;base64,$base64String")
|
|
959
1089
|
})
|
|
960
1090
|
|
|
961
|
-
} catch (e:
|
|
1091
|
+
} catch (e: Throwable) {
|
|
962
1092
|
promise.resolve(Arguments.createMap().apply {
|
|
963
1093
|
putBoolean("success", false)
|
|
964
1094
|
putString("error", e.message ?: "URL_TO_BASE64_ERROR")
|