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
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,71 @@ class NitroFSImpl(val context: ReactApplicationContext) {
srcPath: String,
destPath: String
) {
val file = File(srcPath)
val dest = File(destPath)
file.copyTo(dest)
val src = srcPath.toResolvedPath() ?: throw Error("Invalid source path: $srcPath")
val dest = destPath.toResolvedPath() ?: throw Error("Invalid destination path: $destPath")

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,
)
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 +263,46 @@ 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, arrayOf(OpenableColumns.DISPLAY_NAME), null, null, null
)?.use { cursor ->
if (cursor.moveToFirst()) {
val nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
if (nameIndex != -1) {
cursor.getString(nameIndex)
} else {
null
}
} else {
null
}
} ?: throw Error("Unable to get file name from content URI: $path")

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) ?: throw Error("Unable to get extension from mime type: $mimeType")
} else {
throw Error("Unable to get mime type from content URI: $path")
}
}
}
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