Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -240,16 +240,16 @@ const dir = NitroFS.dirname('/path/to/file.txt')
// Returns: '/path/to'
```

#### `basename(path: string, ext?: string): string`
#### `basename(path: string): string`

Get the filename from a path, optionally removing extension.
Get the filename from a path, including the file extension.

```typescript
const name = NitroFS.basename('/path/to/file.txt')
// Returns: 'file.txt'

const nameWithoutExt = NitroFS.basename('/path/to/file.txt', '.txt')
// Returns: 'file'
const nameWithExt = NitroFS.basename('/path/to/document.pdf')
// Returns: 'document.pdf'
```
Comment thread
patrickkabwe marked this conversation as resolved.

#### `extname(path: string): string`
Expand Down
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.

4 changes: 4 additions & 0 deletions example/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ const AppContent = () => {
getPathInfo,
navigateToDirectoryType,
base64Encoding,
copyImagesFromDCIMToCache,
pickDocument,
} = useFileSystem();

return (
Expand Down Expand Up @@ -76,6 +78,8 @@ const AppContent = () => {
onCopyItem={copyItem}
onRenameItem={renameItem}
onBase64Encoding={base64Encoding}
onCopyImagesFromDCIMToCache={copyImagesFromDCIMToCache}
onPickDocument={pickDocument}
/>

<FileEditor
Expand Down
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
Loading
Loading