react-native-buffered-blob 1.0.0
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/android/AGENTS.md +74 -0
- package/android/build.gradle +34 -0
- package/android/src/main/AndroidManifest.xml +4 -0
- package/android/src/main/java/com/bufferedblob/BufferedBlobModule.kt +274 -0
- package/android/src/main/java/com/bufferedblob/BufferedBlobPackage.kt +32 -0
- package/android/src/main/java/com/bufferedblob/HandleRegistry.kt +84 -0
- package/android/src/main/java/com/bufferedblob/StreamingBridge.kt +211 -0
- package/cpp/AGENTS.md +71 -0
- package/cpp/AndroidPlatformBridge.cpp +437 -0
- package/cpp/AndroidPlatformBridge.h +79 -0
- package/cpp/BufferedBlobStreamingHostObject.cpp +344 -0
- package/cpp/BufferedBlobStreamingHostObject.h +118 -0
- package/cpp/CMakeLists.txt +49 -0
- package/cpp/jni_onload.cpp +32 -0
- package/ios/AGENTS.md +76 -0
- package/ios/BufferedBlobModule.h +44 -0
- package/ios/BufferedBlobModule.m +433 -0
- package/ios/BufferedBlobModule.mm +192 -0
- package/ios/BufferedBlobStreamingBridge.h +21 -0
- package/ios/BufferedBlobStreamingBridge.mm +442 -0
- package/ios/HandleRegistry.h +29 -0
- package/ios/HandleRegistry.m +67 -0
- package/ios/HandleTypes.h +83 -0
- package/ios/HandleTypes.m +333 -0
- package/lib/module/AGENTS.md +70 -0
- package/lib/module/NativeBufferedBlob.js +5 -0
- package/lib/module/NativeBufferedBlob.js.map +1 -0
- package/lib/module/api/AGENTS.md +62 -0
- package/lib/module/api/download.js +40 -0
- package/lib/module/api/download.js.map +1 -0
- package/lib/module/api/fileOps.js +70 -0
- package/lib/module/api/fileOps.js.map +1 -0
- package/lib/module/api/hash.js +13 -0
- package/lib/module/api/hash.js.map +1 -0
- package/lib/module/api/readFile.js +23 -0
- package/lib/module/api/readFile.js.map +1 -0
- package/lib/module/api/writeFile.js +18 -0
- package/lib/module/api/writeFile.js.map +1 -0
- package/lib/module/errors.js +45 -0
- package/lib/module/errors.js.map +1 -0
- package/lib/module/index.js +25 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/module.js +19 -0
- package/lib/module/module.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/module/paths.js +32 -0
- package/lib/module/paths.js.map +1 -0
- package/lib/module/types.js +15 -0
- package/lib/module/types.js.map +1 -0
- package/lib/module/wrappers.js +107 -0
- package/lib/module/wrappers.js.map +1 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/src/NativeBufferedBlob.d.ts +37 -0
- package/lib/typescript/src/NativeBufferedBlob.d.ts.map +1 -0
- package/lib/typescript/src/api/download.d.ts +13 -0
- package/lib/typescript/src/api/download.d.ts.map +1 -0
- package/lib/typescript/src/api/fileOps.d.ts +9 -0
- package/lib/typescript/src/api/fileOps.d.ts.map +1 -0
- package/lib/typescript/src/api/hash.d.ts +3 -0
- package/lib/typescript/src/api/hash.d.ts.map +1 -0
- package/lib/typescript/src/api/readFile.d.ts +3 -0
- package/lib/typescript/src/api/readFile.d.ts.map +1 -0
- package/lib/typescript/src/api/writeFile.d.ts +3 -0
- package/lib/typescript/src/api/writeFile.d.ts.map +1 -0
- package/lib/typescript/src/errors.d.ts +25 -0
- package/lib/typescript/src/errors.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +11 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/lib/typescript/src/module.d.ts +23 -0
- package/lib/typescript/src/module.d.ts.map +1 -0
- package/lib/typescript/src/paths.d.ts +11 -0
- package/lib/typescript/src/paths.d.ts.map +1 -0
- package/lib/typescript/src/types.d.ts +37 -0
- package/lib/typescript/src/types.d.ts.map +1 -0
- package/lib/typescript/src/wrappers.d.ts +14 -0
- package/lib/typescript/src/wrappers.d.ts.map +1 -0
- package/package.json +114 -0
- package/react-native-buffered-blob.podspec +37 -0
- package/react-native.config.js +10 -0
- package/src/AGENTS.md +70 -0
- package/src/NativeBufferedBlob.ts +54 -0
- package/src/api/AGENTS.md +62 -0
- package/src/api/download.ts +46 -0
- package/src/api/fileOps.ts +83 -0
- package/src/api/hash.ts +14 -0
- package/src/api/readFile.ts +37 -0
- package/src/api/writeFile.ts +24 -0
- package/src/errors.ts +50 -0
- package/src/index.ts +28 -0
- package/src/module.ts +48 -0
- package/src/paths.ts +35 -0
- package/src/types.ts +42 -0
- package/src/wrappers.ts +123 -0
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
<!-- Parent: ../AGENTS.md -->
|
|
2
|
+
<!-- Generated: 2026-02-15 -->
|
|
3
|
+
|
|
4
|
+
# android/
|
|
5
|
+
|
|
6
|
+
Android platform bridge: Kotlin Turbo Module, JNI bridge, streaming operations, and handle registry.
|
|
7
|
+
|
|
8
|
+
## Purpose
|
|
9
|
+
|
|
10
|
+
Implement BufferedBlob for Android:
|
|
11
|
+
- **BufferedBlobModule.kt**: Turbo Module class extending NativeBufferedBlobSpec; handle factories, FS operations, hashing
|
|
12
|
+
- **BufferedBlobPackage.kt**: TurboReactPackage provider for React Native
|
|
13
|
+
- **HandleRegistry.kt**: Thread-safe handle storage (ConcurrentHashMap), numeric IDs via AtomicInteger
|
|
14
|
+
- **StreamingBridge.kt**: Static JNI-callable methods for read/write/flush/download; OkHttp enqueue() for async downloads
|
|
15
|
+
- **build.gradle**: CMake linking, OkHttp 4.12.0 dependency
|
|
16
|
+
|
|
17
|
+
## Key Files
|
|
18
|
+
|
|
19
|
+
| File | Description |
|
|
20
|
+
|------|-------------|
|
|
21
|
+
| `src/main/java/com/bufferedblob/BufferedBlobModule.kt` | NativeBufferedBlobSpec impl: install(), openRead, openWrite, createDownload, FS ops, hashFile |
|
|
22
|
+
| `src/main/java/com/bufferedblob/BufferedBlobPackage.kt` | TurboReactPackage, ReactModuleInfoProvider; returns BufferedBlobModule |
|
|
23
|
+
| `src/main/java/com/bufferedblob/HandleRegistry.kt` | Singleton: register(handle), remove(id), get(id); AtomicInteger counter, ConcurrentHashMap storage |
|
|
24
|
+
| `src/main/java/com/bufferedblob/StreamingBridge.kt` | Static JNI-callable methods: readNextChunk, write, flush, close, startDownload, cancelDownload, getReaderInfo, getWriterInfo |
|
|
25
|
+
| `build.gradle` | Dependencies: react-android, OkHttp; CMake pointing to cpp/CMakeLists.txt |
|
|
26
|
+
|
|
27
|
+
## For AI Agents
|
|
28
|
+
|
|
29
|
+
### Working In This Directory
|
|
30
|
+
|
|
31
|
+
1. **NativeBufferedBlobSpec**: Generated from Turbo Module spec (NativeBufferedBlob.ts). BufferedBlobModule extends it; implement all abstract methods.
|
|
32
|
+
2. **Coroutine scope**: Module has private CoroutineScope(Dispatchers.IO + SupervisorJob()). All async FS ops launch() on this scope. Module.invalidate() cancels scope.
|
|
33
|
+
3. **Handle lifecycle**: Turbo Module returns numeric handles. JNI and Kotlin static methods access handles via HandleRegistry.get(handleId).
|
|
34
|
+
4. **JNI bridge**: nativeInstall(jsiPtr, callInvokerHolder) is called from install(). Calls C++ install() which creates AndroidPlatformBridge.
|
|
35
|
+
5. **OkHttp downloads**: StreamingBridge.startDownload() uses OkHttp client.newCall(request).enqueue(callback). Calls JNI progress callback and resolve/reject.
|
|
36
|
+
6. **Error codes**: Return -1 for handle errors; throw RuntimeException with "[CODE]" prefix for validation errors.
|
|
37
|
+
|
|
38
|
+
### Testing Requirements
|
|
39
|
+
|
|
40
|
+
- **Module loading**: Verify install() returns true; verify system.loadLibrary("bufferedblobstreaming") succeeds
|
|
41
|
+
- **Handle factories**: Test openRead/openWrite/createDownload with valid/invalid paths, buffer sizes
|
|
42
|
+
- **File operations**: Test exists, stat, mkdir, ls, cp, mv, unlink on temp files via scope.launch
|
|
43
|
+
- **Hashing**: Verify SHA256/MD5 via MessageDigest.getInstance()
|
|
44
|
+
- **Streaming**: Test readNextChunk, write, flush, close; verify file IO
|
|
45
|
+
- **Download**: Test with HTTP server, verify OkHttp progress callback, verify file written
|
|
46
|
+
- **Cleanup**: Verify module.invalidate() cancels scope and clears handles
|
|
47
|
+
- **Thread safety**: Test concurrent handle access via ConcurrentHashMap
|
|
48
|
+
|
|
49
|
+
### Common Patterns
|
|
50
|
+
|
|
51
|
+
1. **Coroutine file op**: `scope.launch { try { /* do work */ promise.resolve(result) } catch (e: Exception) { promise.reject("ERR_FS", e.message) } }`
|
|
52
|
+
2. **Create reader**: `openRead(path, bufferSize)` → validate buffer size → FileInputStream(file) → ReaderHandle → register → return handleId
|
|
53
|
+
3. **Stream read**: JNI readNextChunk(handleId) → get handle → read chunk → call progress callback → call resolve callback
|
|
54
|
+
4. **Download**: `createDownload(url, destPath)` → DownloaderHandle(url, destPath) → register → StreamingBridge.startDownload() → OkHttp enqueue → callback chain
|
|
55
|
+
5. **Error codes**: Buffer size validation throws with "[INVALID_ARGUMENT]"; file not found with "[FILE_NOT_FOUND]"
|
|
56
|
+
|
|
57
|
+
## Dependencies
|
|
58
|
+
|
|
59
|
+
### Internal
|
|
60
|
+
- `HandleRegistry.kt` — Singleton handle storage
|
|
61
|
+
- `StreamingBridge.kt` — JNI static methods (called from C++)
|
|
62
|
+
|
|
63
|
+
### External
|
|
64
|
+
- **React Native** >= 0.76.0
|
|
65
|
+
- Codegen: NativeBufferedBlobSpec (generated from src/NativeBufferedBlob.ts)
|
|
66
|
+
- TurboModuleRegistry, Promise, ReactMethod, ReadableMap
|
|
67
|
+
- **Android SDK**
|
|
68
|
+
- `java.io.File, FileInputStream, FileOutputStream` — File IO
|
|
69
|
+
- `java.security.MessageDigest` — SHA256, MD5 hashing
|
|
70
|
+
- `android.os.Environment` — DIRECTORY_DOWNLOADS
|
|
71
|
+
- **OkHttp** 4.12.0 — HTTP downloads
|
|
72
|
+
- **Kotlin Coroutines** — Dispatchers.IO, CoroutineScope, SupervisorJob
|
|
73
|
+
|
|
74
|
+
<!-- MANUAL: Document OkHttp configuration (timeouts, certificates), Coroutine scope lifecycle on module invalidate, JNI callback performance -->
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
buildscript {
|
|
2
|
+
ext.safeExtGet = { prop, fallback ->
|
|
3
|
+
rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
|
|
4
|
+
}
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
apply plugin: "com.android.library"
|
|
8
|
+
apply plugin: "kotlin-android"
|
|
9
|
+
apply plugin: "com.facebook.react"
|
|
10
|
+
|
|
11
|
+
android {
|
|
12
|
+
namespace "com.bufferedblob"
|
|
13
|
+
compileSdk safeExtGet("compileSdkVersion", 35)
|
|
14
|
+
|
|
15
|
+
defaultConfig {
|
|
16
|
+
minSdk safeExtGet("minSdkVersion", 24)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
buildFeatures {
|
|
20
|
+
buildConfig true
|
|
21
|
+
prefab true
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
sourceSets {
|
|
25
|
+
main {
|
|
26
|
+
java.srcDirs += ['src/main/java']
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
dependencies {
|
|
32
|
+
implementation "com.facebook.react:react-android"
|
|
33
|
+
implementation "com.squareup.okhttp3:okhttp:4.12.0"
|
|
34
|
+
}
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
package com.bufferedblob
|
|
2
|
+
|
|
3
|
+
import android.os.Environment
|
|
4
|
+
import com.facebook.react.bridge.Promise
|
|
5
|
+
import com.facebook.react.bridge.ReactApplicationContext
|
|
6
|
+
import com.facebook.react.bridge.ReactMethod
|
|
7
|
+
import com.facebook.react.bridge.ReadableMap
|
|
8
|
+
import com.facebook.react.bridge.WritableNativeArray
|
|
9
|
+
import com.facebook.react.bridge.WritableNativeMap
|
|
10
|
+
import java.io.File
|
|
11
|
+
import java.io.FileInputStream
|
|
12
|
+
import java.io.FileOutputStream
|
|
13
|
+
import java.security.MessageDigest
|
|
14
|
+
import kotlinx.coroutines.CoroutineScope
|
|
15
|
+
import kotlinx.coroutines.Dispatchers
|
|
16
|
+
import kotlinx.coroutines.SupervisorJob
|
|
17
|
+
import kotlinx.coroutines.cancel
|
|
18
|
+
import kotlinx.coroutines.launch
|
|
19
|
+
|
|
20
|
+
class BufferedBlobModule(reactContext: ReactApplicationContext)
|
|
21
|
+
: NativeBufferedBlobSpec(reactContext) {
|
|
22
|
+
|
|
23
|
+
companion object {
|
|
24
|
+
const val NAME = "BufferedBlob"
|
|
25
|
+
private const val MIN_BUFFER_SIZE = 4096
|
|
26
|
+
private const val MAX_BUFFER_SIZE = 4194304 // 4MB
|
|
27
|
+
private const val HASH_CHUNK_SIZE = 8192
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
|
31
|
+
|
|
32
|
+
override fun getName(): String = NAME
|
|
33
|
+
|
|
34
|
+
override fun install(): Boolean {
|
|
35
|
+
return try {
|
|
36
|
+
val jsContext = reactApplicationContext.javaScriptContextHolder?.get() ?: return false
|
|
37
|
+
if (jsContext == 0L) return false
|
|
38
|
+
val callInvokerHolder = reactApplicationContext.jsCallInvokerHolder ?: return false
|
|
39
|
+
nativeInstall(jsContext, callInvokerHolder)
|
|
40
|
+
true
|
|
41
|
+
} catch (e: Exception) {
|
|
42
|
+
false
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
private external fun nativeInstall(jsiPtr: Long, callInvokerHolder: Any)
|
|
47
|
+
|
|
48
|
+
init {
|
|
49
|
+
System.loadLibrary("bufferedblobstreaming")
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
@ReactMethod(isBlockingSynchronousMethod = true)
|
|
53
|
+
override fun openRead(path: String, bufferSize: Double): Double {
|
|
54
|
+
val size = bufferSize.toInt()
|
|
55
|
+
if (size < MIN_BUFFER_SIZE || size > MAX_BUFFER_SIZE) {
|
|
56
|
+
throw RuntimeException("[INVALID_ARGUMENT] Buffer size must be $MIN_BUFFER_SIZE-$MAX_BUFFER_SIZE: $size")
|
|
57
|
+
}
|
|
58
|
+
val file = File(path)
|
|
59
|
+
if (!file.exists()) throw RuntimeException("[FILE_NOT_FOUND] File does not exist: $path")
|
|
60
|
+
if (!file.isFile) throw RuntimeException("[INVALID_ARGUMENT] Path is not a file: $path")
|
|
61
|
+
val stream = try {
|
|
62
|
+
FileInputStream(file)
|
|
63
|
+
} catch (e: java.io.FileNotFoundException) {
|
|
64
|
+
throw RuntimeException("[FILE_NOT_FOUND] File was removed during open: $path")
|
|
65
|
+
}
|
|
66
|
+
val reader = ReaderHandle(stream, size, file.length())
|
|
67
|
+
return HandleRegistry.register(reader).toDouble()
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
@ReactMethod(isBlockingSynchronousMethod = true)
|
|
71
|
+
override fun openWrite(path: String, append: Boolean): Double {
|
|
72
|
+
val file = File(path)
|
|
73
|
+
file.parentFile?.mkdirs()
|
|
74
|
+
val stream = FileOutputStream(file, append)
|
|
75
|
+
val writer = WriterHandle(stream)
|
|
76
|
+
return HandleRegistry.register(writer).toDouble()
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
@ReactMethod(isBlockingSynchronousMethod = true)
|
|
80
|
+
override fun createDownload(url: String, destPath: String, headers: ReadableMap): Double {
|
|
81
|
+
val headerMap = mutableMapOf<String, String>()
|
|
82
|
+
val iter = headers.keySetIterator()
|
|
83
|
+
while (iter.hasNextKey()) {
|
|
84
|
+
val key = iter.nextKey()
|
|
85
|
+
headerMap[key] = headers.getString(key) ?: ""
|
|
86
|
+
}
|
|
87
|
+
val handle = DownloaderHandle(url, destPath, headerMap)
|
|
88
|
+
return HandleRegistry.register(handle).toDouble()
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
@ReactMethod(isBlockingSynchronousMethod = true)
|
|
92
|
+
override fun closeHandle(handleId: Double) {
|
|
93
|
+
HandleRegistry.remove(handleId.toInt())
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
override fun getTypedExportedConstants(): Map<String, Any> {
|
|
97
|
+
return mapOf(
|
|
98
|
+
"documentDir" to reactApplicationContext.filesDir.absolutePath,
|
|
99
|
+
"cacheDir" to reactApplicationContext.cacheDir.absolutePath,
|
|
100
|
+
"tempDir" to (System.getProperty("java.io.tmpdir") ?: reactApplicationContext.cacheDir.absolutePath),
|
|
101
|
+
"downloadDir" to Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).absolutePath
|
|
102
|
+
)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
override fun exists(path: String, promise: Promise) {
|
|
106
|
+
scope.launch {
|
|
107
|
+
try {
|
|
108
|
+
promise.resolve(File(path).exists())
|
|
109
|
+
} catch (e: Exception) {
|
|
110
|
+
promise.reject("ERR_FS", e.message, e)
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
override fun stat(path: String, promise: Promise) {
|
|
116
|
+
scope.launch {
|
|
117
|
+
try {
|
|
118
|
+
val file = File(path)
|
|
119
|
+
if (!file.exists()) throw RuntimeException("[FILE_NOT_FOUND] File does not exist: $path")
|
|
120
|
+
val type = when {
|
|
121
|
+
file.isFile -> "file"
|
|
122
|
+
file.isDirectory -> "directory"
|
|
123
|
+
else -> "unknown"
|
|
124
|
+
}
|
|
125
|
+
val map = WritableNativeMap().apply {
|
|
126
|
+
putString("path", file.absolutePath)
|
|
127
|
+
putString("name", file.name)
|
|
128
|
+
putDouble("size", (if (file.isFile) file.length() else 0L).toDouble())
|
|
129
|
+
putString("type", type)
|
|
130
|
+
putDouble("lastModified", file.lastModified().toDouble())
|
|
131
|
+
}
|
|
132
|
+
promise.resolve(map)
|
|
133
|
+
} catch (e: Exception) {
|
|
134
|
+
promise.reject("ERR_FS", e.message, e)
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
override fun unlink(path: String, promise: Promise) {
|
|
140
|
+
scope.launch {
|
|
141
|
+
try {
|
|
142
|
+
val file = File(path)
|
|
143
|
+
if (!file.exists()) throw RuntimeException("[FILE_NOT_FOUND] File does not exist: $path")
|
|
144
|
+
if (file.isDirectory) {
|
|
145
|
+
if (!file.deleteRecursively()) throw RuntimeException("[IO_ERROR] Failed to delete directory: $path")
|
|
146
|
+
} else {
|
|
147
|
+
if (!file.delete()) throw RuntimeException("[IO_ERROR] Failed to delete: $path")
|
|
148
|
+
}
|
|
149
|
+
promise.resolve(null)
|
|
150
|
+
} catch (e: Exception) {
|
|
151
|
+
promise.reject("ERR_FS", e.message, e)
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
override fun mkdir(path: String, promise: Promise) {
|
|
157
|
+
scope.launch {
|
|
158
|
+
try {
|
|
159
|
+
val file = File(path)
|
|
160
|
+
if (file.exists()) {
|
|
161
|
+
if (!file.isDirectory) {
|
|
162
|
+
throw RuntimeException("[INVALID_ARGUMENT] Path exists and is not a directory: $path")
|
|
163
|
+
}
|
|
164
|
+
promise.resolve(null)
|
|
165
|
+
return@launch
|
|
166
|
+
}
|
|
167
|
+
if (!file.mkdirs()) throw RuntimeException("[IO_ERROR] Failed to create directory: $path")
|
|
168
|
+
promise.resolve(null)
|
|
169
|
+
} catch (e: Exception) {
|
|
170
|
+
promise.reject("ERR_FS", e.message, e)
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
override fun ls(path: String, promise: Promise) {
|
|
176
|
+
scope.launch {
|
|
177
|
+
try {
|
|
178
|
+
val dir = File(path)
|
|
179
|
+
if (!dir.exists()) throw RuntimeException("[FILE_NOT_FOUND] Directory does not exist: $path")
|
|
180
|
+
if (!dir.isDirectory) throw RuntimeException("[NOT_A_DIRECTORY] Path is not a directory: $path")
|
|
181
|
+
val arr = WritableNativeArray()
|
|
182
|
+
(dir.listFiles() ?: emptyArray()).forEach { file ->
|
|
183
|
+
val type = when {
|
|
184
|
+
file.isFile -> "file"
|
|
185
|
+
file.isDirectory -> "directory"
|
|
186
|
+
else -> "unknown"
|
|
187
|
+
}
|
|
188
|
+
arr.pushMap(WritableNativeMap().apply {
|
|
189
|
+
putString("path", file.absolutePath)
|
|
190
|
+
putString("name", file.name)
|
|
191
|
+
putDouble("size", (if (file.isFile) file.length() else 0L).toDouble())
|
|
192
|
+
putString("type", type)
|
|
193
|
+
putDouble("lastModified", file.lastModified().toDouble())
|
|
194
|
+
})
|
|
195
|
+
}
|
|
196
|
+
promise.resolve(arr)
|
|
197
|
+
} catch (e: Exception) {
|
|
198
|
+
promise.reject("ERR_FS", e.message, e)
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
override fun cp(srcPath: String, destPath: String, promise: Promise) {
|
|
204
|
+
scope.launch {
|
|
205
|
+
try {
|
|
206
|
+
val src = File(srcPath)
|
|
207
|
+
if (!src.exists()) throw RuntimeException("[FILE_NOT_FOUND] Source does not exist: $srcPath")
|
|
208
|
+
if (!src.isFile) throw RuntimeException("[INVALID_ARGUMENT] Source is not a file: $srcPath")
|
|
209
|
+
val dest = File(destPath)
|
|
210
|
+
dest.parentFile?.mkdirs()
|
|
211
|
+
src.copyTo(dest, overwrite = true)
|
|
212
|
+
promise.resolve(null)
|
|
213
|
+
} catch (e: Exception) {
|
|
214
|
+
promise.reject("ERR_FS", e.message, e)
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
override fun mv(srcPath: String, destPath: String, promise: Promise) {
|
|
220
|
+
scope.launch {
|
|
221
|
+
try {
|
|
222
|
+
val src = File(srcPath)
|
|
223
|
+
if (!src.exists()) throw RuntimeException("[FILE_NOT_FOUND] Source does not exist: $srcPath")
|
|
224
|
+
val dest = File(destPath)
|
|
225
|
+
dest.parentFile?.mkdirs()
|
|
226
|
+
if (!src.renameTo(dest)) {
|
|
227
|
+
if (src.isFile) {
|
|
228
|
+
src.copyTo(dest, overwrite = true)
|
|
229
|
+
if (!src.delete()) {
|
|
230
|
+
throw RuntimeException("[IO_ERROR] Move partially failed: copied but could not delete source: $srcPath")
|
|
231
|
+
}
|
|
232
|
+
} else {
|
|
233
|
+
throw RuntimeException("[IO_ERROR] Failed to move: $srcPath")
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
promise.resolve(null)
|
|
237
|
+
} catch (e: Exception) {
|
|
238
|
+
promise.reject("ERR_FS", e.message, e)
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
override fun hashFile(path: String, algorithm: String, promise: Promise) {
|
|
244
|
+
scope.launch {
|
|
245
|
+
try {
|
|
246
|
+
val file = File(path)
|
|
247
|
+
if (!file.exists()) throw RuntimeException("[FILE_NOT_FOUND] File does not exist: $path")
|
|
248
|
+
if (!file.isFile) throw RuntimeException("[INVALID_ARGUMENT] Path is not a file: $path")
|
|
249
|
+
val algoName = when (algorithm) {
|
|
250
|
+
"sha256" -> "SHA-256"
|
|
251
|
+
"md5" -> "MD5"
|
|
252
|
+
else -> throw RuntimeException("[INVALID_ARGUMENT] Unknown algorithm: $algorithm")
|
|
253
|
+
}
|
|
254
|
+
val digest = MessageDigest.getInstance(algoName)
|
|
255
|
+
FileInputStream(file).use { stream ->
|
|
256
|
+
val buffer = ByteArray(HASH_CHUNK_SIZE)
|
|
257
|
+
var bytesRead: Int
|
|
258
|
+
while (stream.read(buffer).also { bytesRead = it } != -1) {
|
|
259
|
+
digest.update(buffer, 0, bytesRead)
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
promise.resolve(digest.digest().joinToString("") { "%02x".format(it) })
|
|
263
|
+
} catch (e: Exception) {
|
|
264
|
+
promise.reject("ERR_FS", e.message, e)
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
override fun invalidate() {
|
|
270
|
+
scope.cancel()
|
|
271
|
+
HandleRegistry.clear()
|
|
272
|
+
super.invalidate()
|
|
273
|
+
}
|
|
274
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
package com.bufferedblob
|
|
2
|
+
|
|
3
|
+
import com.facebook.react.TurboReactPackage
|
|
4
|
+
import com.facebook.react.bridge.NativeModule
|
|
5
|
+
import com.facebook.react.bridge.ReactApplicationContext
|
|
6
|
+
import com.facebook.react.module.model.ReactModuleInfo
|
|
7
|
+
import com.facebook.react.module.model.ReactModuleInfoProvider
|
|
8
|
+
|
|
9
|
+
class BufferedBlobPackage : TurboReactPackage() {
|
|
10
|
+
override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? {
|
|
11
|
+
return if (name == BufferedBlobModule.NAME) {
|
|
12
|
+
BufferedBlobModule(reactContext)
|
|
13
|
+
} else {
|
|
14
|
+
null
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
override fun getReactModuleInfoProvider(): ReactModuleInfoProvider {
|
|
19
|
+
return ReactModuleInfoProvider {
|
|
20
|
+
mapOf(
|
|
21
|
+
BufferedBlobModule.NAME to ReactModuleInfo(
|
|
22
|
+
BufferedBlobModule.NAME,
|
|
23
|
+
BufferedBlobModule::class.java.name,
|
|
24
|
+
false,
|
|
25
|
+
false,
|
|
26
|
+
false,
|
|
27
|
+
true
|
|
28
|
+
)
|
|
29
|
+
)
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
package com.bufferedblob
|
|
2
|
+
|
|
3
|
+
import java.io.Closeable
|
|
4
|
+
import java.io.FileInputStream
|
|
5
|
+
import java.io.FileOutputStream
|
|
6
|
+
import java.util.concurrent.ConcurrentHashMap
|
|
7
|
+
import java.util.concurrent.atomic.AtomicBoolean
|
|
8
|
+
import java.util.concurrent.atomic.AtomicInteger
|
|
9
|
+
|
|
10
|
+
object HandleRegistry {
|
|
11
|
+
private val nextId = AtomicInteger(1)
|
|
12
|
+
private val handles = ConcurrentHashMap<Int, Any>()
|
|
13
|
+
|
|
14
|
+
fun register(obj: Any): Int {
|
|
15
|
+
while (true) {
|
|
16
|
+
val id = nextId.getAndUpdate { current ->
|
|
17
|
+
if (current >= Int.MAX_VALUE) 1 else current + 1
|
|
18
|
+
}
|
|
19
|
+
if (handles.putIfAbsent(id, obj) == null) return id
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
@Suppress("UNCHECKED_CAST")
|
|
24
|
+
fun <T> get(id: Int): T? = handles[id] as? T
|
|
25
|
+
|
|
26
|
+
fun remove(id: Int): Any? {
|
|
27
|
+
val obj = handles.remove(id)
|
|
28
|
+
if (obj is Closeable) {
|
|
29
|
+
try { obj.close() } catch (_: Exception) {}
|
|
30
|
+
}
|
|
31
|
+
return obj
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
fun clear() {
|
|
35
|
+
handles.keys.toList().forEach { remove(it) }
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
data class ReaderHandle(
|
|
40
|
+
val stream: FileInputStream,
|
|
41
|
+
val bufferSize: Int,
|
|
42
|
+
val fileSize: Long,
|
|
43
|
+
@Volatile var bytesRead: Long = 0L,
|
|
44
|
+
@Volatile var isEOF: Boolean = false
|
|
45
|
+
) : Closeable {
|
|
46
|
+
private val closed = AtomicBoolean(false)
|
|
47
|
+
val isClosed: Boolean get() = closed.get()
|
|
48
|
+
override fun close() {
|
|
49
|
+
if (closed.compareAndSet(false, true)) {
|
|
50
|
+
try { stream.close() } catch (_: Exception) {}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
data class WriterHandle(
|
|
56
|
+
val stream: FileOutputStream,
|
|
57
|
+
@Volatile var bytesWritten: Long = 0L
|
|
58
|
+
) : Closeable {
|
|
59
|
+
private val closed = AtomicBoolean(false)
|
|
60
|
+
val isClosed: Boolean get() = closed.get()
|
|
61
|
+
override fun close() {
|
|
62
|
+
if (closed.compareAndSet(false, true)) {
|
|
63
|
+
try { stream.flush(); stream.close() } catch (_: Exception) {}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
data class DownloaderHandle(
|
|
69
|
+
val url: String,
|
|
70
|
+
val destPath: String,
|
|
71
|
+
val headers: Map<String, String>,
|
|
72
|
+
@Volatile var isCancelled: Boolean = false,
|
|
73
|
+
@Volatile var call: okhttp3.Call? = null,
|
|
74
|
+
@Volatile var bytesDownloaded: Long = 0L,
|
|
75
|
+
@Volatile var totalBytes: Long = -1L
|
|
76
|
+
) : Closeable {
|
|
77
|
+
fun cancel() {
|
|
78
|
+
isCancelled = true
|
|
79
|
+
call?.cancel()
|
|
80
|
+
}
|
|
81
|
+
override fun close() {
|
|
82
|
+
cancel()
|
|
83
|
+
}
|
|
84
|
+
}
|