Skip to content

Fix copy item functionality and improve path handling#128

Merged
patrickkabwe merged 8 commits intomainfrom
fix/copy-item-and-path-handling-improvements
Nov 16, 2025
Merged

Fix copy item functionality and improve path handling#128
patrickkabwe merged 8 commits intomainfrom
fix/copy-item-and-path-handling-improvements

Conversation

@patrickkabwe
Copy link
Copy Markdown
Owner

@patrickkabwe patrickkabwe commented Nov 16, 2025

Closes #98

Summary

This PR fixes several issues and improves path handling across iOS and Android platforms.

Changes

JavaScript/TypeScript

  • ✅ Fixed copy item button being disabled incorrectly (now enabled when file is selected)
  • ✅ Added destination existence validation before copying
  • ✅ Improved error handling with detailed error messages
  • ✅ Ensured parent directory exists before copy operations

iOS

  • ✅ Added path validation utilities (isValidPath, pathToURL)
  • ✅ Normalized all paths before processing operations
  • ✅ Fixed basename to always include file extension
  • ✅ Removed unused ext parameter from basename function
  • ✅ Improved error handling for invalid paths

Android

  • ✅ Fixed syntax errors in basename function for content URIs
  • ✅ Removed unused ext parameter from basename function
  • ✅ Ensured filename includes extension

API Changes

  • Breaking Change: basename(path: string, ext?: string)basename(path: string) (removed optional ext parameter)

Testing

  • Copy item functionality works correctly
  • Path validation works on iOS
  • Basename includes extension on both platforms
  • Error handling provides clear messages

Summary by CodeRabbit

  • New Features

    • Document picker integration and an Android-only "Copy DCIM images to cache" action exposed in the example UI and hooks.
  • Improvements

    • Added content-URI support and centralized path normalization for more reliable reads/writes, copies, deletes and metadata.
    • Basename API simplified and now returns the full filename (extension preserved).
  • Documentation

    • README updated to reflect the basename signature and behavior.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Nov 16, 2025

Walkthrough

Adds content:// URI support via a new ResolvedPath abstraction and stream-based handling, centralizes iOS path normalization, removes the optional ext parameter from basename across platforms, adds a File->NitroFileStat extension, and extends the example app with DCIM copy and document-pick features and hooks.

Changes

Cohort / File(s) Summary
Core Path Resolution
android/src/main/java/com/nitrofs/ResolvedPath.kt
New sealed ResolvedPath (Content
Android FS Implementation
android/src/main/java/com/nitrofs/NitroFSImpl.kt
Switches to ResolvedPath usage; adds content:// handling for exists, unlink, copy/write (stream-based), mkdir, stat, basename (now single-arg) and extname; uses DocumentsContract/OpenableColumns and MIME-based logic.
Android File Extensions
android/src/main/java/com/nitrofs/File+Extensions.kt
Adds fun File.toNitroFileStat(): NitroFileStat mapping File properties (size, isFile, isDirectory, ctime, mtime).
Android API Bridge
android/src/main/java/com/nitrofs/HybridNitroFS.kt
Updated bridge signature basename(path: String) (removed ext) and call propagation to impl.
iOS Implementation & Utils
ios/NitroFSImpl.swift, ios/NitroFSImpl+Utils.swift, ios/HybridNitroFs.swift
Centralized path validation/normalization (isValidPath, pathToURL, getPath); operations now normalize to URL-based paths; basename signature changed to single-arg.
Spec & Docs
src/specs/nitro-fs.nitro.ts, README.md
API and docs updated to reflect basename(path: string): string (removed optional ext) and examples showing basename returns filename including extension.
Example App & Hooks
example/package.json, example/src/components/action-panel.tsx, example/src/hooks/use-file-system.ts, example/App.tsx
Added react-native-nitro-document-picker dependency and start script tweak; UI adds "Copy DCIM" (Android) and "Pick Document" actions; useFileSystem exposes copyImagesFromDCIMToCache and pickDocument with new flows for DCIM copying and document picking/uploading.

Sequence Diagram(s)

sequenceDiagram
    participant App
    participant Hook as UseFileSystem
    participant Resolver as ResolvedPath\nResolver
    participant NitroFSImpl
    participant AndroidCR as Content\nResolver
    participant FS as File System

    App->>Hook: user action (stat / copy / pick)
    Hook->>Resolver: toResolvedPath(path)
    alt content:// URI
        Resolver-->>NitroFSImpl: ResolvedPath.Content(uri)
        rect rgba(120,180,160,0.12)
            NitroFSImpl->>AndroidCR: query metadata / open streams / delete
            AndroidCR-->>NitroFSImpl: metadata / stream / result
        end
    else file path
        Resolver-->>NitroFSImpl: ResolvedPath.FilePath(file)
        rect rgba(120,160,220,0.12)
            NitroFSImpl->>FS: file IO (read/write/delete)
            FS-->>NitroFSImpl: result
        end
    end
    NitroFSImpl-->>Hook: result / error
    Hook-->>App: update UI / notify user
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

  • Attention areas:
    • android/src/main/java/com/nitrofs/NitroFSImpl.kt — content URI branches, DocumentsContract/OpenableColumns queries, stream copy and error handling.
    • android/src/main/java/com/nitrofs/ResolvedPath.kt — stream open/close semantics and existence checks.
    • example/src/hooks/use-file-system.ts — new async flows (DCIM copy, document pick) and platform gating.
    • iOS changes (ios/NitroFSImpl.swift, ios/NitroFSImpl+Utils.swift) — path normalization and consistent error propagation.

Possibly related PRs

  • Feat/new dirs #121 — Modifies NitroFS directory handling and directory accessors (DCIM/PICTURES/MOVIES/MUSIC); likely related to the example DCIM copy and directory logic changes.

Poem

🐰 I hopped through streams and content URIs bright,

Resolved each path by day and by night.
Basenames now whole, extensions left near,
DCIM carrots carried to cache with cheer. 🥕

Pre-merge checks and finishing touches

❌ Failed checks (3 warnings)
Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning The PR partially addresses issue #98 by improving path handling and basename support for content URIs, but does not implement the requested NitroFileStat.filename property. Add the filename property to NitroFileStat or implement an alternative documented method to obtain filename for content:// paths as requested in issue #98.
Out of Scope Changes check ⚠️ Warning The PR includes out-of-scope changes: adding copyImagesFromDCIMToCache and pickDocument features in examples/hooks that are not mentioned in issue #98 objectives. Remove copyImagesFromDCIMToCache and pickDocument features or clarify their connection to issue #98 in the PR description.
Docstring Coverage ⚠️ Warning Docstring coverage is 7.69% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title accurately summarizes the main changes: fixing copy item functionality and improving path handling across platforms.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/copy-item-and-path-handling-improvements

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 10

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/specs/nitro-fs.nitro.ts (1)

99-105: ---

Update README.md documentation to reflect basename API change

The basename signature has been updated across specs, implementations, and examples to remove the ext parameter. However, the README.md (lines 243–251) still documents the old signature and shows examples passing the ext argument. Update the documentation to show only the new single-parameter signature:

  • Line 243: Change basename(path: string, ext?: string): string to basename(path: string): string
  • Line 251: Remove the example NitroFS.basename('/path/to/file.txt', '.txt') since the ext parameter no longer exists

All codebase implementations and example code are already aligned with the breaking change; only documentation needs updating.

🧹 Nitpick comments (10)
example/src/components/action-panel.tsx (2)

132-147: Copy/Rename buttons now rely on downstream validation instead of input‑based disabling

With disabled={!selectedFile}, users can tap Copy/Rename with an empty name and will hit the validation inside the hook instead of seeing the button disabled. That’s functionally safe but slightly noisier UX; if you want to guide users earlier, consider also disabling when !fileName.trim().


159-183: Android‑only DCIM/Document buttons correctly gated, but effectiveness of disabled={loading} depends on the hook

The new Android buttons and copyImagesButton style are wired cleanly and respect loading for opacity/disabled state. Note that this only works as intended if copyImagesFromDCIMToCache and pickDocument both toggle the shared loading flag in useFileSystem; currently pickDocument does not, so the “Pick a Document” button will never actually disable during its work.

Also applies to: 240-242

example/src/hooks/use-file-system.ts (2)

415-517: DCIM→cache copy flow is solid; minor naming and UX refinements possible

The Android‑gated DCIM scan correctly validates DCIM existence, filters by mime/extension, skips existing cache files, and tracks copy failures with aggregated logging and a summary Alert. Two small follow‑ups you might consider:

  • Rename the local files from readdir (line 434) to something like dcimFiles to avoid shadowing the hook‑level files state.
  • Optionally refresh the visible listing after a successful run (e.g., await listFiles(cachePath) or currentPath) if you want the new copies to immediately appear in the UI.

519-566: pickDocument lacks loading/progress finalization; align it with other async actions

The document‑picking flow is functionally correct—resolves metadata, copies into cache, derives name/extension, and uploads with progress—but it never toggles loading or resets uploadProgress when done. This means:

  • The Android button’s disabled={loading}/opacity won’t reflect the operation.
  • uploadProgress may remain at the last non‑zero value after completion or failure.

Bringing it in line with the other handlers will make behavior more predictable. For example:

   const pickDocument = async () => {
-    try {
-      const doc = await NitroDocumentPicker.pick({
+    try {
+      setLoading(true);
+      setUploadProgress(0);
+      const doc = await NitroDocumentPicker.pick({
         types: ['all'],
         multiple: false,
       });
@@
-      await NitroFS.uploadFile(nitroFile, uploadOptions, (uploadedBytes, totalBytes) => {
-        const progress = (uploadedBytes / totalBytes) * 100;
-        setUploadProgress(progress);
-      });
-    } catch (error) {
+      await NitroFS.uploadFile(
+        nitroFile,
+        uploadOptions,
+        (uploadedBytes, totalBytes) => {
+          const progress = (uploadedBytes / totalBytes) * 100;
+          setUploadProgress(progress);
+        },
+      );
+      setUploadProgress(0);
+    } catch (error) {
       console.error('Error picking document:', error);
       Alert.alert('Error', `Failed to pick document: ${error}`);
-    }
+    } finally {
+      setLoading(false);
+    }
   };
ios/NitroFSImpl+Utils.swift (1)

9-46: Path validation helper is very permissive; consider aligning docs/behavior

isValidPath(_:) currently only rejects empty strings and malformed file:// URIs; all other inputs (including obviously invalid ones) return true, even though the comment suggests broader validation. If that’s intentional, consider either tightening the checks (e.g., rejecting unsupported schemes) or updating the doc comment to clarify that “validation” here is minimal and that pathToURL(_:) is mainly a normalization entry point rather than a strict validator.

android/src/main/java/com/nitrofs/File+Extensions.kt (1)

6-13: File → NitroFileStat mapping is straightforward; ctime/mtime semantics to confirm

The extension is a clean, direct mapping. The only nuance is that both ctime and mtime are derived from lastModified(), which means creation and modification times are identical at this layer. If the consumer APIs or JS/TS typings treat ctime as “creation time”, it might be worth documenting this or, if needed later, switching to a source that can distinguish the two (e.g., java.nio.file attributes on supported API levels).

ios/NitroFSImpl.swift (2)

18-29: Path normalization via getPath/pathToURL is a solid improvement

Routing most filesystem operations through getPath(path:) (and ultimately pathToURL(_:)) is a good step toward consistent handling of raw paths and file:// URIs: exists, writeFile, readFile, copy, mkdir, stat, readdir, and rename all benefit from the same normalization and error surface.

Two points to consider:

  1. Behavior for invalid paths differs between methods

    • exists(path:) now returns false on invalid/unnormalizable paths (via isValidPath / try? getPath).
    • Most other methods throw NitroFSError.encodingError when normalization fails.
      If JS callers expect a thrown error rather than false for invalid inputs, you might want to align exists with the others, or at least document that exists treats invalid paths as “not found”.
  2. Ensuring destination parent directories exist in copy

    • On Android, ResolvedPath.openOutputStream ensures parent directories exist for FilePath via file.parentFile?.mkdirs().
    • On iOS, copy(source:destination:) currently assumes the destination’s parent directory already exists and will throw otherwise.

    For cross‑platform parity and a better DX around “copy item”, consider mirroring the Android behavior by creating the destination’s parent directory (with withIntermediateDirectories: true) when it doesn’t exist before calling copyItem(atPath:toPath:).

Also applies to: 37-45, 50-52, 85-89, 103-104, 111-113, 117-118, 138-139, 181-189


121-123: Stat now uses normalized paths; IsDirectory still uses FileManager.default

Using normalizedPath consistently in stat(path:) and IsDirectory(_:) strengthens correctness when callers pass file:// URIs or other normalized forms. One minor note: IsDirectory(_:) still relies on FileManager.default rather than the injected fileManager. If NitroFSImpl is ever used with a custom FileManager (e.g., for sandboxing or testing), you may eventually want to delegate directory checks through that same instance for full consistency.

Also applies to: 253-256

android/src/main/java/com/nitrofs/ResolvedPath.kt (1)

11-57: ResolvedPath abstraction is clean; be explicit about null/unsupported schemes handling

The ResolvedPath sealed class and its helpers (String.toResolvedPath, Uri.toResolvedPath, openInputStream, openOutputStream) nicely centralize handling of content:// vs filesystem paths and ensure parent directories exist for FilePath writes.

A couple of behavioral points to keep in mind at call sites:

  • String.toResolvedPath() and Uri.toResolvedPath() return null for unsupported schemes (e.g. http://) or malformed file:// URIs (no path). Callers in NitroFSImpl should consistently convert these nulls into a domain error (e.g., a NitroFSError variant) rather than letting them bubble up as NullPointerExceptions.
  • openInputStream returns null when a FilePath doesn’t exist, but propagates exceptions for content resolver failures. Similarly, openOutputStream returns null if the resolver returns null. Callers should explicitly handle the null case so that JS receives a clear, intentional error instead of a late NPE.

Overall, the structure is solid; the main work is making sure the use sites translate null into consistent, user-facing errors.

android/src/main/java/com/nitrofs/NitroFSImpl.kt (1)

148-164: Consider creating parent directories for file path destinations.

Unlike writeFile() (lines 59-64), this method doesn't ensure parent directories exist before copying to a FilePath destination. This could cause silent failures.

Consider adding parent directory validation:

fun copyFile(
    srcPath: String,
    destPath: String
) {
    val src = srcPath.toResolvedPath() ?: throw RuntimeException("Invalid source path: $srcPath")
    val dest = destPath.toResolvedPath() ?: throw RuntimeException("Invalid destination path: $destPath")
    
    // Ensure parent directory exists for FilePath destinations
    if (dest is ResolvedPath.FilePath) {
        val parentDir = dest.file.parentFile
        if (parentDir != null && !parentDir.exists()) {
            if (!parentDir.mkdirs()) {
                throw RuntimeException("Failed to create parent directories for: $destPath")
            }
        }
    }

    try {
        src.openInputStream(context)?.use { input ->
            dest.openOutputStream(context)?.use { output ->
                input.copyTo(output, DEFAULT_BUFFER_SIZE)
            }
        }
    } catch (e: Exception) {
        throw RuntimeException("Failed to copy file: ${e.message}")
    }
}
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 270b998 and dac8a7a.

⛔ Files ignored due to path filters (10)
  • bun.lock is excluded by !**/*.lock
  • example/ios/Podfile.lock is excluded by !**/*.lock
  • nitrogen/generated/android/c++/JHybridNitroFSSpec.cpp is excluded by !**/generated/**
  • nitrogen/generated/android/c++/JHybridNitroFSSpec.hpp is excluded by !**/generated/**
  • nitrogen/generated/android/kotlin/com/margelo/nitro/nitrofs/HybridNitroFSSpec.kt is excluded by !**/generated/**
  • nitrogen/generated/ios/NitroFS-Swift-Cxx-Bridge.hpp is excluded by !**/generated/**
  • nitrogen/generated/ios/c++/HybridNitroFSSpecSwift.hpp is excluded by !**/generated/**
  • nitrogen/generated/ios/swift/HybridNitroFSSpec.swift is excluded by !**/generated/**
  • nitrogen/generated/ios/swift/HybridNitroFSSpec_cxx.swift is excluded by !**/generated/**
  • nitrogen/generated/shared/c++/HybridNitroFSSpec.hpp is excluded by !**/generated/**
📒 Files selected for processing (11)
  • android/src/main/java/com/nitrofs/File+Extensions.kt (1 hunks)
  • android/src/main/java/com/nitrofs/HybridNitroFS.kt (1 hunks)
  • android/src/main/java/com/nitrofs/NitroFSImpl.kt (4 hunks)
  • android/src/main/java/com/nitrofs/ResolvedPath.kt (1 hunks)
  • example/package.json (1 hunks)
  • example/src/components/action-panel.tsx (6 hunks)
  • example/src/hooks/use-file-system.ts (1 hunks)
  • ios/HybridNitroFs.swift (1 hunks)
  • ios/NitroFSImpl+Utils.swift (1 hunks)
  • ios/NitroFSImpl.swift (5 hunks)
  • src/specs/nitro-fs.nitro.ts (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (5)
src/specs/nitro-fs.nitro.ts (1)
nitrogen/generated/android/c++/JHybridNitroFSSpec.hpp (10)
  • path (64-64)
  • path (65-65)
  • path (66-66)
  • path (69-69)
  • path (70-70)
  • path (71-71)
  • path (72-72)
  • path (74-74)
  • path (75-75)
  • path (76-76)
android/src/main/java/com/nitrofs/NitroFSImpl.kt (1)
src/type.ts (1)
  • NitroFileStat (42-48)
ios/HybridNitroFs.swift (4)
android/src/main/java/com/nitrofs/HybridNitroFS.kt (1)
  • basename (161-168)
android/src/main/java/com/nitrofs/NitroFSImpl.kt (1)
  • basename (267-290)
ios/NitroFSImpl.swift (1)
  • basename (202-205)
nitrogen/generated/android/kotlin/com/margelo/nitro/nitrofs/HybridNitroFSSpec.kt (1)
  • basename (123-125)
ios/NitroFSImpl.swift (1)
ios/NitroFSImpl+Utils.swift (3)
  • isValidPath (13-26)
  • getPath (45-48)
  • pathToURL (29-42)
example/src/hooks/use-file-system.ts (3)
example/src/types/index.ts (2)
  • FileItem (1-6)
  • DirectoryType (8-8)
example/src/utils/file-utils.ts (1)
  • getMimeTypeFromExtension (82-142)
src/type.ts (1)
  • NitroUploadOptions (5-19)
🪛 detekt (1.23.8)
android/src/main/java/com/nitrofs/NitroFSImpl.kt

[warning] 161-161: The caught exception is swallowed. The original exception could be lost.

(detekt.exceptions.SwallowedException)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: Build Android Example App (new architecture)
  • GitHub Check: Build Android Example App (old architecture)
  • GitHub Check: Build iOS Example App (old architecture)
  • GitHub Check: Build iOS Example App (new architecture)
🔇 Additional comments (9)
example/src/hooks/use-file-system.ts (2)

9-29: Initialization and directory listing look correct; consider parallelizing stats if performance matters

Bootstrapping from NitroFS.DOCUMENT_DIR via listFiles and capturing FileItem metadata with per‑entry stat is logically sound, and loading/error handling is consistent. If you expect large directories, you may want to parallelize the stat calls (e.g., await Promise.all(...)) to avoid N+1 sequential I/O, but for an example app this is acceptable as‑is.

Also applies to: 31-59


266-305: Verified: No remaining callers expect older copy behavior

Searched the codebase and confirmed copyItem has only two reference points: exported from the hook at line 587 and passed as onCopyItem prop to a single component in App.tsx. No other callers discovered. The enhanced validation and error handling in the new copyItem implementation do not create compatibility issues—the function is the sole consumer of this behavior, and its interface remains unchanged from the caller's perspective.

ios/HybridNitroFs.swift (1)

150-153: iOS basename wrapper matches new core implementation and error‑handling pattern

The updated basename(path: String) correctly delegates to nitroFSImpl.basename(path:) and mirrors the error logging/propagation used in dirname and extname, keeping the Swift bridge consistent with the new, simplified API.

example/package.json (1)

9-16: Dependency and start script changes are correct

The react-native-nitro-document-picker addition is verified—it's imported and used in example/src/hooks/use-file-system.ts at line 521. The start script flag reordering is benign. No changes needed.

android/src/main/java/com/nitrofs/HybridNitroFS.kt (1)

161-164: Basename wrapper correctly updated to new API

The basename(path: String) override cleanly forwards to nitroFsImpl.basename(path) and keeps the existing error-handling pattern, so the Android public API is consistent with the implementation and the cross‑platform signature change.

android/src/main/java/com/nitrofs/NitroFSImpl.kt (4)

5-6: LGTM: Imports support Content URI operations.

The new imports are correctly used for Content URI metadata queries in stat() and basename().


23-23: LGTM: Content resolver properly initialized.


25-39: LGTM: Content URI existence check is correct.

The implementation properly handles both file paths and Content URIs with efficient querying and proper cursor cleanup.


41-49: LGTM: Deletion correctly handles both path types.

Using deleteRecursively() for file paths appropriately handles both files and directories, and Content URI deletion is correct.

Comment thread android/src/main/java/com/nitrofs/NitroFSImpl.kt Outdated
Comment thread android/src/main/java/com/nitrofs/NitroFSImpl.kt
Comment thread android/src/main/java/com/nitrofs/NitroFSImpl.kt
Comment thread android/src/main/java/com/nitrofs/NitroFSImpl.kt
Comment thread android/src/main/java/com/nitrofs/NitroFSImpl.kt
Comment thread android/src/main/java/com/nitrofs/NitroFSImpl.kt
Comment thread android/src/main/java/com/nitrofs/NitroFSImpl.kt Outdated
Comment thread android/src/main/java/com/nitrofs/NitroFSImpl.kt
Comment thread example/src/components/action-panel.tsx
Comment thread ios/NitroFSImpl.swift
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between dac8a7a and 73a3a3d.

📒 Files selected for processing (2)
  • README.md (1 hunks)
  • example/App.tsx (2 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: Build iOS Example App (old architecture)
  • GitHub Check: Build iOS Example App (new architecture)
  • GitHub Check: Build Android Example App (old architecture)
  • GitHub Check: Build Android Example App (new architecture)
🔇 Additional comments (1)
example/App.tsx (1)

37-38: LGTM! ActionPanel interface correctly accepts new props.

Verification confirms the ActionPanel interface has been properly updated with onCopyImagesFromDCIMToCache and onPickDocument (both () => void). The props are correctly destructured and used in the component. Type signatures match between App.tsx and ActionPanel—no issues found.

Comment thread README.md
…thod

- Enhance error handling in Android's path resolution to throw descriptive errors for invalid paths.
- Update iOS's dirname method to return the correct path instead of the absolute string representation.
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (3)
android/src/main/java/com/nitrofs/NitroFSImpl.kt (3)

266-288: Tighten basename() error handling for content URIs.

For file paths this correctly returns the name including extension, but for content URIs failure cases currently degrade to an empty string, which can hide real errors (query failure, missing DISPLAY_NAME, etc.).

You can make failures explicit and narrow the projection like this:

     fun basename(path: String): String {
-        val resolved = path.toResolvedPath() ?: throw Error("Invalid path: $path")
+        val resolved = path.toResolvedPath() ?: throw RuntimeException("Invalid path: $path")
         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 {
-                        ""
-                    }
-                } ?: ""
-                fileName
+                val fileName = contentResolver.query(
+                    uri,
+                    arrayOf(OpenableColumns.DISPLAY_NAME),
+                    null,
+                    null,
+                    null
+                )?.use { cursor ->
+                    if (!cursor.moveToFirst()) {
+                        null
+                    } else {
+                        val nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
+                        if (nameIndex != -1 && !cursor.isNull(nameIndex)) {
+                            cursor.getString(nameIndex)
+                        } else {
+                            null
+                        }
+                    }
+                } ?: throw RuntimeException("Unable to get basename for content uri: $path")
+
+                fileName
             }
         }
     }

This keeps the “basename includes extension” behavior while failing loudly when the provider cannot supply a name.

Is `OpenableColumns.DISPLAY_NAME` the recommended column for obtaining a display name for Android content URIs (e.g., from the Storage Access Framework)?

152-163: Standardize on a single exception type instead of mixing Error and RuntimeException.

Within this class some methods now throw RuntimeException (writeFile, readFile, downloadFile), while others in these ranges throw Error for similar kinds of failures. Mixing these two unchecked types can be surprising for callers and makes error handling less consistent.

It would be cleaner to standardize on RuntimeException for all operational failures in copyFile, mkdir, stat, basename, and extname (and optionally older methods like readdir/rename) so consumers see a consistent exception surface.

Also applies to: 167-173, 175-177, 214-215, 266-268, 292-293


152-163: Handle null streams and preserve the original cause in copyFile.

Two issues here:

  • openInputStream / openOutputStream are nullable; if either returns null, the copy silently does nothing and still appears successful.
  • The catch block wraps Exception in a new Error without the original cause, which is what detekt flags as a swallowed exception.

Consider tightening this implementation as follows:

     fun copyFile(
         srcPath: String,
         destPath: String
     ) {
-        val src = srcPath.toResolvedPath() ?:  throw Error("Invalid source path: $srcPath")
-        val dest = destPath.toResolvedPath() ?: throw Error("Invalid destination path: $destPath")
+        val src = srcPath.toResolvedPath()
+            ?: throw RuntimeException("Invalid source path: $srcPath")
+        val dest = destPath.toResolvedPath()
+            ?: throw RuntimeException("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}")
-        }
+        try {
+            val input = src.openInputStream(context)
+                ?: throw RuntimeException("Unable to open input stream for: $srcPath")
+            val output = dest.openOutputStream(context)
+                ?: throw RuntimeException("Unable to open output stream for: $destPath")
+
+            input.use { inStream ->
+                output.use { outStream ->
+                    inStream.copyTo(outStream, DEFAULT_BUFFER_SIZE)
+                }
+            }
+        } catch (e: Exception) {
+            // Keeps full stack trace and satisfies detekt's swallowed-exception check
+            throw RuntimeException("Failed to copy file from $srcPath to $destPath", e)
+        }
     }

This makes failures explicit, standardizes on RuntimeException, and preserves the original stack trace.

#!/bin/bash
# Inspect definitions of openInputStream/openOutputStream to confirm they are nullable and behave as expected.
rg -n "openInputStream" -S
rg -n "openOutputStream" -S
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 73a3a3d and 55eaa58.

📒 Files selected for processing (2)
  • android/src/main/java/com/nitrofs/NitroFSImpl.kt (4 hunks)
  • ios/NitroFSImpl.swift (5 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
android/src/main/java/com/nitrofs/NitroFSImpl.kt (1)
src/type.ts (1)
  • NitroFileStat (42-48)
ios/NitroFSImpl.swift (1)
ios/NitroFSImpl+Utils.swift (3)
  • isValidPath (13-26)
  • getPath (45-48)
  • pathToURL (29-42)
🪛 detekt (1.23.8)
android/src/main/java/com/nitrofs/NitroFSImpl.kt

[warning] 161-161: The caught exception is swallowed. The original exception could be lost.

(detekt.exceptions.SwallowedException)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: Build iOS Example App (old architecture)
  • GitHub Check: Build iOS Example App (new architecture)
  • GitHub Check: Build Android Example App (old architecture)
  • GitHub Check: Build Android Example App (new architecture)
🔇 Additional comments (10)
ios/NitroFSImpl.swift (4)

22-29: LGTM: Defensive path validation in exists.

The addition of path validation and graceful error handling with try? is appropriate for an existence check. Returning false for invalid or non-normalizable paths aligns with standard exists semantics.


37-46: LGTM: Consistent path normalization across file operations.

All file operations now consistently normalize paths via getPath before processing. The pattern is applied uniformly across writeFile, readFile, copy, mkdir, stat, and readdir, which ensures reliable path handling throughout the implementation.

Also applies to: 50-78, 85-88, 103-104, 111-122, 130-148


192-194: LGTM: dirname correctly returns filesystem path.

The function now returns .path instead of .absoluteString, which ensures dirname returns a plain filesystem path (e.g., "/foo") rather than a URL string (e.g., "file:///foo"). This makes the return value directly consumable by other NitroFS methods and addresses the concern raised in the previous review.


202-204: Breaking change properly implemented and verified.

Verification confirms all callers of basename() have been updated to the new single-parameter signature. No calls to the old two-parameter variant remain in the codebase across ios/HybridNitroFs.swift, ios/NitroFSImpl.swift, and the generated bridge files.

android/src/main/java/com/nitrofs/NitroFSImpl.kt (6)

5-6: New imports align with content-URI handling.

DocumentsContract and OpenableColumns are appropriate for the new stat/basename implementations; no issues here.


23-24: Caching contentResolver is appropriate.

Storing context.contentResolver in a private val is fine given the ReactApplicationContext lifetime; avoids repeated lookups without leaking anything.


26-38: exists() logic for file vs content URIs looks correct.

Resolving the path first and using cursor.moveToFirst() as the existence test (with proper use-based closing) matches the boolean contract and cleanly handles invalid paths via false.


42-48: unlink() behavior is reasonable for both path types.

Resolving first and returning deleteRecursively() / delete(...) > 0 gives a clear success/failure signal while treating unresolved paths as false.


167-172: mkdir() behavior for file and content URIs is clear.

Using mkdirs() for filesystem paths and throwing for unsupported content URIs makes the unsupported case explicit; unresolved paths returning false is also reasonable.


176-215: Content-URI stat implementation looks robust.

The projection, helper accessors, and derivation of size, mtime, and directory/file flags from the MIME type are all sensible; the NitroFileStat mapping matches the expected shape.

Comment thread android/src/main/java/com/nitrofs/NitroFSImpl.kt
- Remove redundant fragment wrapping in ActionPanel component.
- Ensure proper rendering of elements within the component.
…ieval

- Update content resolver queries to throw descriptive errors when unable to retrieve file name or extension from content URIs.
- Ensure null values are handled appropriately instead of returning empty strings.
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (4)
android/src/main/java/com/nitrofs/NitroFSImpl.kt (4)

167-173: Use RuntimeException for consistency.

This method throws Error at line 171, but other methods in the class throw RuntimeException.

Apply this diff:

-        is ResolvedPath.Content -> throw Error("Cannot create directory from content URI: $path")
+        is ResolvedPath.Content -> throw RuntimeException("Cannot create directory from content URI: $path")

175-217: Use RuntimeException for consistency.

This method throws Error at lines 176, 188, and 214, but other methods in the class throw RuntimeException. The content URI handling logic is well-implemented with appropriate fallbacks.

Apply this diff:

-    val resolved = path.toResolvedPath() ?: throw Error("Invalid path: $path")
+    val resolved = path.toResolvedPath() ?: throw RuntimeException("Invalid path: $path")
     return when(resolved) {
         is ResolvedPath.FilePath -> resolved.file.toNitroFileStat()
         is ResolvedPath.Content -> {
             // ... existing code ...
             contentResolver.query(uri, projection, null, null, null)?.use { cursor ->
                 if (!cursor.moveToFirst()) {
-                    throw Error("Unable to stat content uri (no row): $path")
+                    throw RuntimeException("Unable to stat content uri (no row): $path")
                 }
                 // ... existing code ...
-            } ?: throw Error("Unable to stat content uri (query failed): $path")
+            } ?: throw RuntimeException("Unable to stat content uri (query failed): $path")
         }
     }

266-290: Use RuntimeException for consistency.

This method throws Error at lines 267 and 285, but other methods in the class throw RuntimeException. The content URI handling via DISPLAY_NAME is correct.

Apply this diff:

-    val resolved = path.toResolvedPath() ?: throw Error("Invalid path: $path")
+    val resolved = path.toResolvedPath() ?: throw RuntimeException("Invalid path: $path")
     return when(resolved) {
         is ResolvedPath.FilePath -> resolved.file.name
         is ResolvedPath.Content -> {
             // ... existing code ...
-            } ?: throw Error("Unable to get file name from content URI: $path")
+            } ?: throw RuntimeException("Unable to get file name from content URI: $path")
             
             fileName
         }
     }

292-306: CRITICAL: Return extension with leading dot to match documented API contract.

This critical issue was flagged in previous reviews and remains unaddressed. The documentation explicitly states that extname('/path/to/file.txt') should return .txt (with leading dot), matching Node.js semantics. However:

  • Line 296: resolved.file.extension returns 'txt' (no dot)
  • Line 300: MimeTypeMap.getSingleton().getExtensionFromMimeType(...) returns 'txt' (no dot)

Both must prepend '.' when a non-empty extension exists. Edge case: when no extension exists (e.g., hidden files like .env), return empty string ''.

Secondary issue: This method throws Error (lines 293, 300, 302) instead of RuntimeException used elsewhere in the class.

Apply this diff:

-    val resolved = path.toResolvedPath() ?: throw Error("Invalid path: $path")
+    val resolved = path.toResolvedPath() ?: throw RuntimeException("Invalid path: $path")

     return when(resolved){
-        is ResolvedPath.FilePath -> resolved.file.extension
+        is ResolvedPath.FilePath -> {
+            val ext = resolved.file.extension
+            if (ext.isEmpty()) "" else ".$ext"
+        }
         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")
+                val ext = MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType) 
+                    ?: throw RuntimeException("Unable to get extension from mime type: $mimeType")
+                if (ext.isEmpty()) "" else ".$ext"
             } else {
-                throw Error("Unable to get mime type from content URI: $path")
+                throw RuntimeException("Unable to get mime type from content URI: $path")
             }
         }
     }
🧹 Nitpick comments (1)
example/src/components/action-panel.tsx (1)

159-172: Consider removing unnecessary fragment.

The Android-specific block uses a fragment to wrap a single View. Since there's only one child element, the fragment can be removed for cleaner code.

 {Platform.OS === 'android' && (
-  <>
   <View style={styles.buttonRow}>
     <TouchableOpacity
       style={[styles.button, styles.copyImagesButton, { opacity: loading ? 0.5 : 1 }]}
       onPress={onCopyImagesFromDCIMToCache}
       disabled={loading}
     >
       <Text style={styles.buttonText}>Copy DCIM Images</Text>
     </TouchableOpacity>
   </View>
-  </>
 )}
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 55eaa58 and 260725e.

📒 Files selected for processing (2)
  • android/src/main/java/com/nitrofs/NitroFSImpl.kt (4 hunks)
  • example/src/components/action-panel.tsx (6 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
android/src/main/java/com/nitrofs/NitroFSImpl.kt (1)
src/type.ts (1)
  • NitroFileStat (42-48)
🪛 detekt (1.23.8)
android/src/main/java/com/nitrofs/NitroFSImpl.kt

[warning] 161-161: The caught exception is swallowed. The original exception could be lost.

(detekt.exceptions.SwallowedException)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: Build iOS Example App (old architecture)
  • GitHub Check: Build iOS Example App (new architecture)
  • GitHub Check: Build Android Example App (new architecture)
  • GitHub Check: Build Android Example App (old architecture)
🔇 Additional comments (10)
android/src/main/java/com/nitrofs/NitroFSImpl.kt (4)

5-6: LGTM!

The new imports are necessary for content URI support via DocumentsContract queries and OpenableColumns metadata access.


23-23: LGTM!

Caching the contentResolver reference is appropriate and improves code readability throughout the class.


25-39: LGTM!

The unified path handling via toResolvedPath() correctly supports both file paths and content URIs. The minimal projection for content URI queries is efficient.


41-49: LGTM!

The implementation correctly handles both file paths and content URIs with appropriate deletion methods for each type.

example/src/components/action-panel.tsx (6)

3-3: LGTM!

The Platform import is correctly used for the Android-specific UI check on line 159.


24-25: LGTM!

The new callback props are correctly typed and the past review confirmed that all call sites have been updated.


40-41: LGTM!

The props are correctly destructured and match the interface definition.


240-242: LGTM!

The new button style is well-defined with a distinct color that differentiates the new actions from existing buttons.


136-136: Parent component properly validates empty names—no issue.

Verification confirms that both copyItem and renameItem callbacks in the parent component validate the newName parameter with if (!newName.trim()) before executing any operations. When an empty or whitespace-only name is passed, an error alert is shown and the operation returns early. Therefore, removing the !fileName check from the disabled state is safe, and the PR change is correct.


173-183: No changes required; code correctly supports cross-platform document picking.

The react-native-nitro-document-picker library supports both iOS (UIDocumentPickerViewController) and Android (Intent.ACTION_OPEN_DOCUMENT), and the pickDocument implementation properly handles errors across both platforms with user alerts. The button can safely render on all platforms.

Comment thread android/src/main/java/com/nitrofs/NitroFSImpl.kt
@patrickkabwe patrickkabwe merged commit 01c6bbf into main Nov 16, 2025
5 checks passed
@github-actions
Copy link
Copy Markdown

🎉 This PR is included in version 0.8.0 🎉

The release is available on:

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature]: Enable file loading from content:// paths

1 participant