Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
14 changes: 14 additions & 0 deletions android/src/main/java/com/nitrofs/File+Extensions.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.nitrofs

import com.margelo.nitro.nitrofs.NitroFileStat
import java.io.File

fun File.toNitroFileStat(): NitroFileStat {
return NitroFileStat(
size = length().toDouble(),
isFile = isFile,
isDirectory = isDirectory,
ctime = lastModified().toDouble(),
mtime = lastModified().toDouble()
)
}
4 changes: 2 additions & 2 deletions android/src/main/java/com/nitrofs/HybridNitroFS.kt
Original file line number Diff line number Diff line change
Expand Up @@ -158,9 +158,9 @@ class HybridNitroFS: HybridNitroFSSpec() {
}
}

override fun basename(path: String, ext: String?): String {
override fun basename(path: String): String {
try {
return nitroFsImpl.basename(path, ext)
return nitroFsImpl.basename(path)
} catch (e: Exception) {
Log.e(TAG, "Error while calling basename(...): ${e.message}")
throw Error(e)
Expand Down
142 changes: 119 additions & 23 deletions android/src/main/java/com/nitrofs/NitroFSImpl.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package com.nitrofs

import android.content.Context
import android.os.Environment
import android.provider.DocumentsContract
import android.provider.OpenableColumns
import android.util.Log
import android.webkit.MimeTypeMap
import com.facebook.react.bridge.ReactApplicationContext
Expand All @@ -18,14 +20,32 @@ class NitroFSImpl(val context: ReactApplicationContext) {
private val nitroFileUploader: NitroFileUploader = NitroFileUploader()
private val fileDownloader: FileDownloader = FileDownloader()

private val contentResolver = context.contentResolver

fun exists(path: String): Boolean {
val dir = File(path)
return dir.exists()
val resolved = path.toResolvedPath() ?: return false
return when(resolved) {
is ResolvedPath.FilePath -> resolved.file.exists()
is ResolvedPath.Content -> {
contentResolver.query(
resolved.uri,
arrayOf("_id"),
null,
null,
null
)?.use { cursor -> cursor.moveToFirst() } ?: false
}
}
}

fun unlink(path: String): Boolean {
val file = File(path)
return file.deleteRecursively()
val resolved = path.toResolvedPath() ?: return false
return when(resolved) {
is ResolvedPath.FilePath -> resolved.file.deleteRecursively()
is ResolvedPath.Content -> {
contentResolver.delete(resolved.uri, null, null) > 0
}
}
}

fun writeFile(
Expand Down Expand Up @@ -129,27 +149,72 @@ class NitroFSImpl(val context: ReactApplicationContext) {
srcPath: String,
destPath: String
) {
val file = File(srcPath)
val dest = File(destPath)
file.copyTo(dest)
val src = srcPath.toResolvedPath() ?: return
val dest = destPath.toResolvedPath() ?: return
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

try {
src.openInputStream(context)?.use { input ->
dest.openOutputStream(context)?.use { output ->
input.copyTo(output, DEFAULT_BUFFER_SIZE)
}
}
} catch (e: Exception) {
throw Error("Failed to copy file: ${e.message}")
Comment thread
patrickkabwe marked this conversation as resolved.
}
}
Comment thread
patrickkabwe marked this conversation as resolved.


fun mkdir(path: String): Boolean {
val file = File(path)
return file.mkdirs()
val resolved = path.toResolvedPath() ?: return false
return when(resolved) {
is ResolvedPath.FilePath -> resolved.file.mkdirs()
is ResolvedPath.Content -> throw Error("Cannot create directory from content URI: $path")
}
}
Comment thread
patrickkabwe marked this conversation as resolved.

fun stat(path: String): NitroFileStat {
val file = File(path)
val stat = NitroFileStat(
size = file.length().toDouble(),
isFile = file.isFile,
isDirectory = file.isDirectory,
ctime = file.lastModified().toDouble(),
mtime = file.lastModified().toDouble()
)
return stat
val resolved = path.toResolvedPath() ?: throw Error("Invalid path: $path")
Comment thread
patrickkabwe marked this conversation as resolved.
return when(resolved) {
is ResolvedPath.FilePath -> resolved.file.toNitroFileStat()
is ResolvedPath.Content -> {
val uri = resolved.uri
val projection = arrayOf(
DocumentsContract.Document.COLUMN_SIZE,
DocumentsContract.Document.COLUMN_MIME_TYPE,
DocumentsContract.Document.COLUMN_LAST_MODIFIED,
DocumentsContract.Document.MIME_TYPE_DIR
)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
contentResolver.query(uri, projection, null, null, null)?.use { cursor ->
if (!cursor.moveToFirst()) {
throw Error("Unable to stat content uri (no row): $path")
}

fun getString(col: String): String? {
val idx = cursor.getColumnIndex(col)
return if (idx >= 0 && !cursor.isNull(idx)) cursor.getString(idx) else null
}

fun getLong(col: String): Long? {
val idx = cursor.getColumnIndex(col)
return if (idx >= 0 && !cursor.isNull(idx)) cursor.getLong(idx) else null
}

val size = getLong(DocumentsContract.Document.COLUMN_SIZE) ?: 0L
val mime = getString(DocumentsContract.Document.COLUMN_MIME_TYPE) ?: contentResolver.getType(uri)
val isDirectory = mime == DocumentsContract.Document.MIME_TYPE_DIR
val isFile = !isDirectory
val lastModified = getLong(DocumentsContract.Document.COLUMN_LAST_MODIFIED) ?: 0L

NitroFileStat(
size = size.toDouble(),
isFile = isFile,
isDirectory = isDirectory,
ctime = lastModified.toDouble(),
mtime = lastModified.toDouble()
)
} ?: throw Error("Unable to stat content uri (query failed): $path")
}
}
}

fun readdir(path: String): Array<NitroFile> {
Expand Down Expand Up @@ -199,14 +264,45 @@ class NitroFSImpl(val context: ReactApplicationContext) {
return file.parent ?: ""
}

fun basename(path: String, ext: String?): String {
val file = File(path)
return file.nameWithoutExtension
fun basename(path: String): String {
val resolved = path.toResolvedPath() ?: throw Error("Invalid path: $path")
Comment thread
patrickkabwe marked this conversation as resolved.
return when(resolved) {
is ResolvedPath.FilePath -> resolved.file.name
is ResolvedPath.Content -> {
val uri = resolved.uri
val fileName = contentResolver.query(
uri, null, null, null, null
)?.use { cursor ->
if (cursor.moveToFirst()) {
val nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
if (nameIndex != -1) {
cursor.getString(nameIndex)
} else {
""
}
} else {
""
}
} ?: ""
Comment thread
patrickkabwe marked this conversation as resolved.
Outdated
fileName
}
}
}

fun extname(path: String): String {
val file = File(path)
return file.extension
val resolved = path.toResolvedPath() ?: throw Error("Invalid path: $path")

return when(resolved){
is ResolvedPath.FilePath -> resolved.file.extension
is ResolvedPath.Content -> {
val mimeType: String? = contentResolver.getType(resolved.uri)
if (mimeType != null) {
MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType) ?: ""
} else {
""
}
}
}
Comment thread
patrickkabwe marked this conversation as resolved.
}
Comment thread
patrickkabwe marked this conversation as resolved.

fun getDocumentDir(): String {
Expand Down
57 changes: 57 additions & 0 deletions android/src/main/java/com/nitrofs/ResolvedPath.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package com.nitrofs

import android.content.ContentResolver
import android.content.Context
import android.net.Uri
import java.io.File
import androidx.core.net.toUri
import java.io.InputStream
import java.io.OutputStream

sealed class ResolvedPath {
data class Content(val uri: Uri) : ResolvedPath()
data class FilePath(val file: File) : ResolvedPath()
}

fun String.toResolvedPath(): ResolvedPath? {
return when {
startsWith("content://") || startsWith("file://") ->
this.toUri().toResolvedPath()

else -> ResolvedPath.FilePath(File(this))
}
}

fun Uri.toResolvedPath(): ResolvedPath? {
return when (scheme) {
ContentResolver.SCHEME_CONTENT ->
ResolvedPath.Content(this)

ContentResolver.SCHEME_FILE -> {
val p = path ?: return null
ResolvedPath.FilePath(File(p))
}

else -> null
}
}

fun ResolvedPath.openInputStream(context: Context): InputStream? {
return when (this) {
is ResolvedPath.Content -> context.contentResolver.openInputStream(uri)
is ResolvedPath.FilePath -> if (file.exists()) file.inputStream() else null
}
}

fun ResolvedPath.openOutputStream(context: Context, append: Boolean = false): OutputStream? {
return when (this) {
is ResolvedPath.Content -> {
val mode = if (append) "wa" else "w"
context.contentResolver.openOutputStream(uri, mode)
}
is ResolvedPath.FilePath -> {
file.parentFile?.mkdirs()
file.outputStream()
}
}
}
3 changes: 3 additions & 0 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

34 changes: 34 additions & 0 deletions example/ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,36 @@ PODS:
- hermes-engine (0.81.0):
- hermes-engine/Pre-built (= 0.81.0)
- hermes-engine/Pre-built (0.81.0)
- NitroDocumentPicker (1.2.0):
- boost
- DoubleConversion
- fast_float
- fmt
- glog
- hermes-engine
- NitroModules
- RCT-Folly
- RCT-Folly/Fabric
- RCTRequired
- RCTTypeSafety
- React-callinvoker
- React-Core
- React-debug
- React-Fabric
- React-featureflags
- React-graphics
- React-ImageManager
- React-jsi
- React-NativeModulesApple
- React-RCTFabric
- React-renderercss
- React-rendererdebug
- React-utils
- ReactCodegen
- ReactCommon/turbomodule/bridging
- ReactCommon/turbomodule/core
- SocketRocket
- Yoga
- NitroFS (0.7.0):
- boost
- DoubleConversion
Expand Down Expand Up @@ -2380,6 +2410,7 @@ DEPENDENCIES:
- fmt (from `../../node_modules/react-native/third-party-podspecs/fmt.podspec`)
- glog (from `../../node_modules/react-native/third-party-podspecs/glog.podspec`)
- hermes-engine (from `../../node_modules/react-native/sdks/hermes-engine/hermes-engine.podspec`)
- NitroDocumentPicker (from `../../node_modules/react-native-nitro-document-picker`)
- NitroFS (from `../..`)
- NitroModules (from `../../node_modules/react-native-nitro-modules`)
- RCT-Folly (from `../../node_modules/react-native/third-party-podspecs/RCT-Folly.podspec`)
Expand Down Expand Up @@ -2470,6 +2501,8 @@ EXTERNAL SOURCES:
hermes-engine:
:podspec: "../../node_modules/react-native/sdks/hermes-engine/hermes-engine.podspec"
:tag: hermes-2025-07-07-RNv0.81.0-e0fc67142ec0763c6b6153ca2bf96df815539782
NitroDocumentPicker:
:path: "../../node_modules/react-native-nitro-document-picker"
NitroFS:
:path: "../.."
NitroModules:
Expand Down Expand Up @@ -2613,6 +2646,7 @@ SPEC CHECKSUMS:
fmt: a40bb5bd0294ea969aaaba240a927bd33d878cdd
glog: 5683914934d5b6e4240e497e0f4a3b42d1854183
hermes-engine: e7491a2038f2618c8cd444ed411a6deb350a3742
NitroDocumentPicker: 3f7adcb535ed9ac19a92a65c7228da559227ffdb
NitroFS: 5d5ad45cd2351ea71bbdc7f5bb45857fc1219e08
NitroModules: edd5870885e786b0f2119836cf47e8b28d5b9c1f
RCT-Folly: 846fda9475e61ec7bcbf8a3fe81edfcaeb090669
Expand Down
3 changes: 2 additions & 1 deletion example/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,14 @@
"android": "react-native run-android",
"ios": "react-native run-ios --simulator='iPhone 16'",
"lint": "eslint .",
"start": "react-native start --reset-cache --client-logs",
"start": "react-native start --client-logs --reset-cache",
"test": "jest",
"pod": "bundle install && bundle exec pod install --project-directory=ios"
},
"dependencies": {
"react": "19.1.0",
"react-native": "0.81.0",
"react-native-nitro-document-picker": "^1.2.0",
"react-native-nitro-modules": "^0.31.5",
"react-native-safe-area-context": "^5.6.2"
},
Expand Down
Loading
Loading