Skip to content
Merged
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
1 change: 1 addition & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ on:
push:
branches:
- main
- next
paths-ignore:
- 'docs/**'

Expand Down
2 changes: 1 addition & 1 deletion NitroFs.podspec
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ Pod::Spec.new do |s|
# C++ compiler flags, mainly for folly.
"GCC_PREPROCESSOR_DEFINITIONS" => "$(inherited) FOLLY_NO_CONFIG FOLLY_CFG_NO_COROUTINES"
}

load 'nitrogen/generated/ios/NitroFS+autolinking.rb'
add_nitrogen_files(s)

Expand Down
197 changes: 194 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,22 @@
# react-native-nitro-fs

![ChatGPT Image May 17, 2025, 12_31_35 PM](https://github.com/user-attachments/assets/14fa98eb-fbee-4e00-bc85-eb3a8f0e9832)

A high-performance file system module for React Native that handles file operations and transfers with native speed.

## Features

- 💾 File system operations (read, write, copy, delete)
- 📊 Directory management
- ⬆️ File uploads with progress tracking
- ⬇️ File downloads with progress tracking
- 🔎 File existence and stat checking
- ⚡ Native performance with Swift and Kotlin implementations
- 📲 Cross-platform support (iOS and Android)

## Requirements

- React Native v0.76.0 or higher
- React Native v0.78.0 or higher
- Node 18.0.0 or higher

> [!IMPORTANT]
Expand All @@ -14,10 +28,187 @@
bun add react-native-nitro-fs react-native-nitro-modules
```

## Credits
## Quick Start

Bootstrapped with [create-nitro-module](https://github.com/patrickkabwe/create-nitro-module).
```typescript
import { NitroFS } from 'react-native-nitro-fs'

// Check if a file exists
const exists = await NitroFS.exists('/path/to/file')

// Read a file
const content = await NitroFS.readFile('/path/to/file', 'utf8')

// Write to a file
await NitroFS.writeFile('/path/to/file', 'Hello, World!', 'utf8')

// Download a file
const file = await NitroFS.downloadFile(
'https://example.com/file.txt',
'file.txt',
NitroFS.DOWNLOAD_DIR + '/file.txt',
(downloadedBytes, totalBytes) => {
console.log(`Downloading ${(downloadedBytes / totalBytes) * 100}%`)
}
)
```

## API Reference

### Constants

The module provides several directory constants:

- `BUNDLE_DIR`: Directory for storing bundle files
- `DOCUMENT_DIR`: Directory for storing documents
- `CACHE_DIR`: Directory for storing cache
- `DOWNLOAD_DIR`: Directory for storing downloads

### Methods

#### `exists(path: string): Promise<boolean>`

Checks if a file or directory exists at the specified path.

```typescript
const exists = await NitroFS.exists('/path/to/file')
```

#### `writeFile(path: string, data: string, encoding: NitroFileEncoding): Promise<void>`

Writes data to a file at the specified path.

```typescript
await NitroFS.writeFile('/path/to/file', 'Hello, world!', 'utf8')
```

#### `readFile(path: string, encoding: NitroFileEncoding): Promise<string>`

Reads the contents of a file at the specified path.

```typescript
const data = await NitroFS.readFile('/path/to/file', 'utf8')
```

#### `copyFile(srcPath: string, destPath: string): Promise<void>`

Copies a file from source path to destination path.

```typescript
await NitroFS.copyFile('/path/to/file', '/path/to/destination')
```

#### `copy(srcPath: string, destPath: string): Promise<void>`

Copies a file or directory from source path to destination path.

```typescript
await NitroFS.copy('/path/to/file', '/path/to/destination')
```

#### `unlink(path: string): Promise<boolean>`

Deletes a file or directory from the file system.

```typescript
await NitroFS.unlink('/path/to/file')
```

#### `mkdir(path: string): Promise<boolean>`

Creates a directory in the file system.

```typescript
await NitroFS.mkdir('/path/to/directory')
```

#### `stat(path: string): Promise<NitroFileStat>`

Gets the stat information of a file or directory.

```typescript
const stat = await NitroFS.stat('/path/to/file')
```

#### `uploadFile(file: NitroFile, uploadOptions: NitroUploadOptions, onProgress?: (uploadedBytes: number, totalBytes: number) => void): Promise<void>`

Uploads a file to a server with progress tracking.

```typescript
const options: NitroUploadOptions = {
file: {
name: 'test.txt',
mimeType: 'text/plain',
path: 'test.txt',
},
url: 'https://example.com/upload',
headers: {
'X-Filename': 'test.txt',
},
}

await NitroFS.uploadFile(options, (uploadedBytes, totalBytes) => {
console.log(`Uploading ${(uploadedBytes / totalBytes) * 100}%`)
})
```

#### `downloadFile(serverUrl: string, fileName: string, destinationPath: string, onProgress?: (downloadedBytes: number, totalBytes: number) => void): Promise<NitroFile>`

Downloads a file from a server to the specified destination path with progress tracking.

```typescript
const serverUrl = 'https://example.com/download'
const fileName = 'file.txt'
const destinationPath = NitroFS.DOWNLOAD_DIR + '/file.txt'

const file = await NitroFS.downloadFile(
serverUrl,
fileName,
destinationPath,
(downloadedBytes, totalBytes) => {
console.log(`Downloading ${(downloadedBytes / totalBytes) * 100}%`)
}
)
```

### Types

#### `NitroFile`

```typescript
interface NitroFile {
name: string
mimeType: string
path: string
}
```

#### `NitroUploadOptions`

```typescript
interface NitroUploadOptions {
file: NitroFile
url: string
headers?: Record<string, string>
}
```

#### `NitroFileStat`

Contains file/directory statistics information.

#### `NitroFileEncoding`

Type for file encoding options (e.g., 'utf8').

## Contributing

Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change.

## Credits

Bootstrapped with [create-nitro-module](https://github.com/patrickkabwe/create-nitro-module).

## License

[MIT](LICENSE)
45 changes: 45 additions & 0 deletions android/src/main/java/com/nitrofs/FileDownloader.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package com.nitrofs

import android.os.Handler
import android.os.Looper
import io.ktor.client.HttpClient
import io.ktor.client.call.body
import io.ktor.client.engine.okhttp.OkHttp
import io.ktor.client.plugins.onDownload
import io.ktor.client.request.prepareGet
import io.ktor.http.HttpMethod
import io.ktor.util.cio.writeChannel
import io.ktor.utils.io.ByteReadChannel
import io.ktor.utils.io.copyAndClose
import java.io.File

class FileDownloader {
suspend fun downloadFile(
serverUrl: String,
fileName: String,
destinationPath: String,
onProgress: ((Double, Double) -> Unit)?
) {
val outputFile = File(destinationPath)
outputFile.parentFile?.mkdirs()
val client = HttpClient(OkHttp)

client.use { it
it.prepareGet("$serverUrl/$fileName") {
method = HttpMethod.Get
onDownload { bytesSentTotal, contentLength ->
if (bytesSentTotal > 0){
onProgress?.let {
Handler(Looper.getMainLooper()).post {
onProgress.invoke(bytesSentTotal.toDouble(), contentLength.toDouble())
}
}
}
}
}.execute { response ->
val channel: ByteReadChannel = response.body()
channel.copyAndClose(outputFile.writeChannel())
}
}
}
}
Comment on lines +1 to +45
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Well-implemented file download functionality.

The new FileDownloader class is well-structured with several good practices:

  • Creating parent directories if they don't exist (line 24)
  • Properly using HttpClient with resource management via use block (lines 25-43)
  • Setting up download progress callbacks on the main thread (lines 30-38)
  • Using efficient streaming with copyAndClose to write directly to file (line 41)

However, there are a few opportunities for improvement:

  1. Missing validation for the server URL and file name parameters
  2. No handling for HTTP error responses (non-200 status codes)
  3. Missing timeout configuration for the HTTP client

Consider adding error handling for HTTP responses and timeouts:

 val client = HttpClient(OkHttp) {
+    engine {
+        config {
+            connectTimeout(30, TimeUnit.SECONDS)
+            readTimeout(30, TimeUnit.SECONDS)
+        }
+    }
 }

 client.use { it
     it.prepareGet("$serverUrl/$fileName") {
         method = HttpMethod.Get
         onDownload { bytesSentTotal, contentLength ->
             // ... existing code ...
         }
     }.execute { response ->
+        if (!response.status.isSuccess()) {
+            throw IOException("Download failed with status: ${response.status}")
+        }
         val channel: ByteReadChannel = response.body()
         channel.copyAndClose(outputFile.writeChannel())
     }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
package com.nitrofs
import android.os.Handler
import android.os.Looper
import io.ktor.client.HttpClient
import io.ktor.client.call.body
import io.ktor.client.engine.okhttp.OkHttp
import io.ktor.client.plugins.onDownload
import io.ktor.client.request.prepareGet
import io.ktor.http.HttpMethod
import io.ktor.util.cio.writeChannel
import io.ktor.utils.io.ByteReadChannel
import io.ktor.utils.io.copyAndClose
import java.io.File
class FileDownloader {
suspend fun downloadFile(
serverUrl: String,
fileName: String,
destinationPath: String,
onProgress: ((Double, Double) -> Unit)?
) {
val outputFile = File(destinationPath)
outputFile.parentFile?.mkdirs()
val client = HttpClient(OkHttp)
client.use { it
it.prepareGet("$serverUrl/$fileName") {
method = HttpMethod.Get
onDownload { bytesSentTotal, contentLength ->
if (bytesSentTotal > 0){
onProgress?.let {
Handler(Looper.getMainLooper()).post {
onProgress.invoke(bytesSentTotal.toDouble(), contentLength.toDouble())
}
}
}
}
}.execute { response ->
val channel: ByteReadChannel = response.body()
channel.copyAndClose(outputFile.writeChannel())
}
}
}
}
package com.nitrofs
import android.os.Handler
import android.os.Looper
import io.ktor.client.HttpClient
import io.ktor.client.call.body
import io.ktor.client.engine.okhttp.OkHttp
import io.ktor.client.plugins.onDownload
import io.ktor.client.request.prepareGet
import io.ktor.http.HttpMethod
import io.ktor.util.cio.writeChannel
import io.ktor.utils.io.ByteReadChannel
import io.ktor.utils.io.copyAndClose
import java.io.File
class FileDownloader {
suspend fun downloadFile(
serverUrl: String,
fileName: String,
destinationPath: String,
onProgress: ((Double, Double) -> Unit)?
) {
val outputFile = File(destinationPath)
outputFile.parentFile?.mkdirs()
val client = HttpClient(OkHttp) {
engine {
config {
connectTimeout(30, TimeUnit.SECONDS)
readTimeout(30, TimeUnit.SECONDS)
}
}
}
client.use { it
it.prepareGet("$serverUrl/$fileName") {
method = HttpMethod.Get
onDownload { bytesSentTotal, contentLength ->
if (bytesSentTotal > 0){
onProgress?.let {
Handler(Looper.getMainLooper()).post {
onProgress.invoke(bytesSentTotal.toDouble(), contentLength.toDouble())
}
}
}
}
}.execute { response ->
if (!response.status.isSuccess()) {
throw IOException("Download failed with status: ${response.status}")
}
val channel: ByteReadChannel = response.body()
channel.copyAndClose(outputFile.writeChannel())
}
}
}
}
🤖 Prompt for AI Agents
In android/src/main/java/com/nitrofs/FileDownloader.kt lines 1 to 45, add
validation checks for serverUrl and fileName parameters to ensure they are not
empty or malformed before proceeding. Implement error handling for HTTP
responses by checking the status code after executing the request and throwing
an exception or returning an error if it is not successful (non-200). Configure
the HttpClient with appropriate timeout settings to avoid indefinite waits
during download. These changes will improve robustness by validating inputs,
handling HTTP errors gracefully, and preventing hangs due to network issues.

14 changes: 13 additions & 1 deletion android/src/main/java/com/nitrofs/HybridNitroFS.kt
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,18 @@ class HybridNitroFS: HybridNitroFSSpec() {
destinationPath: String,
onProgress: ((Double, Double) -> Unit)?
): Promise<NitroFile> {
TODO("Not yet implemented")
return Promise.async {
try {
nitroFsImpl.downloadFile(serverUrl, fileName, destinationPath, onProgress)
NitroFile(
name = fileName,
path = destinationPath,
mimeType = "",
)
} catch (e: Exception) {
Log.e("NitroFS", "Error downloading file: ${e.message}")
throw Error(e)
}
}
}
}
31 changes: 19 additions & 12 deletions android/src/main/java/com/nitrofs/NitroFSImpl.kt
Original file line number Diff line number Diff line change
@@ -1,16 +1,9 @@
package com.nitrofs

import android.Manifest
import android.content.Context
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Environment
import android.util.Log
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import com.facebook.react.bridge.ReactApplicationContext
import com.margelo.nitro.NitroModules
import com.margelo.nitro.core.Promise
import com.margelo.nitro.nitrofs.NitroFile
import com.margelo.nitro.nitrofs.NitroFileEncoding
import com.margelo.nitro.nitrofs.NitroFileStat
Expand All @@ -21,6 +14,7 @@ import java.nio.charset.Charset

class NitroFSImpl(val context: ReactApplicationContext) {
private val nitroFileUploader: NitroFileUploader = NitroFileUploader()
private val fileDownloader: FileDownloader = FileDownloader()

fun exists(path: String): Boolean {
val dir = File(path)
Expand Down Expand Up @@ -88,13 +82,26 @@ class NitroFSImpl(val context: ReactApplicationContext) {
return Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS)?.absolutePath ?: ""
}

suspend fun uploadFile(nitroFile: NitroFile,
suspend fun uploadFile(file: NitroFile,
uploadOptions: NitroUploadOptions,
onProgress: ((Double, Double) -> Unit)?) {
val nitroFile = File(nitroFile.path)
onProgress: ((Double, Double) -> Unit)?
) {
val nitroFile = File(file.path)
nitroFileUploader.handleUpload(nitroFile, uploadOptions, onProgress)
}

Log.d("NitroFS", "Uploading file: ${nitroFile.absolutePath}")
nitroFileUploader.handleUpload(nitroFile,uploadOptions, onProgress)
suspend fun downloadFile(
serverUrl: String,
fileName: String,
destinationPath: String,
onProgress: ((Double, Double) -> Unit)?
) {
fileDownloader.downloadFile(
serverUrl,
fileName,
destinationPath,
onProgress
)
}

fun getFileEncoding(encoding: NitroFileEncoding): Charset {
Expand Down
Loading
Loading