rn-file-toolkit 1.0.8 → 1.0.9
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 +1 -0
- package/README.md +149 -3
- package/android/src/main/java/com/filetoolkit/FileToolkitModule.kt +273 -0
- package/ios/FileToolkit.mm +220 -0
- package/lib/commonjs/NativeFileToolkit.js.map +1 -1
- package/lib/commonjs/index.js +137 -3
- package/lib/commonjs/index.js.map +1 -1
- package/lib/module/NativeFileToolkit.js.map +1 -1
- package/lib/module/index.js +129 -3
- package/lib/module/index.js.map +1 -1
- package/lib/typescript/commonjs/src/NativeFileToolkit.d.ts +6 -0
- package/lib/typescript/commonjs/src/NativeFileToolkit.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/index.d.ts +67 -0
- package/lib/typescript/commonjs/src/index.d.ts.map +1 -1
- package/lib/typescript/module/src/NativeFileToolkit.d.ts +6 -0
- package/lib/typescript/module/src/NativeFileToolkit.d.ts.map +1 -1
- package/lib/typescript/module/src/index.d.ts +67 -0
- package/lib/typescript/module/src/index.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/NativeFileToolkit.ts +6 -0
- package/src/index.tsx +183 -2
package/FileToolkit.podspec
CHANGED
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
|
-
| `
|
|
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
|
|
|
@@ -1327,6 +1327,279 @@ class FileToolkitModule(private val reactContext: ReactApplicationContext) :
|
|
|
1327
1327
|
zos.closeEntry()
|
|
1328
1328
|
}
|
|
1329
1329
|
|
|
1330
|
+
// ─── df (disk space) ──────────────────────────────────────────────────────
|
|
1331
|
+
|
|
1332
|
+
override fun df(promise: Promise) {
|
|
1333
|
+
try {
|
|
1334
|
+
val stat = android.os.StatFs(Environment.getDataDirectory().path)
|
|
1335
|
+
val freeBytes = stat.availableBytes
|
|
1336
|
+
val totalBytes = stat.totalBytes
|
|
1337
|
+
|
|
1338
|
+
promise.resolve(Arguments.createMap().apply {
|
|
1339
|
+
putBoolean("success", true)
|
|
1340
|
+
putDouble("freeBytes", freeBytes.toDouble())
|
|
1341
|
+
putDouble("totalBytes", totalBytes.toDouble())
|
|
1342
|
+
})
|
|
1343
|
+
} catch (e: Throwable) {
|
|
1344
|
+
promise.resolve(Arguments.createMap().apply {
|
|
1345
|
+
putBoolean("success", false)
|
|
1346
|
+
putString("error", e.message ?: "DF_ERROR")
|
|
1347
|
+
})
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
// ─── appendFile ───────────────────────────────────────────────────────────
|
|
1352
|
+
|
|
1353
|
+
override fun appendFile(filePath: String, data: String, encoding: String, promise: Promise) {
|
|
1354
|
+
try {
|
|
1355
|
+
val file = File(filePath)
|
|
1356
|
+
if (!file.exists()) {
|
|
1357
|
+
// Create parent dirs and the file if it doesn't exist
|
|
1358
|
+
file.parentFile?.mkdirs()
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
if (encoding.equals("base64", ignoreCase = true)) {
|
|
1362
|
+
val bytes = android.util.Base64.decode(data, android.util.Base64.DEFAULT)
|
|
1363
|
+
FileOutputStream(file, true).use { fos ->
|
|
1364
|
+
fos.write(bytes)
|
|
1365
|
+
}
|
|
1366
|
+
} else {
|
|
1367
|
+
FileOutputStream(file, true).use { fos ->
|
|
1368
|
+
fos.write(data.toByteArray(Charsets.UTF_8))
|
|
1369
|
+
}
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1372
|
+
promise.resolve(Arguments.createMap().apply {
|
|
1373
|
+
putBoolean("success", true)
|
|
1374
|
+
})
|
|
1375
|
+
} catch (e: Throwable) {
|
|
1376
|
+
promise.resolve(Arguments.createMap().apply {
|
|
1377
|
+
putBoolean("success", false)
|
|
1378
|
+
putString("error", e.message ?: "APPEND_FILE_ERROR")
|
|
1379
|
+
})
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
// ─── hash ─────────────────────────────────────────────────────────────────
|
|
1384
|
+
|
|
1385
|
+
override fun hash(filePath: String, algorithm: String, promise: Promise) {
|
|
1386
|
+
try {
|
|
1387
|
+
val file = File(filePath)
|
|
1388
|
+
if (!file.exists() || file.isDirectory) {
|
|
1389
|
+
promise.resolve(Arguments.createMap().apply {
|
|
1390
|
+
putBoolean("success", false)
|
|
1391
|
+
putString("error", "File not found: $filePath")
|
|
1392
|
+
})
|
|
1393
|
+
return
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1396
|
+
val hashValue = calculateChecksum(file, algorithm)
|
|
1397
|
+
|
|
1398
|
+
promise.resolve(Arguments.createMap().apply {
|
|
1399
|
+
putBoolean("success", true)
|
|
1400
|
+
putString("hash", hashValue)
|
|
1401
|
+
})
|
|
1402
|
+
} catch (e: Throwable) {
|
|
1403
|
+
promise.resolve(Arguments.createMap().apply {
|
|
1404
|
+
putBoolean("success", false)
|
|
1405
|
+
putString("error", e.message ?: "HASH_ERROR")
|
|
1406
|
+
})
|
|
1407
|
+
}
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1410
|
+
// ─── getCookies ───────────────────────────────────────────────────────────
|
|
1411
|
+
|
|
1412
|
+
override fun getCookies(domain: String, promise: Promise) {
|
|
1413
|
+
try {
|
|
1414
|
+
val cookieManager = android.webkit.CookieManager.getInstance()
|
|
1415
|
+
val cookieString = cookieManager.getCookie(domain)
|
|
1416
|
+
|
|
1417
|
+
val cookiesArray = Arguments.createArray()
|
|
1418
|
+
|
|
1419
|
+
if (!cookieString.isNullOrBlank()) {
|
|
1420
|
+
// CookieManager returns cookies as "name1=value1; name2=value2; ..."
|
|
1421
|
+
cookieString.split(";").forEach { pair ->
|
|
1422
|
+
val trimmed = pair.trim()
|
|
1423
|
+
val eqIndex = trimmed.indexOf("=")
|
|
1424
|
+
if (eqIndex > 0) {
|
|
1425
|
+
val name = trimmed.substring(0, eqIndex)
|
|
1426
|
+
val value = trimmed.substring(eqIndex + 1)
|
|
1427
|
+
cookiesArray.pushMap(Arguments.createMap().apply {
|
|
1428
|
+
putString("name", name)
|
|
1429
|
+
putString("value", value)
|
|
1430
|
+
putString("domain", domain)
|
|
1431
|
+
putString("path", "/")
|
|
1432
|
+
})
|
|
1433
|
+
}
|
|
1434
|
+
}
|
|
1435
|
+
}
|
|
1436
|
+
|
|
1437
|
+
promise.resolve(Arguments.createMap().apply {
|
|
1438
|
+
putBoolean("success", true)
|
|
1439
|
+
putArray("cookies", cookiesArray)
|
|
1440
|
+
})
|
|
1441
|
+
} catch (e: Throwable) {
|
|
1442
|
+
promise.resolve(Arguments.createMap().apply {
|
|
1443
|
+
putBoolean("success", false)
|
|
1444
|
+
putString("error", e.message ?: "GET_COOKIES_ERROR")
|
|
1445
|
+
})
|
|
1446
|
+
}
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
// ─── clearCookies ─────────────────────────────────────────────────────────
|
|
1450
|
+
|
|
1451
|
+
override fun clearCookies(domain: String, promise: Promise) {
|
|
1452
|
+
try {
|
|
1453
|
+
val cookieManager = android.webkit.CookieManager.getInstance()
|
|
1454
|
+
|
|
1455
|
+
if (domain.isBlank()) {
|
|
1456
|
+
// Clear ALL cookies
|
|
1457
|
+
cookieManager.removeAllCookies { success ->
|
|
1458
|
+
promise.resolve(Arguments.createMap().apply {
|
|
1459
|
+
putBoolean("success", success)
|
|
1460
|
+
})
|
|
1461
|
+
}
|
|
1462
|
+
} else {
|
|
1463
|
+
// Android CookieManager does not support per-domain clearing directly.
|
|
1464
|
+
// We read cookies for the domain and set each to expired.
|
|
1465
|
+
val cookieString = cookieManager.getCookie(domain)
|
|
1466
|
+
if (!cookieString.isNullOrBlank()) {
|
|
1467
|
+
cookieString.split(";").forEach { pair ->
|
|
1468
|
+
val trimmed = pair.trim()
|
|
1469
|
+
val eqIndex = trimmed.indexOf("=")
|
|
1470
|
+
if (eqIndex > 0) {
|
|
1471
|
+
val name = trimmed.substring(0, eqIndex)
|
|
1472
|
+
cookieManager.setCookie(domain, "$name=; Expires=Thu, 01 Jan 1970 00:00:00 GMT")
|
|
1473
|
+
}
|
|
1474
|
+
}
|
|
1475
|
+
cookieManager.flush()
|
|
1476
|
+
}
|
|
1477
|
+
promise.resolve(Arguments.createMap().apply {
|
|
1478
|
+
putBoolean("success", true)
|
|
1479
|
+
})
|
|
1480
|
+
}
|
|
1481
|
+
} catch (e: Throwable) {
|
|
1482
|
+
promise.resolve(Arguments.createMap().apply {
|
|
1483
|
+
putBoolean("success", false)
|
|
1484
|
+
putString("error", e.message ?: "CLEAR_COOKIES_ERROR")
|
|
1485
|
+
})
|
|
1486
|
+
}
|
|
1487
|
+
}
|
|
1488
|
+
|
|
1489
|
+
// ─── saveToMediaStore ─────────────────────────────────────────────────────
|
|
1490
|
+
|
|
1491
|
+
override fun saveToMediaStore(options: ReadableMap, promise: Promise) {
|
|
1492
|
+
val filePath = options.getString("filePath")
|
|
1493
|
+
if (filePath.isNullOrBlank()) {
|
|
1494
|
+
promise.resolve(Arguments.createMap().apply {
|
|
1495
|
+
putBoolean("success", false)
|
|
1496
|
+
putString("error", "filePath is required")
|
|
1497
|
+
})
|
|
1498
|
+
return
|
|
1499
|
+
}
|
|
1500
|
+
|
|
1501
|
+
val mediaType = if (options.hasKey("mediaType")) options.getString("mediaType") else "download"
|
|
1502
|
+
val album = if (options.hasKey("album")) options.getString("album") else null
|
|
1503
|
+
|
|
1504
|
+
thread {
|
|
1505
|
+
try {
|
|
1506
|
+
val file = File(filePath)
|
|
1507
|
+
if (!file.exists()) {
|
|
1508
|
+
promise.resolve(Arguments.createMap().apply {
|
|
1509
|
+
putBoolean("success", false)
|
|
1510
|
+
putString("error", "File not found: $filePath")
|
|
1511
|
+
})
|
|
1512
|
+
return@thread
|
|
1513
|
+
}
|
|
1514
|
+
|
|
1515
|
+
val mimeType = getMimeType(filePath)
|
|
1516
|
+
|
|
1517
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
|
1518
|
+
// Android 10+ — use MediaStore ContentResolver API
|
|
1519
|
+
val contentUri = when (mediaType) {
|
|
1520
|
+
"image" -> android.provider.MediaStore.Images.Media.getContentUri(android.provider.MediaStore.VOLUME_EXTERNAL_PRIMARY)
|
|
1521
|
+
"video" -> android.provider.MediaStore.Video.Media.getContentUri(android.provider.MediaStore.VOLUME_EXTERNAL_PRIMARY)
|
|
1522
|
+
"audio" -> android.provider.MediaStore.Audio.Media.getContentUri(android.provider.MediaStore.VOLUME_EXTERNAL_PRIMARY)
|
|
1523
|
+
else -> android.provider.MediaStore.Downloads.getContentUri(android.provider.MediaStore.VOLUME_EXTERNAL_PRIMARY)
|
|
1524
|
+
}
|
|
1525
|
+
|
|
1526
|
+
val relativePath = when (mediaType) {
|
|
1527
|
+
"image" -> if (album != null) "${Environment.DIRECTORY_PICTURES}/$album" else Environment.DIRECTORY_PICTURES
|
|
1528
|
+
"video" -> if (album != null) "${Environment.DIRECTORY_MOVIES}/$album" else Environment.DIRECTORY_MOVIES
|
|
1529
|
+
"audio" -> if (album != null) "${Environment.DIRECTORY_MUSIC}/$album" else Environment.DIRECTORY_MUSIC
|
|
1530
|
+
else -> if (album != null) "${Environment.DIRECTORY_DOWNLOADS}/$album" else Environment.DIRECTORY_DOWNLOADS
|
|
1531
|
+
}
|
|
1532
|
+
|
|
1533
|
+
val values = android.content.ContentValues().apply {
|
|
1534
|
+
put(android.provider.MediaStore.MediaColumns.DISPLAY_NAME, file.name)
|
|
1535
|
+
put(android.provider.MediaStore.MediaColumns.MIME_TYPE, mimeType)
|
|
1536
|
+
put(android.provider.MediaStore.MediaColumns.RELATIVE_PATH, relativePath)
|
|
1537
|
+
put(android.provider.MediaStore.MediaColumns.IS_PENDING, 1)
|
|
1538
|
+
}
|
|
1539
|
+
|
|
1540
|
+
val resolver = reactContext.contentResolver
|
|
1541
|
+
val uri = resolver.insert(contentUri, values)
|
|
1542
|
+
|
|
1543
|
+
if (uri == null) {
|
|
1544
|
+
promise.resolve(Arguments.createMap().apply {
|
|
1545
|
+
putBoolean("success", false)
|
|
1546
|
+
putString("error", "Failed to create MediaStore entry")
|
|
1547
|
+
})
|
|
1548
|
+
return@thread
|
|
1549
|
+
}
|
|
1550
|
+
|
|
1551
|
+
resolver.openOutputStream(uri)?.use { outputStream ->
|
|
1552
|
+
FileInputStream(file).use { inputStream ->
|
|
1553
|
+
val buffer = ByteArray(8192)
|
|
1554
|
+
var count: Int
|
|
1555
|
+
while (inputStream.read(buffer).also { count = it } != -1) {
|
|
1556
|
+
outputStream.write(buffer, 0, count)
|
|
1557
|
+
}
|
|
1558
|
+
}
|
|
1559
|
+
}
|
|
1560
|
+
|
|
1561
|
+
values.clear()
|
|
1562
|
+
values.put(android.provider.MediaStore.MediaColumns.IS_PENDING, 0)
|
|
1563
|
+
resolver.update(uri, values, null, null)
|
|
1564
|
+
|
|
1565
|
+
promise.resolve(Arguments.createMap().apply {
|
|
1566
|
+
putBoolean("success", true)
|
|
1567
|
+
putString("uri", uri.toString())
|
|
1568
|
+
})
|
|
1569
|
+
} else {
|
|
1570
|
+
// Android 9 and below — copy to public directory and media-scan
|
|
1571
|
+
val destDir = when (mediaType) {
|
|
1572
|
+
"image" -> Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES)
|
|
1573
|
+
"video" -> Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MOVIES)
|
|
1574
|
+
"audio" -> Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MUSIC)
|
|
1575
|
+
else -> Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
|
|
1576
|
+
}
|
|
1577
|
+
|
|
1578
|
+
val albumDir = if (album != null) File(destDir, album) else destDir
|
|
1579
|
+
albumDir.mkdirs()
|
|
1580
|
+
val destFile = File(albumDir, file.name)
|
|
1581
|
+
file.copyTo(destFile, overwrite = true)
|
|
1582
|
+
|
|
1583
|
+
android.media.MediaScannerConnection.scanFile(
|
|
1584
|
+
reactContext,
|
|
1585
|
+
arrayOf(destFile.absolutePath),
|
|
1586
|
+
arrayOf(mimeType)
|
|
1587
|
+
) { _, scannedUri ->
|
|
1588
|
+
promise.resolve(Arguments.createMap().apply {
|
|
1589
|
+
putBoolean("success", true)
|
|
1590
|
+
putString("uri", scannedUri?.toString() ?: destFile.absolutePath)
|
|
1591
|
+
})
|
|
1592
|
+
}
|
|
1593
|
+
}
|
|
1594
|
+
} catch (e: Exception) {
|
|
1595
|
+
promise.resolve(Arguments.createMap().apply {
|
|
1596
|
+
putBoolean("success", false)
|
|
1597
|
+
putString("error", e.message ?: "MEDIA_STORE_ERROR")
|
|
1598
|
+
})
|
|
1599
|
+
}
|
|
1600
|
+
}
|
|
1601
|
+
}
|
|
1602
|
+
|
|
1330
1603
|
companion object {
|
|
1331
1604
|
const val NAME = NativeFileToolkitSpec.NAME
|
|
1332
1605
|
}
|