Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 32 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -297,7 +297,7 @@ await NitroFS.uploadFile(uploadOptions, (uploadedBytes, totalBytes) => {
})
```

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

Download a file from a server with progress tracking.

Expand All @@ -310,18 +310,41 @@ const downloadOptions = {
},
}

const downloadedFile = await NitroFS.downloadFile(
const downloadResult = await NitroFS.downloadFile(
downloadOptions,
(downloadedBytes, totalBytes) => {
const progress = (downloadedBytes / totalBytes) * 100
console.log(`Download progress: ${progress.toFixed(1)}%`)
}
)

if (downloadResult instanceof ArrayBuffer) {
throw new Error('Expected file metadata')
}

const downloadedFile = downloadResult
console.log('Downloaded file:', downloadedFile)
// Returns: { name: 'document.pdf', mimeType: 'application/pdf', path: '/path/to/file' }
```

Return the downloaded bytes as a zero-copy `ArrayBuffer` by setting `output` to `'arrayBuffer'`.
The file is still saved to `destinationPath`.

```typescript
const downloadedBytes = await NitroFS.downloadFile({
url: 'https://example.com/files/document.pdf',
destinationPath: NitroFS.DOWNLOAD_DIR + '/document.pdf',
output: 'arrayBuffer',
})

if (!(downloadedBytes instanceof ArrayBuffer)) {
throw new Error('Expected ArrayBuffer')
}

const view = new Uint8Array(downloadedBytes)
console.log('Downloaded byte length:', view.byteLength)
```

## 📝 Type Definitions

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

### `NitroDownloadResult`

```typescript
type NitroDownloadResult = NitroFile | ArrayBuffer
```

### `NitroFileStat`

```typescript
Expand Down
26 changes: 26 additions & 0 deletions android/src/main/java/com/nitrofs/File+toMappedArrayBuffer.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.nitrofs

import com.margelo.nitro.core.ArrayBuffer
import java.io.File
import java.io.RandomAccessFile
import java.nio.channels.FileChannel

internal fun File.toMappedArrayBuffer(): ArrayBuffer {
RandomAccessFile(this, "rw").use { randomAccessFile ->
val channel = randomAccessFile.channel
val byteSize = channel.size()

if (byteSize > Int.MAX_VALUE) {
throw IllegalStateException(
"File is too large to expose as ArrayBuffer. path=$absolutePath, size=$byteSize"
)
}

if (byteSize == 0L) {
return ArrayBuffer.allocate(0)
}

val mappedBuffer = channel.map(FileChannel.MapMode.PRIVATE, 0, byteSize)
return ArrayBuffer.wrap(mappedBuffer)
}
}
33 changes: 20 additions & 13 deletions android/src/main/java/com/nitrofs/FileDownloader.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package com.nitrofs

import android.util.Log
import com.margelo.nitro.nitrofs.NitroDownloadOptions
import com.margelo.nitro.nitrofs.NitroDownloadOutput
import com.margelo.nitro.nitrofs.NitroDownloadResult
import com.margelo.nitro.nitrofs.NitroFile
import io.ktor.client.HttpClient
import io.ktor.client.call.body
Expand All @@ -22,26 +24,25 @@ class FileDownloader {
suspend fun downloadFile(
downloadOptions: NitroDownloadOptions,
onProgress: ((Double, Double) -> Unit)?
): NitroFile? {
): NitroDownloadResult {
var contentType = ""
val outputFile = File(downloadOptions.destinationPath)
outputFile.parentFile?.mkdirs()

val client = HttpClient(OkHttp)


client.use { it
it.prepareGet(downloadOptions.url) {
client.use { httpClient ->
httpClient.prepareGet(downloadOptions.url) {
method = HttpMethod.Get
downloadOptions.headers?.forEach { (name, value) ->
header(name, value)
}
onDownload { totalBytesSent, contentLength ->
if (totalBytesSent > 0 && contentLength != null){
onProgress?.let {
withContext(Dispatchers.Main) {
onProgress.invoke(totalBytesSent.toDouble(), contentLength.toDouble())
}
val progressCallback = onProgress
if (totalBytesSent > 0 && contentLength != null && progressCallback != null) {
withContext(Dispatchers.Main) {
progressCallback.invoke(totalBytesSent.toDouble(), contentLength.toDouble())
}
}
}
Expand All @@ -56,10 +57,16 @@ class FileDownloader {
}
}

return NitroFile(
name = outputFile.name,
path = outputFile.absolutePath,
mimeType = contentType
)
return when (downloadOptions.output) {
NitroDownloadOutput.ARRAYBUFFER -> NitroDownloadResult.First(outputFile.toMappedArrayBuffer())
NitroDownloadOutput.FILE,
null -> NitroDownloadResult.Second(
NitroFile(
name = outputFile.name,
path = outputFile.absolutePath,
mimeType = contentType
)
)
}
}
}
3 changes: 2 additions & 1 deletion android/src/main/java/com/nitrofs/HybridNitroFS.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import com.margelo.nitro.NitroModules
import com.margelo.nitro.core.Promise
import com.margelo.nitro.nitrofs.HybridNitroFSSpec
import com.margelo.nitro.nitrofs.NitroDownloadOptions
import com.margelo.nitro.nitrofs.NitroDownloadResult
import com.margelo.nitro.nitrofs.NitroFile
import com.margelo.nitro.nitrofs.NitroFileEncoding
import com.margelo.nitro.nitrofs.NitroFileStat
Expand Down Expand Up @@ -194,7 +195,7 @@ class HybridNitroFS: HybridNitroFSSpec() {
override fun downloadFile(
downloadOptions: NitroDownloadOptions,
onProgress: ((Double, Double) -> Unit)?
): Promise<NitroFile> {
): Promise<NitroDownloadResult> {
return Promise.async(ioScope) {
try {
nitroFsImpl.downloadFile(downloadOptions, onProgress)
Expand Down
10 changes: 3 additions & 7 deletions android/src/main/java/com/nitrofs/NitroFSImpl.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import android.util.Log
import android.webkit.MimeTypeMap
import com.facebook.react.bridge.ReactApplicationContext
import com.margelo.nitro.nitrofs.NitroDownloadOptions
import com.margelo.nitro.nitrofs.NitroDownloadResult
import com.margelo.nitro.nitrofs.NitroFile
import com.margelo.nitro.nitrofs.NitroFileEncoding
import com.margelo.nitro.nitrofs.NitroFileStat
Expand Down Expand Up @@ -345,16 +346,11 @@ class NitroFSImpl(val context: ReactApplicationContext) {
suspend fun downloadFile(
downloadOptions: NitroDownloadOptions,
onProgress: ((Double, Double) -> Unit)?
): NitroFile {
val file = fileDownloader.downloadFile(
): NitroDownloadResult {
return fileDownloader.downloadFile(
downloadOptions,
onProgress
)
if (file != null) {
return file
} else {
throw RuntimeException("Failed to download file from: ${downloadOptions.url}")
}
}

fun getFileEncoding(encoding: NitroFileEncoding): Charset {
Expand Down
7 changes: 6 additions & 1 deletion example/src/hooks/use-file-system.ts
Original file line number Diff line number Diff line change
Expand Up @@ -210,14 +210,19 @@ export const useFileSystem = () => {
const url = 'https://httpbin.org/bytes/1024';
const destinationPath = `${NitroFS.DOWNLOAD_DIR}/downloaded_file.txt`;

const file = await NitroFS.downloadFile(
const result = await NitroFS.downloadFile(
{ url, destinationPath },
(downloadedBytes, totalBytes) => {
const progress = (downloadedBytes / totalBytes) * 100;
setDownloadProgress(progress);
},
);

if (result instanceof ArrayBuffer) {
throw new Error('Expected NitroFile result for default download output');
}

const file = result;
Alert.alert('Success', `File downloaded successfully: ${file.name}`);
setDownloadProgress(0);
await listFiles(currentPath);
Expand Down
41 changes: 41 additions & 0 deletions ios/ArrayBuffer+mapFile.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import Darwin
import Foundation
import NitroModules

extension ArrayBuffer {
static func mapFile(atPath path: String) throws -> ArrayBuffer {
let fileDescriptor = open(path, O_RDWR)
guard fileDescriptor >= 0 else {
throw NitroFSError.fileError(message: "Failed to open file for memory mapping. path=\(path), errno=\(errno)")
}
defer {
close(fileDescriptor)
}

var fileStat = stat()
guard fstat(fileDescriptor, &fileStat) == 0 else {
throw NitroFSError.fileError(message: "Failed to stat file for memory mapping. path=\(path), errno=\(errno)")
}

let byteSize = Int(fileStat.st_size)
guard byteSize >= 0 else {
throw NitroFSError.fileError(message: "Invalid file size for memory mapping. path=\(path), size=\(fileStat.st_size)")
}

if byteSize == 0 {
return ArrayBuffer.allocate(size: 0)
}

let mappedData = mmap(nil, byteSize, PROT_READ | PROT_WRITE, MAP_PRIVATE, fileDescriptor, 0)
guard mappedData != MAP_FAILED else {
throw NitroFSError.fileError(message: "Failed to memory map file. path=\(path), size=\(byteSize), errno=\(errno)")
}
guard let mappedData else {
throw NitroFSError.fileError(message: "Memory mapping returned nil. path=\(path), size=\(byteSize)")
}

return ArrayBuffer.wrap(dataWithoutCopy: mappedData, size: byteSize) {
munmap(mappedData, byteSize)
}
}
}
14 changes: 7 additions & 7 deletions ios/HybridNitroFs.swift
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ class HybridNitroFS: HybridNitroFSSpec {
}
}

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

func unlink(path: String) throws -> NitroModules.Promise<Bool> {
func unlink(path: String) throws -> Promise<Bool> {
return .async { [unowned self] in
do {
try self.nitroFSImpl.unlink(path: path)
Expand All @@ -84,7 +84,7 @@ class HybridNitroFS: HybridNitroFSSpec {
}
}

func mkdir(path: String) throws -> NitroModules.Promise<Bool> {
func mkdir(path: String) throws -> Promise<Bool> {
return .async { [unowned self] in
do {
try self.nitroFSImpl.mkdir(path: path)
Expand All @@ -96,7 +96,7 @@ class HybridNitroFS: HybridNitroFSSpec {
}
}

func stat(path: String) throws -> NitroModules.Promise<NitroFileStat> {
func stat(path: String) throws -> Promise<NitroFileStat> {
return .async { [unowned self] in
do {
return try self.nitroFSImpl.stat(path: path)
Expand All @@ -107,7 +107,7 @@ class HybridNitroFS: HybridNitroFSSpec {
}
}

func readdir(path: String) throws -> NitroModules.Promise<[NitroFile]> {
func readdir(path: String) throws -> Promise<[NitroFile]> {
return .async {
do {
return try self.nitroFSImpl.readdir(atPath: path)
Expand All @@ -118,7 +118,7 @@ class HybridNitroFS: HybridNitroFSSpec {
}
}

func rename(oldPath: String, newPath: String) throws -> NitroModules.Promise<Void> {
func rename(oldPath: String, newPath: String) throws -> Promise<Void> {
return .async {
do {
return try self.nitroFSImpl.rename(oldPath: oldPath, newPath: newPath)
Expand Down Expand Up @@ -173,7 +173,7 @@ class HybridNitroFS: HybridNitroFSSpec {
}
}

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