Skip to content

Commit 6b5b5f5

Browse files
committed
feat: return ArrayBuffer from downloadFile
1 parent bfcd4ac commit 6b5b5f5

37 files changed

Lines changed: 828 additions & 192 deletions

README.md

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -297,7 +297,7 @@ await NitroFS.uploadFile(uploadOptions, (uploadedBytes, totalBytes) => {
297297
})
298298
```
299299

300-
#### `downloadFile(downloadOptions: NitroDownloadOptions, onProgress?: (downloadedBytes: number, totalBytes: number) => void): Promise<NitroFile>`
300+
#### `downloadFile(downloadOptions: NitroDownloadOptions, onProgress?: (downloadedBytes: number, totalBytes: number) => void): Promise<NitroDownloadResult>`
301301

302302
Download a file from a server with progress tracking.
303303

@@ -310,18 +310,41 @@ const downloadOptions = {
310310
},
311311
}
312312

313-
const downloadedFile = await NitroFS.downloadFile(
313+
const downloadResult = await NitroFS.downloadFile(
314314
downloadOptions,
315315
(downloadedBytes, totalBytes) => {
316316
const progress = (downloadedBytes / totalBytes) * 100
317317
console.log(`Download progress: ${progress.toFixed(1)}%`)
318318
}
319319
)
320320

321+
if (downloadResult instanceof ArrayBuffer) {
322+
throw new Error('Expected file metadata')
323+
}
324+
325+
const downloadedFile = downloadResult
321326
console.log('Downloaded file:', downloadedFile)
322327
// Returns: { name: 'document.pdf', mimeType: 'application/pdf', path: '/path/to/file' }
323328
```
324329

330+
Return the downloaded bytes as a zero-copy `ArrayBuffer` by setting `output` to `'arrayBuffer'`.
331+
The file is still saved to `destinationPath`.
332+
333+
```typescript
334+
const downloadedBytes = await NitroFS.downloadFile({
335+
url: 'https://example.com/files/document.pdf',
336+
destinationPath: NitroFS.DOWNLOAD_DIR + '/document.pdf',
337+
output: 'arrayBuffer',
338+
})
339+
340+
if (!(downloadedBytes instanceof ArrayBuffer)) {
341+
throw new Error('Expected ArrayBuffer')
342+
}
343+
344+
const view = new Uint8Array(downloadedBytes)
345+
console.log('Downloaded byte length:', view.byteLength)
346+
```
347+
325348
## 📝 Type Definitions
326349

327350
### `NitroFile`
@@ -353,9 +376,16 @@ interface NitroDownloadOptions {
353376
url: string // Download endpoint URL
354377
destinationPath: string // Path where the downloaded file is saved
355378
headers?: Record<string, string> // Custom headers
379+
output?: 'file' | 'arrayBuffer' // Return file metadata or downloaded bytes
356380
}
357381
```
358382

383+
### `NitroDownloadResult`
384+
385+
```typescript
386+
type NitroDownloadResult = NitroFile | ArrayBuffer
387+
```
388+
359389
### `NitroFileStat`
360390
361391
```typescript
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package com.nitrofs
2+
3+
import com.margelo.nitro.core.ArrayBuffer
4+
import java.io.File
5+
import java.io.RandomAccessFile
6+
import java.nio.channels.FileChannel
7+
8+
internal fun File.toMappedArrayBuffer(): ArrayBuffer {
9+
RandomAccessFile(this, "rw").use { randomAccessFile ->
10+
val channel = randomAccessFile.channel
11+
val byteSize = channel.size()
12+
13+
if (byteSize > Int.MAX_VALUE) {
14+
throw IllegalStateException(
15+
"File is too large to expose as ArrayBuffer. path=$absolutePath, size=$byteSize"
16+
)
17+
}
18+
19+
if (byteSize == 0L) {
20+
return ArrayBuffer.allocate(0)
21+
}
22+
23+
val mappedBuffer = channel.map(FileChannel.MapMode.PRIVATE, 0, byteSize)
24+
return ArrayBuffer.wrap(mappedBuffer)
25+
}
26+
}

android/src/main/java/com/nitrofs/FileDownloader.kt

Lines changed: 20 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ package com.nitrofs
22

33
import android.util.Log
44
import com.margelo.nitro.nitrofs.NitroDownloadOptions
5+
import com.margelo.nitro.nitrofs.NitroDownloadOutput
6+
import com.margelo.nitro.nitrofs.NitroDownloadResult
57
import com.margelo.nitro.nitrofs.NitroFile
68
import io.ktor.client.HttpClient
79
import io.ktor.client.call.body
@@ -22,26 +24,25 @@ class FileDownloader {
2224
suspend fun downloadFile(
2325
downloadOptions: NitroDownloadOptions,
2426
onProgress: ((Double, Double) -> Unit)?
25-
): NitroFile? {
27+
): NitroDownloadResult {
2628
var contentType = ""
2729
val outputFile = File(downloadOptions.destinationPath)
2830
outputFile.parentFile?.mkdirs()
2931

3032
val client = HttpClient(OkHttp)
3133

3234

33-
client.use { it
34-
it.prepareGet(downloadOptions.url) {
35+
client.use { httpClient ->
36+
httpClient.prepareGet(downloadOptions.url) {
3537
method = HttpMethod.Get
3638
downloadOptions.headers?.forEach { (name, value) ->
3739
header(name, value)
3840
}
3941
onDownload { totalBytesSent, contentLength ->
40-
if (totalBytesSent > 0 && contentLength != null){
41-
onProgress?.let {
42-
withContext(Dispatchers.Main) {
43-
onProgress.invoke(totalBytesSent.toDouble(), contentLength.toDouble())
44-
}
42+
val progressCallback = onProgress
43+
if (totalBytesSent > 0 && contentLength != null && progressCallback != null) {
44+
withContext(Dispatchers.Main) {
45+
progressCallback.invoke(totalBytesSent.toDouble(), contentLength.toDouble())
4546
}
4647
}
4748
}
@@ -56,10 +57,16 @@ class FileDownloader {
5657
}
5758
}
5859

59-
return NitroFile(
60-
name = outputFile.name,
61-
path = outputFile.absolutePath,
62-
mimeType = contentType
63-
)
60+
return when (downloadOptions.output) {
61+
NitroDownloadOutput.ARRAYBUFFER -> NitroDownloadResult.First(outputFile.toMappedArrayBuffer())
62+
NitroDownloadOutput.FILE,
63+
null -> NitroDownloadResult.Second(
64+
NitroFile(
65+
name = outputFile.name,
66+
path = outputFile.absolutePath,
67+
mimeType = contentType
68+
)
69+
)
70+
}
6471
}
6572
}

android/src/main/java/com/nitrofs/HybridNitroFS.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import com.margelo.nitro.NitroModules
55
import com.margelo.nitro.core.Promise
66
import com.margelo.nitro.nitrofs.HybridNitroFSSpec
77
import com.margelo.nitro.nitrofs.NitroDownloadOptions
8+
import com.margelo.nitro.nitrofs.NitroDownloadResult
89
import com.margelo.nitro.nitrofs.NitroFile
910
import com.margelo.nitro.nitrofs.NitroFileEncoding
1011
import com.margelo.nitro.nitrofs.NitroFileStat
@@ -194,7 +195,7 @@ class HybridNitroFS: HybridNitroFSSpec() {
194195
override fun downloadFile(
195196
downloadOptions: NitroDownloadOptions,
196197
onProgress: ((Double, Double) -> Unit)?
197-
): Promise<NitroFile> {
198+
): Promise<NitroDownloadResult> {
198199
return Promise.async(ioScope) {
199200
try {
200201
nitroFsImpl.downloadFile(downloadOptions, onProgress)

android/src/main/java/com/nitrofs/NitroFSImpl.kt

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import android.util.Log
88
import android.webkit.MimeTypeMap
99
import com.facebook.react.bridge.ReactApplicationContext
1010
import com.margelo.nitro.nitrofs.NitroDownloadOptions
11+
import com.margelo.nitro.nitrofs.NitroDownloadResult
1112
import com.margelo.nitro.nitrofs.NitroFile
1213
import com.margelo.nitro.nitrofs.NitroFileEncoding
1314
import com.margelo.nitro.nitrofs.NitroFileStat
@@ -345,16 +346,11 @@ class NitroFSImpl(val context: ReactApplicationContext) {
345346
suspend fun downloadFile(
346347
downloadOptions: NitroDownloadOptions,
347348
onProgress: ((Double, Double) -> Unit)?
348-
): NitroFile {
349-
val file = fileDownloader.downloadFile(
349+
): NitroDownloadResult {
350+
return fileDownloader.downloadFile(
350351
downloadOptions,
351352
onProgress
352353
)
353-
if (file != null) {
354-
return file
355-
} else {
356-
throw RuntimeException("Failed to download file from: ${downloadOptions.url}")
357-
}
358354
}
359355

360356
fun getFileEncoding(encoding: NitroFileEncoding): Charset {

example/src/hooks/use-file-system.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -210,14 +210,19 @@ export const useFileSystem = () => {
210210
const url = 'https://httpbin.org/bytes/1024';
211211
const destinationPath = `${NitroFS.DOWNLOAD_DIR}/downloaded_file.txt`;
212212

213-
const file = await NitroFS.downloadFile(
213+
const result = await NitroFS.downloadFile(
214214
{ url, destinationPath },
215215
(downloadedBytes, totalBytes) => {
216216
const progress = (downloadedBytes / totalBytes) * 100;
217217
setDownloadProgress(progress);
218218
},
219219
);
220220

221+
if (result instanceof ArrayBuffer) {
222+
throw new Error('Expected NitroFile result for default download output');
223+
}
224+
225+
const file = result;
221226
Alert.alert('Success', `File downloaded successfully: ${file.name}`);
222227
setDownloadProgress(0);
223228
await listFiles(currentPath);

ios/ArrayBuffer+mapFile.swift

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import Darwin
2+
import Foundation
3+
import NitroModules
4+
5+
extension ArrayBuffer {
6+
static func mapFile(atPath path: String) throws -> ArrayBuffer {
7+
let fileDescriptor = open(path, O_RDWR)
8+
guard fileDescriptor >= 0 else {
9+
throw NitroFSError.fileError(message: "Failed to open file for memory mapping. path=\(path), errno=\(errno)")
10+
}
11+
defer {
12+
close(fileDescriptor)
13+
}
14+
15+
var fileStat = stat()
16+
guard fstat(fileDescriptor, &fileStat) == 0 else {
17+
throw NitroFSError.fileError(message: "Failed to stat file for memory mapping. path=\(path), errno=\(errno)")
18+
}
19+
20+
let byteSize = Int(fileStat.st_size)
21+
guard byteSize >= 0 else {
22+
throw NitroFSError.fileError(message: "Invalid file size for memory mapping. path=\(path), size=\(fileStat.st_size)")
23+
}
24+
25+
if byteSize == 0 {
26+
return ArrayBuffer.allocate(size: 0)
27+
}
28+
29+
let mappedData = mmap(nil, byteSize, PROT_READ | PROT_WRITE, MAP_PRIVATE, fileDescriptor, 0)
30+
guard mappedData != MAP_FAILED else {
31+
throw NitroFSError.fileError(message: "Failed to memory map file. path=\(path), size=\(byteSize), errno=\(errno)")
32+
}
33+
guard let mappedData else {
34+
throw NitroFSError.fileError(message: "Memory mapping returned nil. path=\(path), size=\(byteSize)")
35+
}
36+
37+
return ArrayBuffer.wrap(dataWithoutCopy: mappedData, size: byteSize) {
38+
munmap(mappedData, byteSize)
39+
}
40+
}
41+
}

ios/HybridNitroFs.swift

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ class HybridNitroFS: HybridNitroFSSpec {
6161
}
6262
}
6363

64-
func copy(srcPath: String, destPath: String) throws -> NitroModules.Promise<Void> {
64+
func copy(srcPath: String, destPath: String) throws -> Promise<Void> {
6565
return .async { [unowned self] in
6666
do {
6767
try self.nitroFSImpl.copy(source: srcPath, destination: destPath)
@@ -72,7 +72,7 @@ class HybridNitroFS: HybridNitroFSSpec {
7272
}
7373
}
7474

75-
func unlink(path: String) throws -> NitroModules.Promise<Bool> {
75+
func unlink(path: String) throws -> Promise<Bool> {
7676
return .async { [unowned self] in
7777
do {
7878
try self.nitroFSImpl.unlink(path: path)
@@ -84,7 +84,7 @@ class HybridNitroFS: HybridNitroFSSpec {
8484
}
8585
}
8686

87-
func mkdir(path: String) throws -> NitroModules.Promise<Bool> {
87+
func mkdir(path: String) throws -> Promise<Bool> {
8888
return .async { [unowned self] in
8989
do {
9090
try self.nitroFSImpl.mkdir(path: path)
@@ -96,7 +96,7 @@ class HybridNitroFS: HybridNitroFSSpec {
9696
}
9797
}
9898

99-
func stat(path: String) throws -> NitroModules.Promise<NitroFileStat> {
99+
func stat(path: String) throws -> Promise<NitroFileStat> {
100100
return .async { [unowned self] in
101101
do {
102102
return try self.nitroFSImpl.stat(path: path)
@@ -107,7 +107,7 @@ class HybridNitroFS: HybridNitroFSSpec {
107107
}
108108
}
109109

110-
func readdir(path: String) throws -> NitroModules.Promise<[NitroFile]> {
110+
func readdir(path: String) throws -> Promise<[NitroFile]> {
111111
return .async {
112112
do {
113113
return try self.nitroFSImpl.readdir(atPath: path)
@@ -118,7 +118,7 @@ class HybridNitroFS: HybridNitroFSSpec {
118118
}
119119
}
120120

121-
func rename(oldPath: String, newPath: String) throws -> NitroModules.Promise<Void> {
121+
func rename(oldPath: String, newPath: String) throws -> Promise<Void> {
122122
return .async {
123123
do {
124124
return try self.nitroFSImpl.rename(oldPath: oldPath, newPath: newPath)
@@ -173,7 +173,7 @@ class HybridNitroFS: HybridNitroFSSpec {
173173
}
174174
}
175175

176-
func downloadFile(downloadOptions: NitroDownloadOptions, onProgress: ((Double, Double) -> Void)?) throws -> NitroModules.Promise<NitroFile> {
176+
func downloadFile(downloadOptions: NitroDownloadOptions, onProgress: ((Double, Double) -> Void)?) throws -> Promise<NitroDownloadResult> {
177177
return .async { [unowned self] in
178178
do {
179179
return try await self.nitroFSImpl.downloadFile(

0 commit comments

Comments
 (0)