rn-file-toolkit 1.0.8 → 1.0.10

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.
@@ -17,6 +17,7 @@ Pod::Spec.new do |s|
17
17
  s.private_header_files = "ios/**/*.h"
18
18
 
19
19
  s.libraries = "z"
20
+ s.frameworks = "Photos"
20
21
 
21
22
  install_modules_dependencies(s)
22
23
  end
package/README.md CHANGED
@@ -32,6 +32,12 @@
32
32
  - [Zip & Unzip Archives](#zip--unzip-archives)
33
33
  - [Cache Management](#cache-management)
34
34
  - [Media & Utilities](#media--utilities)
35
+ - [Disk Space](#disk-space)
36
+ - [File Appending](#file-appending)
37
+ - [File Hashing](#file-hashing)
38
+ - [Session Management](#session-management)
39
+ - [Cookie Management](#cookie-management)
40
+ - [MediaStore / Photos Library](#mediastore--photos-library)
35
41
  - [Event Listeners](#event-listeners)
36
42
  - [API Reference](#-api-reference)
37
43
  - [Expo Support](#-expo-support)
@@ -50,7 +56,10 @@ Most React Native file solutions (`rn-fetch-blob`, `react-native-fs`) are fragme
50
56
  - 🚦 **Smart Queueing:** Cap concurrency and set priorities without touching native code.
51
57
  - 🛡️ **Resilient:** Auto-retries on network errors with exponential backoff and HTTP resume.
52
58
  - 🗜️ **Zero-Dependency Zip:** Uses native `java.util.zip` and iOS `zlib`.
53
- - 🗄️ **Rich File System API:** Comprehensive FS methods (`readFile`, `writeFile`, `copyFile`, `mkdir`, `stat`, etc.).
59
+ - 🗄️ **Rich File System API:** Comprehensive FS methods (`readFile`, `writeFile`, `appendFile`, `copyFile`, `mkdir`, `stat`, `hash`, `df`, etc.).
60
+ - 🍪 **Cookie Management:** Read and clear HTTP cookies from the platform's shared cookie store.
61
+ - 📸 **MediaStore / Photos Library:** Save files directly to the device's shared media store.
62
+ - 📦 **Session Management:** Group files into named sessions for batch cleanup.
54
63
  - 🛠️ **Expo Compatible:** Seamless integration with Expo custom dev clients.
55
64
 
56
65
  ---
@@ -401,6 +410,122 @@ unsub3();
401
410
  unsub4();
402
411
  ```
403
412
 
413
+ ### Disk Space
414
+
415
+ Check available and total device storage.
416
+
417
+ ```typescript
418
+ import { df } from 'rn-file-toolkit';
419
+ // or: import { fs } from 'rn-file-toolkit'; const result = await fs.df();
420
+
421
+ const space = await df();
422
+ if (space.success) {
423
+ console.log(`Free: ${(space.freeBytes! / 1024 / 1024 / 1024).toFixed(2)} GB`);
424
+ console.log(`Total: ${(space.totalBytes! / 1024 / 1024 / 1024).toFixed(2)} GB`);
425
+ }
426
+ ```
427
+
428
+ ### File Appending
429
+
430
+ Append data to the end of a file without overwriting existing content.
431
+
432
+ ```typescript
433
+ import { appendFile } from 'rn-file-toolkit';
434
+
435
+ // Append a log line
436
+ await appendFile('/path/to/log.txt', 'New log entry\n');
437
+
438
+ // Append base64 data
439
+ await appendFile('/path/to/data.bin', base64String, 'base64');
440
+ ```
441
+
442
+ ### File Hashing
443
+
444
+ Compute the MD5, SHA-1, or SHA-256 hash of any file on disk.
445
+
446
+ ```typescript
447
+ import { hash } from 'rn-file-toolkit';
448
+
449
+ const result = await hash('/path/to/file.zip', 'sha256');
450
+ if (result.success) {
451
+ console.log('SHA-256:', result.hash);
452
+ }
453
+ ```
454
+
455
+ ### Session Management
456
+
457
+ Group downloaded files into sessions for batch cleanup. Useful for temporary file workflows.
458
+
459
+ ```typescript
460
+ import { session, download } from 'rn-file-toolkit';
461
+
462
+ // Download files and track them in a session
463
+ const result = await download({ url: 'https://example.com/tmp1.pdf', destination: 'cache' });
464
+ if (result.filePath) session.add('my-workflow', result.filePath);
465
+
466
+ const result2 = await download({ url: 'https://example.com/tmp2.pdf', destination: 'cache' });
467
+ if (result2.filePath) session.add('my-workflow', result2.filePath);
468
+
469
+ // List session files
470
+ console.log(session.get('my-workflow')); // ['/path/to/tmp1.pdf', '/path/to/tmp2.pdf']
471
+
472
+ // Clean up everything when done
473
+ await session.clear('my-workflow');
474
+ ```
475
+
476
+ > **Note:** Session data lives in memory and does not persist across app restarts.
477
+
478
+ ### Cookie Management
479
+
480
+ Read and clear HTTP cookies from the platform's shared cookie store.
481
+
482
+ ```typescript
483
+ import { getCookies, clearCookies } from 'rn-file-toolkit';
484
+ // or: import { cookies } from 'rn-file-toolkit';
485
+
486
+ // Get cookies for a domain
487
+ const result = await getCookies('example.com');
488
+ result.cookies?.forEach((c) => {
489
+ console.log(`${c.name}=${c.value} (domain: ${c.domain})`);
490
+ });
491
+
492
+ // Clear cookies for a specific domain
493
+ await clearCookies('example.com');
494
+
495
+ // Clear ALL cookies
496
+ await clearCookies();
497
+ ```
498
+
499
+ ### MediaStore / Photos Library
500
+
501
+ Save files to the device's shared media store (Android MediaStore / iOS Photos Library).
502
+
503
+ ```typescript
504
+ import { saveToMediaStore } from 'rn-file-toolkit';
505
+
506
+ // Save an image to the Photos library / gallery
507
+ const result = await saveToMediaStore({
508
+ filePath: '/path/to/photo.jpg',
509
+ mediaType: 'image',
510
+ album: 'MyApp', // Optional album/subfolder
511
+ });
512
+ console.log(result.uri); // content://... (Android) or file path (iOS)
513
+
514
+ // Save a video
515
+ await saveToMediaStore({
516
+ filePath: '/path/to/video.mp4',
517
+ mediaType: 'video',
518
+ });
519
+
520
+ // Save to the Downloads folder
521
+ await saveToMediaStore({
522
+ filePath: '/path/to/report.pdf',
523
+ mediaType: 'download',
524
+ });
525
+ ```
526
+
527
+ > **Permissions:** iOS requires `NSPhotoLibraryAddUsageDescription` in your `Info.plist` for image/video saves. Android may require `WRITE_EXTERNAL_STORAGE` on API < 29.
528
+
404
529
  ---
405
530
 
406
531
  ## 📚 API Reference
@@ -416,7 +541,7 @@ unsub4();
416
541
  | `UploadResult` | `success`, `status`, `data`, `uploadId`, `error` | Result returned after an upload completes. |
417
542
  | `ActionResult` | `success`, `error` | Generic result for actions like pause/resume/cancel. |
418
543
  | `UseDownloadReturn` | `start`, `pause`, `resume`, `cancel`, `status`, `progress`, `result`, `downloadId` | Hook state and control methods. |
419
- | `FsApi` | `exists`, `stat`, `readFile`, `writeFile`, `copyFile`, `moveFile`, `deleteFile`, `mkdir`, `ls` | Namespaced filesystem API. |
544
+ | `FsApi` | `exists`, `stat`, `readFile`, `writeFile`, `appendFile`, `copyFile`, `moveFile`, `deleteFile`, `mkdir`, `ls`, `df`, `hash` | Namespaced filesystem API. |
420
545
  | `FsStat` | `path`, `name`, `size`, `modified`, `isDir` | Output of the filesystem `stat` method. |
421
546
  | `FsEncoding` | `'utf8'` \| `'base64'` | Encoding used for read/write operations. |
422
547
  | `QueueOptions` | `maxConcurrent` | Configuration for the download queue. |
@@ -466,7 +591,28 @@ unsub4();
466
591
  | `onDownloadRetry` | `(cb) => () => void` | Subscribe to download retry events. |
467
592
  | `onUploadProgress` | `(cb) => () => void` | Subscribe to upload progress events. |
468
593
  | `useDownload` | `() => UseDownloadReturn` | React hook for managing a download with state. |
469
- | `fs` | `FsApi` | Namespaced object grouping all filesystem methods. |
594
+ | `df` | `() => Promise<DiskSpaceResult>` | Get free and total device disk space. |
595
+ | `appendFile` | `(path: string, data: string, encoding?: FsEncoding) => Promise<void>` | Append data to a file. |
596
+ | `hash` | `(path: string, algorithm?: HashAlgorithm) => Promise<HashResult>` | Compute a file's hash digest. |
597
+ | `getCookies` | `(domain: string) => Promise<CookiesResult>` | Get cookies for a domain. |
598
+ | `clearCookies` | `(domain?: string) => Promise<ActionResult>` | Clear cookies (domain or all). |
599
+ | `saveToMediaStore` | `(options: MediaStoreOptions) => Promise<MediaStoreResult>` | Save file to shared media store. |
600
+ | `fs` | `FsApi` | Namespaced object grouping all filesystem methods (includes `df`, `appendFile`, `hash`). |
601
+ | `cookies` | `{ get, clear }` | Namespaced cookie management. |
602
+ | `session` | `SessionApi` | Namespaced session management. |
603
+
604
+ ### New Types & Interfaces
605
+
606
+ | Interface | Key Properties | Description |
607
+ | :--- | :--- | :--- |
608
+ | `DiskSpaceResult` | `success`, `freeBytes`, `totalBytes`, `error` | Result of `df()`. |
609
+ | `HashAlgorithm` | `'md5'` \| `'sha1'` \| `'sha256'` | Algorithm for file hashing. |
610
+ | `HashResult` | `success`, `hash`, `error` | Result of `hash()`. |
611
+ | `Cookie` | `name`, `value`, `domain`, `path`, `expiresDate`, `isSecure`, `isHTTPOnly` | A single cookie entry. |
612
+ | `CookiesResult` | `success`, `cookies`, `error` | Result of `getCookies()`. |
613
+ | `MediaStoreOptions` | `filePath`, `mediaType`, `album` | Options for saving to the media store. |
614
+ | `MediaStoreResult` | `success`, `uri`, `error` | Result of `saveToMediaStore()`. |
615
+ | `SessionApi` | `add`, `get`, `clear`, `clearAll` | Session management methods. |
470
616
 
471
617
  ---
472
618
 
@@ -99,11 +99,17 @@ class FileToolkitModule(private val reactContext: ReactApplicationContext) :
99
99
 
100
100
  private fun getDestinationFile(fileName: String, destination: String?): File {
101
101
  return when (destination) {
102
- "cache" -> File(reactContext.cacheDir, fileName)
102
+ "cache" -> {
103
+ val dir = File(reactContext.cacheDir, "RNFileToolkit")
104
+ dir.mkdirs()
105
+ File(dir, fileName)
106
+ }
103
107
  "documents" -> {
104
108
  // getExternalFilesDir can return null if external storage is unavailable
105
- val dir = reactContext.getExternalFilesDir(Environment.DIRECTORY_DOCUMENTS)
109
+ val baseDir = reactContext.getExternalFilesDir(Environment.DIRECTORY_DOCUMENTS)
106
110
  ?: reactContext.filesDir
111
+ val dir = File(baseDir, "RNFileToolkit")
112
+ dir.mkdirs()
107
113
  File(dir, fileName)
108
114
  }
109
115
  else -> File(
@@ -531,8 +537,11 @@ class FileToolkitModule(private val reactContext: ReactApplicationContext) :
531
537
  // Scan all three directories: public downloads, cache, documents
532
538
  val dirs = listOfNotNull(
533
539
  Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS),
534
- reactContext.cacheDir,
535
- reactContext.getExternalFilesDir(Environment.DIRECTORY_DOCUMENTS) ?: reactContext.filesDir
540
+ File(reactContext.cacheDir, "RNFileToolkit"),
541
+ File(
542
+ reactContext.getExternalFilesDir(Environment.DIRECTORY_DOCUMENTS) ?: reactContext.filesDir,
543
+ "RNFileToolkit"
544
+ )
536
545
  )
537
546
 
538
547
  for (dir in dirs) {
@@ -573,13 +582,16 @@ class FileToolkitModule(private val reactContext: ReactApplicationContext) :
573
582
 
574
583
  override fun clearCache(promise: Promise) {
575
584
  try {
576
- // Only clear app-private directories — never touch public Downloads
585
+ // Only clear the toolkit-owned subdirectory — never touch the app's own files or public Downloads
577
586
  val dirs = listOfNotNull(
578
- reactContext.cacheDir,
579
- reactContext.getExternalFilesDir(Environment.DIRECTORY_DOCUMENTS) ?: reactContext.filesDir
587
+ File(reactContext.cacheDir, "RNFileToolkit"),
588
+ File(
589
+ reactContext.getExternalFilesDir(Environment.DIRECTORY_DOCUMENTS) ?: reactContext.filesDir,
590
+ "RNFileToolkit"
591
+ )
580
592
  )
581
593
  for (dir in dirs) {
582
- dir.listFiles()?.forEach { it.delete() }
594
+ dir.listFiles()?.forEach { it.deleteRecursively() }
583
595
  }
584
596
  promise.resolve(Arguments.createMap().apply { putBoolean("success", true) })
585
597
  } catch (e: Exception) {
@@ -1327,6 +1339,279 @@ class FileToolkitModule(private val reactContext: ReactApplicationContext) :
1327
1339
  zos.closeEntry()
1328
1340
  }
1329
1341
 
1342
+ // ─── df (disk space) ──────────────────────────────────────────────────────
1343
+
1344
+ override fun df(promise: Promise) {
1345
+ try {
1346
+ val stat = android.os.StatFs(Environment.getDataDirectory().path)
1347
+ val freeBytes = stat.availableBytes
1348
+ val totalBytes = stat.totalBytes
1349
+
1350
+ promise.resolve(Arguments.createMap().apply {
1351
+ putBoolean("success", true)
1352
+ putDouble("freeBytes", freeBytes.toDouble())
1353
+ putDouble("totalBytes", totalBytes.toDouble())
1354
+ })
1355
+ } catch (e: Throwable) {
1356
+ promise.resolve(Arguments.createMap().apply {
1357
+ putBoolean("success", false)
1358
+ putString("error", e.message ?: "DF_ERROR")
1359
+ })
1360
+ }
1361
+ }
1362
+
1363
+ // ─── appendFile ───────────────────────────────────────────────────────────
1364
+
1365
+ override fun appendFile(filePath: String, data: String, encoding: String, promise: Promise) {
1366
+ try {
1367
+ val file = File(filePath)
1368
+ if (!file.exists()) {
1369
+ // Create parent dirs and the file if it doesn't exist
1370
+ file.parentFile?.mkdirs()
1371
+ }
1372
+
1373
+ if (encoding.equals("base64", ignoreCase = true)) {
1374
+ val bytes = android.util.Base64.decode(data, android.util.Base64.DEFAULT)
1375
+ FileOutputStream(file, true).use { fos ->
1376
+ fos.write(bytes)
1377
+ }
1378
+ } else {
1379
+ FileOutputStream(file, true).use { fos ->
1380
+ fos.write(data.toByteArray(Charsets.UTF_8))
1381
+ }
1382
+ }
1383
+
1384
+ promise.resolve(Arguments.createMap().apply {
1385
+ putBoolean("success", true)
1386
+ })
1387
+ } catch (e: Throwable) {
1388
+ promise.resolve(Arguments.createMap().apply {
1389
+ putBoolean("success", false)
1390
+ putString("error", e.message ?: "APPEND_FILE_ERROR")
1391
+ })
1392
+ }
1393
+ }
1394
+
1395
+ // ─── hash ─────────────────────────────────────────────────────────────────
1396
+
1397
+ override fun hash(filePath: String, algorithm: String, promise: Promise) {
1398
+ try {
1399
+ val file = File(filePath)
1400
+ if (!file.exists() || file.isDirectory) {
1401
+ promise.resolve(Arguments.createMap().apply {
1402
+ putBoolean("success", false)
1403
+ putString("error", "File not found: $filePath")
1404
+ })
1405
+ return
1406
+ }
1407
+
1408
+ val hashValue = calculateChecksum(file, algorithm)
1409
+
1410
+ promise.resolve(Arguments.createMap().apply {
1411
+ putBoolean("success", true)
1412
+ putString("hash", hashValue)
1413
+ })
1414
+ } catch (e: Throwable) {
1415
+ promise.resolve(Arguments.createMap().apply {
1416
+ putBoolean("success", false)
1417
+ putString("error", e.message ?: "HASH_ERROR")
1418
+ })
1419
+ }
1420
+ }
1421
+
1422
+ // ─── getCookies ───────────────────────────────────────────────────────────
1423
+
1424
+ override fun getCookies(domain: String, promise: Promise) {
1425
+ try {
1426
+ val cookieManager = android.webkit.CookieManager.getInstance()
1427
+ val cookieString = cookieManager.getCookie(domain)
1428
+
1429
+ val cookiesArray = Arguments.createArray()
1430
+
1431
+ if (!cookieString.isNullOrBlank()) {
1432
+ // CookieManager returns cookies as "name1=value1; name2=value2; ..."
1433
+ cookieString.split(";").forEach { pair ->
1434
+ val trimmed = pair.trim()
1435
+ val eqIndex = trimmed.indexOf("=")
1436
+ if (eqIndex > 0) {
1437
+ val name = trimmed.substring(0, eqIndex)
1438
+ val value = trimmed.substring(eqIndex + 1)
1439
+ cookiesArray.pushMap(Arguments.createMap().apply {
1440
+ putString("name", name)
1441
+ putString("value", value)
1442
+ putString("domain", domain)
1443
+ putString("path", "/")
1444
+ })
1445
+ }
1446
+ }
1447
+ }
1448
+
1449
+ promise.resolve(Arguments.createMap().apply {
1450
+ putBoolean("success", true)
1451
+ putArray("cookies", cookiesArray)
1452
+ })
1453
+ } catch (e: Throwable) {
1454
+ promise.resolve(Arguments.createMap().apply {
1455
+ putBoolean("success", false)
1456
+ putString("error", e.message ?: "GET_COOKIES_ERROR")
1457
+ })
1458
+ }
1459
+ }
1460
+
1461
+ // ─── clearCookies ─────────────────────────────────────────────────────────
1462
+
1463
+ override fun clearCookies(domain: String, promise: Promise) {
1464
+ try {
1465
+ val cookieManager = android.webkit.CookieManager.getInstance()
1466
+
1467
+ if (domain.isBlank()) {
1468
+ // Clear ALL cookies
1469
+ cookieManager.removeAllCookies { success ->
1470
+ promise.resolve(Arguments.createMap().apply {
1471
+ putBoolean("success", success)
1472
+ })
1473
+ }
1474
+ } else {
1475
+ // Android CookieManager does not support per-domain clearing directly.
1476
+ // We read cookies for the domain and set each to expired.
1477
+ val cookieString = cookieManager.getCookie(domain)
1478
+ if (!cookieString.isNullOrBlank()) {
1479
+ cookieString.split(";").forEach { pair ->
1480
+ val trimmed = pair.trim()
1481
+ val eqIndex = trimmed.indexOf("=")
1482
+ if (eqIndex > 0) {
1483
+ val name = trimmed.substring(0, eqIndex)
1484
+ cookieManager.setCookie(domain, "$name=; Expires=Thu, 01 Jan 1970 00:00:00 GMT")
1485
+ }
1486
+ }
1487
+ cookieManager.flush()
1488
+ }
1489
+ promise.resolve(Arguments.createMap().apply {
1490
+ putBoolean("success", true)
1491
+ })
1492
+ }
1493
+ } catch (e: Throwable) {
1494
+ promise.resolve(Arguments.createMap().apply {
1495
+ putBoolean("success", false)
1496
+ putString("error", e.message ?: "CLEAR_COOKIES_ERROR")
1497
+ })
1498
+ }
1499
+ }
1500
+
1501
+ // ─── saveToMediaStore ─────────────────────────────────────────────────────
1502
+
1503
+ override fun saveToMediaStore(options: ReadableMap, promise: Promise) {
1504
+ val filePath = options.getString("filePath")
1505
+ if (filePath.isNullOrBlank()) {
1506
+ promise.resolve(Arguments.createMap().apply {
1507
+ putBoolean("success", false)
1508
+ putString("error", "filePath is required")
1509
+ })
1510
+ return
1511
+ }
1512
+
1513
+ val mediaType = if (options.hasKey("mediaType")) options.getString("mediaType") else "download"
1514
+ val album = if (options.hasKey("album")) options.getString("album") else null
1515
+
1516
+ thread {
1517
+ try {
1518
+ val file = File(filePath)
1519
+ if (!file.exists()) {
1520
+ promise.resolve(Arguments.createMap().apply {
1521
+ putBoolean("success", false)
1522
+ putString("error", "File not found: $filePath")
1523
+ })
1524
+ return@thread
1525
+ }
1526
+
1527
+ val mimeType = getMimeType(filePath)
1528
+
1529
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
1530
+ // Android 10+ — use MediaStore ContentResolver API
1531
+ val contentUri = when (mediaType) {
1532
+ "image" -> android.provider.MediaStore.Images.Media.getContentUri(android.provider.MediaStore.VOLUME_EXTERNAL_PRIMARY)
1533
+ "video" -> android.provider.MediaStore.Video.Media.getContentUri(android.provider.MediaStore.VOLUME_EXTERNAL_PRIMARY)
1534
+ "audio" -> android.provider.MediaStore.Audio.Media.getContentUri(android.provider.MediaStore.VOLUME_EXTERNAL_PRIMARY)
1535
+ else -> android.provider.MediaStore.Downloads.getContentUri(android.provider.MediaStore.VOLUME_EXTERNAL_PRIMARY)
1536
+ }
1537
+
1538
+ val relativePath = when (mediaType) {
1539
+ "image" -> if (album != null) "${Environment.DIRECTORY_PICTURES}/$album" else Environment.DIRECTORY_PICTURES
1540
+ "video" -> if (album != null) "${Environment.DIRECTORY_MOVIES}/$album" else Environment.DIRECTORY_MOVIES
1541
+ "audio" -> if (album != null) "${Environment.DIRECTORY_MUSIC}/$album" else Environment.DIRECTORY_MUSIC
1542
+ else -> if (album != null) "${Environment.DIRECTORY_DOWNLOADS}/$album" else Environment.DIRECTORY_DOWNLOADS
1543
+ }
1544
+
1545
+ val values = android.content.ContentValues().apply {
1546
+ put(android.provider.MediaStore.MediaColumns.DISPLAY_NAME, file.name)
1547
+ put(android.provider.MediaStore.MediaColumns.MIME_TYPE, mimeType)
1548
+ put(android.provider.MediaStore.MediaColumns.RELATIVE_PATH, relativePath)
1549
+ put(android.provider.MediaStore.MediaColumns.IS_PENDING, 1)
1550
+ }
1551
+
1552
+ val resolver = reactContext.contentResolver
1553
+ val uri = resolver.insert(contentUri, values)
1554
+
1555
+ if (uri == null) {
1556
+ promise.resolve(Arguments.createMap().apply {
1557
+ putBoolean("success", false)
1558
+ putString("error", "Failed to create MediaStore entry")
1559
+ })
1560
+ return@thread
1561
+ }
1562
+
1563
+ resolver.openOutputStream(uri)?.use { outputStream ->
1564
+ FileInputStream(file).use { inputStream ->
1565
+ val buffer = ByteArray(8192)
1566
+ var count: Int
1567
+ while (inputStream.read(buffer).also { count = it } != -1) {
1568
+ outputStream.write(buffer, 0, count)
1569
+ }
1570
+ }
1571
+ }
1572
+
1573
+ values.clear()
1574
+ values.put(android.provider.MediaStore.MediaColumns.IS_PENDING, 0)
1575
+ resolver.update(uri, values, null, null)
1576
+
1577
+ promise.resolve(Arguments.createMap().apply {
1578
+ putBoolean("success", true)
1579
+ putString("uri", uri.toString())
1580
+ })
1581
+ } else {
1582
+ // Android 9 and below — copy to public directory and media-scan
1583
+ val destDir = when (mediaType) {
1584
+ "image" -> Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES)
1585
+ "video" -> Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MOVIES)
1586
+ "audio" -> Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MUSIC)
1587
+ else -> Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
1588
+ }
1589
+
1590
+ val albumDir = if (album != null) File(destDir, album) else destDir
1591
+ albumDir.mkdirs()
1592
+ val destFile = File(albumDir, file.name)
1593
+ file.copyTo(destFile, overwrite = true)
1594
+
1595
+ android.media.MediaScannerConnection.scanFile(
1596
+ reactContext,
1597
+ arrayOf(destFile.absolutePath),
1598
+ arrayOf(mimeType)
1599
+ ) { _, scannedUri ->
1600
+ promise.resolve(Arguments.createMap().apply {
1601
+ putBoolean("success", true)
1602
+ putString("uri", scannedUri?.toString() ?: destFile.absolutePath)
1603
+ })
1604
+ }
1605
+ }
1606
+ } catch (e: Exception) {
1607
+ promise.resolve(Arguments.createMap().apply {
1608
+ putBoolean("success", false)
1609
+ putString("error", e.message ?: "MEDIA_STORE_ERROR")
1610
+ })
1611
+ }
1612
+ }
1613
+ }
1614
+
1330
1615
  companion object {
1331
1616
  const val NAME = NativeFileToolkitSpec.NAME
1332
1617
  }