rn-file-toolkit 1.0.2 → 1.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,68 +1,119 @@
1
- # rn-file-toolkit
1
+ <div align="center">
2
+ <h1>rn-file-toolkit 🗂️</h1>
3
+ <p><b>The ultimate, unified native file management toolkit for React Native & Expo</b></p>
4
+
5
+ [![npm version](https://img.shields.io/npm/v/rn-file-toolkit.svg?style=for-the-badge&color=success)](https://www.npmjs.com/package/rn-file-toolkit)
6
+ [![npm downloads](https://img.shields.io/npm/dt/rn-file-toolkit.svg?style=for-the-badge)](https://www.npmjs.com/package/rn-file-toolkit)
7
+ [![TypeScript](https://img.shields.io/badge/TypeScript-Ready-blue.svg?style=for-the-badge&logo=typescript)](https://www.typescriptlang.org/)
8
+ [![platforms](https://img.shields.io/badge/Platforms-iOS%20%7C%20Android-lightgrey.svg?style=for-the-badge&logo=apple&logoColor=white)](https://github.com/chavan-labs/rn-file-toolkit)
9
+ [![license](https://img.shields.io/npm/l/rn-file-toolkit.svg?style=for-the-badge)](https://github.com/chavan-labs/rn-file-toolkit/blob/main/LICENSE)
10
+ </div>
2
11
 
3
- [![npm version](https://img.shields.io/npm/v/rn-file-toolkit.svg?style=flat-square)](https://www.npmjs.com/package/rn-file-toolkit)
4
- [![npm downloads](https://img.shields.io/npm/dm/rn-file-toolkit.svg?style=flat-square)](https://www.npmjs.com/package/rn-file-toolkit)
5
- [![license](https://img.shields.io/npm/l/rn-file-toolkit.svg?style=flat-square)](https://github.com/chavan-labs/rn-file-toolkit/blob/main/LICENSE)
6
- [![TypeScript](https://img.shields.io/badge/TypeScript-Ready-blue.svg?style=flat-square)](https://www.typescriptlang.org/)
7
- [![platforms](https://img.shields.io/badge/platforms-iOS%20%7C%20Android-lightgrey.svg?style=flat-square)](https://github.com/chavan-labs/rn-file-toolkit)
12
+ ---
13
+
14
+ **rn-file-toolkit** is the modern replacement for legacy file libraries. Download, upload, manage queues, extract archives, and interact with the filesystem—all powered by pure native implementations (Kotlin + Swift) with **zero third-party dependencies**.
15
+
16
+ ⭐ **Star this repo if you find it useful to help others discover it!**
17
+
18
+ ## 📖 Table of Contents
19
+ - [Why rn-file-toolkit?](#-why-rn-file-toolkit)
20
+ - [Why not Expo FileSystem?](#-why-not-expo-filesystem)
21
+ - [Installation](#-installation)
22
+ - [Quick Start: `useDownload`](#-quick-start-usedownload)
23
+ - [Core APIs](#-core-apis)
24
+ - [Background Downloads](#background-downloads)
25
+ - [Multipart Uploads](#multipart-uploads)
26
+ - [File System (FS)](#file-system-fs)
27
+ - [Zip & Unzip Archives](#zip--unzip-archives)
28
+ - [Media & Utilities](#media--utilities)
29
+ - [API Reference](#-api-reference)
30
+ - [Expo Support](#-expo-support)
31
+ - [Contributing](#-contributing)
32
+
33
+ ---
8
34
 
9
- The ultimate, unified file management library for React Native. Download, upload, manage queues, and interact with the filesystem—all powered by pure native implementations (Kotlin + Swift) with **zero third-party dependencies**.
35
+ ## 🚀 Why rn-file-toolkit?
10
36
 
11
- **Star this repo if you found it useful!**
37
+ Most React Native file solutions (`rn-fetch-blob`, `react-native-fs`) are fragmented, lightly maintained, or lack modern features. **rn-file-toolkit** gives you a unified, **TurboModule-compatible** API utilizing OS-native managers (`URLSession` on iOS, `DownloadManager` on Android) for reliable, battery-efficient operations.
38
+
39
+ ### ✨ Highlights
40
+ - 🪝 **Drop-in React Hooks:** Built-in state management (`useDownload`) for progress and controls.
41
+ - 📥 **Background Ready:** Downloads and uploads survive app suspension with automatic re-attachment.
42
+ - 🚦 **Smart Queueing:** Cap concurrency and set priorities without touching native code.
43
+ - 🛡️ **Resilient:** Auto-retries on network errors with exponential backoff and HTTP resume.
44
+ - 🗜️ **Zero-Dependency Zip:** Uses native `java.util.zip` and iOS `zlib`.
45
+ - 🗄️ **Rich File System API:** Comprehensive FS methods (`readFile`, `writeFile`, `copyFile`, `mkdir`, `stat`, etc.).
46
+ - 🛠️ **Expo Compatible:** Seamless integration with Expo custom dev clients.
12
47
 
13
48
  ---
14
49
 
15
- ## Why rn-file-toolkit?
16
- Most React Native file solutions are fragmented, bloated, or lack critical features like background queues or stable pause/resume. **rn-file-toolkit** gives you a unified, TurboModule-powered API utilizing OS-native download managers (`URLSession` on iOS, `DownloadManager` on Android) for reliable, battery-efficient file operations.
50
+ ## 🤔 Why not Expo FileSystem?
51
+
52
+ Many developers looking for an **"expo-file-system alternative"** or a **"better react native download manager"** come here. While Expo FileSystem is a great tool for basic file operations, `rn-file-toolkit` is built specifically to handle complex, real-world file management scenarios:
17
53
 
18
- ### Key Features
19
- - **Drop-in `useDownload()` Hook:** Built-in state management (`status`, `progress`, `result`) and controls (`pause`, `resume`, `cancel`).
20
- - **Background Downloads:** Survives app suspension with automatic re-attachment capabilities.
21
- - **Smart Queueing:** Cap concurrency and set priorities (`high`/`normal`) without native configuration.
22
- - **Resilient:** Auto-retries on network errors with exponential backoff and Range-based HTTP resume.
23
- - **Zero-Dependency Zip/Unzip:** Uses native `java.util.zip` and iOS `zlib` for extracting/compressing files.
24
- - **Rich File System API:** `readFile`, `writeFile`, `copyFile`, `moveFile`, `mkdir`, `ls`, `stat`, `exists`, and `deleteFile`.
25
- - **Media Utilities:** Base64 encoding/decoding, URL to Base64 conversions, and native share/open dialogs.
54
+ - **Queueing & Concurrency:** Built-in smart queueing allows you to cap concurrent downloads and set priorities without writing complex JavaScript wrappers.
55
+ - **Native Download/Upload Managers:** We hook directly into OS-level managers (`URLSession` on iOS, `DownloadManager` on Android) for maximum reliability and battery efficiency.
56
+ - **Auto-Retries & Resiliency:** Native implementations handle network drops with exponential backoff and HTTP resume automatically.
57
+ - **Multipart Uploads:** Robust, memory-efficient multipart uploads for large media are built right in.
58
+ - **Drop-in React Hooks:** Provides a `useDownload` hook out-of-the-box for instant progress tracking, speed, ETA, and state controls.
59
+ - **Background Persistence:** Downloads and uploads survive app suspension and backgrounding, automatically re-attaching when the app resumes.
26
60
 
27
61
  ---
28
62
 
29
- ## Installation
63
+ ## 📦 Installation
30
64
 
31
- ```sh
65
+ ```bash
66
+ # npm
32
67
  npm install rn-file-toolkit
68
+
69
+ # yarn
70
+ yarn add rn-file-toolkit
71
+
72
+ # pnpm
73
+ pnpm add rn-file-toolkit
33
74
  ```
34
75
 
76
+ *(Optional) If you are not using Expo or an auto-linking setup, run `pod install` in your `ios` directory.*
77
+
35
78
  ---
36
79
 
37
- ## Quick Start: `useDownload` Hook
80
+ ## Quick Start: `useDownload`
38
81
 
39
- The easiest way to manage a download inside a React component. Get status, rich progress (with speed & ETA), and full controls out of the box.
82
+ The easiest way to manage a download inside a React component. Get status, rich progress (with speed & ETA), and full controls instantly.
40
83
 
41
84
  ```tsx
85
+ import React from 'react';
86
+ import { View, Text, Button } from 'react-native';
42
87
  import { useDownload } from 'rn-file-toolkit';
43
88
 
44
- function DownloadScreen() {
89
+ export default function DownloadScreen() {
45
90
  const { start, pause, resume, cancel, status, progress, result } = useDownload();
46
91
 
47
92
  return (
48
- <View>
93
+ <View style={{ padding: 20 }}>
49
94
  <Button
50
- title="Download"
51
- onPress={() => start({ url: 'https://example.com/video.mp4', destination: 'documents' })}
95
+ title="Start Download"
96
+ onPress={() => start({
97
+ url: 'https://example.com/large-video.mp4',
98
+ destination: 'documents'
99
+ })}
52
100
  />
53
101
 
54
102
  {status === 'downloading' && progress && (
55
- <View>
56
- <Text>{progress.percent.toFixed(1)}% ({(progress.speedBps / 1024).toFixed(1)} KB/s)</Text>
57
- <Text>ETA: {progress.etaSeconds.toFixed(0)}s</Text>
58
- <Button title="Pause" onPress={pause} />
59
- <Button title="Cancel" onPress={cancel} />
103
+ <View style={{ marginTop: 20 }}>
104
+ <Text>Progress: {progress.percent.toFixed(1)}%</Text>
105
+ <Text>Speed: {(progress.speedBps / 1024 / 1024).toFixed(2)} MB/s</Text>
106
+ <Text>ETA: {progress.etaSeconds.toFixed(0)} seconds</Text>
107
+ <View style={{ flexDirection: 'row', gap: 10, marginTop: 10 }}>
108
+ <Button title="Pause" onPress={pause} />
109
+ <Button title="Cancel" onPress={cancel} color="red" />
110
+ </View>
60
111
  </View>
61
112
  )}
62
113
 
63
114
  {status === 'paused' && <Button title="Resume" onPress={resume} />}
64
- {status === 'done' && <Text>✅ Saved to: {result?.filePath}</Text>}
65
- {status === 'error' && <Text>❌ {result?.error}</Text>}
115
+ {status === 'done' && <Text style={{ color: 'green' }}>✅ Saved: {result?.filePath}</Text>}
116
+ {status === 'error' && <Text style={{ color: 'red' }}>❌ Error: {result?.error}</Text>}
66
117
  </View>
67
118
  );
68
119
  }
@@ -70,103 +121,136 @@ function DownloadScreen() {
70
121
 
71
122
  ---
72
123
 
73
- ## Core APIs
124
+ ## 🛠️ Core APIs
74
125
 
75
- ### `download(options)`
76
- For programmatic, queue-aware background downloads.
126
+ ### Background Downloads
127
+ For programmatic, queue-aware background downloads outside of React components.
77
128
 
78
- ```javascript
129
+ ```typescript
79
130
  import { download, setQueueOptions } from 'rn-file-toolkit';
80
131
 
81
- // Optional: Configure global concurrency queue
132
+ // Optimize global concurrency
82
133
  setQueueOptions({ maxConcurrent: 3 });
83
134
 
84
135
  const result = await download({
85
136
  url: 'https://example.com/file.pdf',
86
137
  destination: 'documents', // 'downloads' | 'cache' | 'documents'
87
- queue: true, // Joins the managed queue
88
- priority: 'high', // 'high' | 'normal'
138
+ queue: true, // Join the managed queue
139
+ priority: 'high', // 'high' | 'normal'
89
140
  retry: { attempts: 3, delay: 1000 },
90
- checksum: { hash: 'd41d8cd...', algorithm: 'md5' },
91
- onProgress: (p) => console.log(`${p.percent.toFixed(1)}%`),
141
+ onProgress: (p) => console.log(`${p.percent.toFixed(1)}% downloaded`),
92
142
  });
93
143
  ```
94
144
 
95
- ### `upload(options)`
96
- Robust, memory-efficient multipart file uploading.
145
+ ### Multipart Uploads
146
+ Robust, memory-efficient multipart file uploading for large media or documents.
97
147
 
98
- ```javascript
148
+ ```typescript
99
149
  import { upload } from 'rn-file-toolkit';
100
150
 
101
151
  const result = await upload({
102
- url: 'https://example.com/api/upload',
103
- filePath: '/path/to/my_image.jpg',
104
- fieldName: 'avatar',
105
- parameters: { userId: '123' },
106
- onProgress: (p) => console.log(`Uploading: ${p}%`),
152
+ url: 'https://api.example.com/v1/upload',
153
+ filePath: '/path/to/local/image.jpg',
154
+ fieldName: 'file',
155
+ parameters: { userId: '123', folder: 'avatars' },
156
+ onProgress: (percent) => console.log(`Uploading: ${percent}%`),
107
157
  });
108
158
  ```
109
159
 
110
- ### Filesystem (`fs`)
111
- Perform native filesystem operations directly.
160
+ ### File System (FS)
161
+ Perform native filesystem operations securely.
112
162
 
113
- ```javascript
163
+ ```typescript
114
164
  import { fs } from 'rn-file-toolkit';
115
165
 
116
- await fs.exists('/path/to/file.txt');
117
- await fs.stat('/path/to/file.txt'); // { size, modified, isDir ... }
118
- await fs.writeFile('/path/to/file.txt', 'hello', 'utf8'); // Supports 'base64'
119
- await fs.copyFile('/src/file.txt', '/dst/file.txt');
120
- await fs.mkdir('/path/to/folder');
121
- const files = await fs.ls('/path/to/folder');
166
+ // Check & Inspect
167
+ const exists = await fs.exists('/path/to/data.json');
168
+ const stats = await fs.stat('/path/to/data.json'); // { size, modified, isDir }
169
+
170
+ // Read & Write
171
+ await fs.writeFile('/path/to/data.txt', 'Hello World', 'utf8'); // Also supports 'base64'
172
+ const content = await fs.readFile('/path/to/data.txt', 'utf8');
173
+
174
+ // Manage Folders & Files
175
+ await fs.mkdir('/path/to/new_folder');
176
+ const files = await fs.ls('/path/to/new_folder');
177
+ await fs.copyFile('/path/src.txt', '/path/dest.txt');
178
+ await fs.moveFile('/path/old.txt', '/path/new.txt');
179
+ await fs.deleteFile('/path/unwanted.txt');
122
180
  ```
123
181
 
124
- ### Zip & Unzip
125
- Compress and extract archives securely.
182
+ ### Zip & Unzip Archives
183
+ Compress and extract archives directly on the device.
126
184
 
127
- ```javascript
185
+ ```typescript
128
186
  import { unzip, zip } from 'rn-file-toolkit';
129
187
 
130
- await unzip('/path/to/assets.zip', '/path/to/output-folder');
131
- await zip('/path/to/document.pdf', '/path/to/document.zip');
188
+ // Extract a downloaded zip
189
+ await unzip('/path/to/bundle.zip', '/path/to/extract-folder');
190
+
191
+ // Compress user data before uploading
192
+ await zip('/path/to/user-data-folder', '/path/to/backup.zip');
132
193
  ```
133
194
 
134
195
  ### Media & Utilities
196
+ Helpful tools for sharing, opening, and encoding files.
135
197
 
136
- ```javascript
198
+ ```typescript
137
199
  import { saveBase64AsFile, urlToBase64, shareFile, openFile } from 'rn-file-toolkit';
138
200
 
139
- // Convert base64 / Data URIs to files
140
- await saveBase64AsFile({ base64Data: 'data:image/png;base64,...', destination: 'documents' });
201
+ // Base64 to File
202
+ await saveBase64AsFile({
203
+ base64Data: 'data:image/png;base64,...',
204
+ destination: 'documents',
205
+ fileName: 'image.png'
206
+ });
141
207
 
142
- // Fetch remote media as base64
143
- await urlToBase64({ url: 'https://example.com/photo.jpg' });
208
+ // URL to Base64 (Great for caching small images)
209
+ const b64 = await urlToBase64({ url: 'https://example.com/icon.png' });
144
210
 
145
- // Share with native dialog
146
- await shareFile({ filePath: '/path/to/document.pdf' });
211
+ // Native Share Sheet
212
+ await shareFile({ filePath: '/path/to/report.pdf' });
147
213
 
148
- // Open with default app
149
- await openFile({ filePath: '/path/to/document.pdf', mimeType: 'application/pdf' });
214
+ // Open with default system app
215
+ await openFile({ filePath: '/path/to/report.pdf', mimeType: 'application/pdf' });
150
216
  ```
151
217
 
152
218
  ---
153
219
 
154
- ## API Type Reference
155
-
156
- | Type | Fields |
157
- | -------------------- | ------ |
158
- | `DownloadOptions` | `url`, `fileName?`, `background?`, `headers?`, `destination?`, `notificationTitle?`, `notificationDescription?`, `checksum?`, `onProgress?`, `retry?`, `queue?`, `priority?` |
159
- | `ProgressInfo` | `percent`, `bytesDownloaded`, `totalBytes`, `speedBps`, `etaSeconds` |
160
- | `RetryOptions` | `attempts`, `delay?`, `onRetry?` |
161
- | `QueueOptions` | `maxConcurrent?` |
162
- | `DownloadResult` | `success`, `filePath?`, `downloadId?`, `error?` |
163
- | `UploadOptions` | `url`, `filePath`, `fieldName?`, `headers?`, `parameters?`, `onProgress?`, `uploadId?` |
164
- | `UploadResult` | `success`, `status?`, `data?`, `error?`, `uploadId?` |
165
- | `UseDownloadReturn` | `start`, `pause`, `resume`, `cancel`, `status`, `progress`, `result`, `downloadId` |
166
- | `FsStat` | `path`, `name`, `size`, `modified`, `isDir` |
167
- | `UnzipResult` | `success`, `destDir?`, `files?`, `error?` |
168
- | `ZipResult` | `success`, `zipPath?`, `error?` |
220
+ ## 📚 API Reference
221
+
222
+ | Interface | Key Properties | Description |
223
+ | :--- | :--- | :--- |
224
+ | `DownloadOptions` | `url`, `destination`, `queue`, `retry`, `onProgress` | Configuration for downloading a file. |
225
+ | `UploadOptions` | `url`, `filePath`, `fieldName`, `parameters`, `onProgress` | Configuration for multipart uploads. |
226
+ | `ProgressInfo` | `percent`, `bytesDownloaded`, `speedBps`, `etaSeconds` | Rich real-time progress payload. |
227
+ | `UseDownloadReturn` | `start`, `pause`, `resume`, `cancel`, `status`, `progress` | Hook state and control methods. |
228
+ | `FsStat` | `size`, `modified`, `isDir` | Output of the filesystem `stat` method. |
229
+
230
+ *For advanced types and detailed parameter documentation, please refer to the source TypeScript definitions.*
231
+
232
+ ---
233
+
234
+ ## 🎪 Expo Support
235
+
236
+ **rn-file-toolkit** works seamlessly with Expo custom development clients (EAS Build / `npx expo run:android` / `npx expo run:ios`). Since it contains native code, it is not compatible with Expo Go.
237
+
238
+ An Expo config plugin is included automatically. No extra configuration is needed in your `app.json` unless you want to customize permissions.
239
+
240
+ ---
241
+
242
+ ## 🤝 Contributing
243
+
244
+ Contributions are welcome! If you find a bug or want to request a feature, please [open an issue](https://github.com/chavan-labs/rn-file-toolkit/issues).
245
+
246
+ 1. Fork the repository
247
+ 2. Create your feature branch (`git checkout -b feature/AmazingFeature`)
248
+ 3. Commit your changes (`git commit -m 'Add some AmazingFeature'`)
249
+ 4. Push to the branch (`git push origin feature/AmazingFeature`)
250
+ 5. Open a Pull Request
169
251
 
170
252
  ---
171
253
 
172
- _Made natively for the community 🤝 by Rohit Chavan_
254
+ <div align="center">
255
+ <i>Built with ❤️ for the React Native community by <a href="https://github.com/chavanRk">Rohit Chavan</a></i>
256
+ </div>
@@ -598,7 +598,7 @@ class FileToolkitModule(private val reactContext: ReactApplicationContext) :
598
598
  putBoolean("success", true)
599
599
  putBoolean("exists", file.exists())
600
600
  })
601
- } catch (e: Exception) {
601
+ } catch (e: Throwable) {
602
602
  promise.resolve(Arguments.createMap().apply {
603
603
  putBoolean("success", false)
604
604
  putString("error", e.message ?: "EXISTS_ERROR")
@@ -629,7 +629,7 @@ class FileToolkitModule(private val reactContext: ReactApplicationContext) :
629
629
  putDouble("modified", file.lastModified().toDouble())
630
630
  })
631
631
  })
632
- } catch (e: Exception) {
632
+ } catch (e: Throwable) {
633
633
  promise.resolve(Arguments.createMap().apply {
634
634
  putBoolean("success", false)
635
635
  putString("error", e.message ?: "STAT_ERROR")
@@ -650,6 +650,15 @@ class FileToolkitModule(private val reactContext: ReactApplicationContext) :
650
650
  return
651
651
  }
652
652
 
653
+ // Safety check: reject files > 50MB to prevent crashing the RN bridge
654
+ if (file.length() > 50L * 1024 * 1024) {
655
+ promise.resolve(Arguments.createMap().apply {
656
+ putBoolean("success", false)
657
+ putString("error", "File exceeds 50MB limit for readFile. Use streaming or base64 encoding for large files.")
658
+ })
659
+ return
660
+ }
661
+
653
662
  val data = if (encoding.equals("base64", ignoreCase = true)) {
654
663
  android.util.Base64.encodeToString(file.readBytes(), android.util.Base64.NO_WRAP)
655
664
  } else {
@@ -660,7 +669,7 @@ class FileToolkitModule(private val reactContext: ReactApplicationContext) :
660
669
  putBoolean("success", true)
661
670
  putString("data", data)
662
671
  })
663
- } catch (e: Exception) {
672
+ } catch (e: Throwable) {
664
673
  promise.resolve(Arguments.createMap().apply {
665
674
  putBoolean("success", false)
666
675
  putString("error", e.message ?: "READ_FILE_ERROR")
@@ -685,7 +694,7 @@ class FileToolkitModule(private val reactContext: ReactApplicationContext) :
685
694
  promise.resolve(Arguments.createMap().apply {
686
695
  putBoolean("success", true)
687
696
  })
688
- } catch (e: Exception) {
697
+ } catch (e: Throwable) {
689
698
  promise.resolve(Arguments.createMap().apply {
690
699
  putBoolean("success", false)
691
700
  putString("error", e.message ?: "WRITE_FILE_ERROR")
@@ -713,7 +722,7 @@ class FileToolkitModule(private val reactContext: ReactApplicationContext) :
713
722
  promise.resolve(Arguments.createMap().apply {
714
723
  putBoolean("success", true)
715
724
  })
716
- } catch (e: Exception) {
725
+ } catch (e: Throwable) {
717
726
  promise.resolve(Arguments.createMap().apply {
718
727
  putBoolean("success", false)
719
728
  putString("error", e.message ?: "COPY_FILE_ERROR")
@@ -753,7 +762,7 @@ class FileToolkitModule(private val reactContext: ReactApplicationContext) :
753
762
  promise.resolve(Arguments.createMap().apply {
754
763
  putBoolean("success", true)
755
764
  })
756
- } catch (e: Exception) {
765
+ } catch (e: Throwable) {
757
766
  promise.resolve(Arguments.createMap().apply {
758
767
  putBoolean("success", false)
759
768
  putString("error", e.message ?: "MOVE_FILE_ERROR")
@@ -771,7 +780,7 @@ class FileToolkitModule(private val reactContext: ReactApplicationContext) :
771
780
  putBoolean("success", ok)
772
781
  if (!ok) putString("error", "Could not create directory: $dirPath")
773
782
  })
774
- } catch (e: Exception) {
783
+ } catch (e: Throwable) {
775
784
  promise.resolve(Arguments.createMap().apply {
776
785
  putBoolean("success", false)
777
786
  putString("error", e.message ?: "MKDIR_ERROR")
@@ -799,7 +808,7 @@ class FileToolkitModule(private val reactContext: ReactApplicationContext) :
799
808
  putBoolean("success", true)
800
809
  putArray("entries", entries)
801
810
  })
802
- } catch (e: Exception) {
811
+ } catch (e: Throwable) {
803
812
  promise.resolve(Arguments.createMap().apply {
804
813
  putBoolean("success", false)
805
814
  putString("error", e.message ?: "LS_ERROR")
@@ -1018,7 +1027,7 @@ class FileToolkitModule(private val reactContext: ReactApplicationContext) :
1018
1027
  putString("filePath", destFile.absolutePath)
1019
1028
  })
1020
1029
 
1021
- } catch (e: Exception) {
1030
+ } catch (e: Throwable) {
1022
1031
  promise.resolve(Arguments.createMap().apply {
1023
1032
  putBoolean("success", false)
1024
1033
  putString("error", e.message ?: "BASE64_SAVE_ERROR")
@@ -1079,7 +1088,7 @@ class FileToolkitModule(private val reactContext: ReactApplicationContext) :
1079
1088
  putString("dataUri", "data:$mimeType;base64,$base64String")
1080
1089
  })
1081
1090
 
1082
- } catch (e: Exception) {
1091
+ } catch (e: Throwable) {
1083
1092
  promise.resolve(Arguments.createMap().apply {
1084
1093
  putBoolean("success", false)
1085
1094
  putString("error", e.message ?: "URL_TO_BASE64_ERROR")
@@ -376,6 +376,14 @@ RCT_EXPORT_MODULE()
376
376
  return;
377
377
  }
378
378
 
379
+ // Safety check: reject files > 50MB to prevent crashing the RN bridge
380
+ NSDictionary *attrs = [fm attributesOfItemAtPath:filePath error:nil];
381
+ unsigned long long fileSize = [attrs fileSize];
382
+ if (fileSize > 50 * 1024 * 1024) {
383
+ resolve(@{@"success": @NO, @"error": @"File exceeds 50MB limit for readFile. Use streaming or base64 encoding for large files."});
384
+ return;
385
+ }
386
+
379
387
  NSError *readError = nil;
380
388
  NSData *raw = [NSData dataWithContentsOfFile:filePath options:0 error:&readError];
381
389
  if (!raw || readError) {
@@ -1155,9 +1163,10 @@ RCT_EXPORT_METHOD(unzip:(NSString *)sourcePath
1155
1163
  // pure-Foundation approach using NSInputStream with a known zip local-file header parser.
1156
1164
 
1157
1165
  // ── Pure-Foundation zip reader (no third-party, no subprocess) ──────────
1158
- NSData *zipData = [NSData dataWithContentsOfFile:sourcePath];
1166
+ NSError *readError = nil;
1167
+ NSData *zipData = [NSData dataWithContentsOfFile:sourcePath options:NSDataReadingMappedIfSafe error:&readError];
1159
1168
  if (!zipData) {
1160
- resolve(@{@"success": @NO, @"error": @"Cannot read zip file"});
1169
+ resolve(@{@"success": @NO, @"error": readError.localizedDescription ?: @"Cannot read zip file"});
1161
1170
  return;
1162
1171
  }
1163
1172
 
@@ -1342,7 +1351,14 @@ RCT_EXPORT_METHOD(zip:(NSString *)sourcePath
1342
1351
  // Delete existing destination file
1343
1352
  [fm removeItemAtPath:destPath error:nil];
1344
1353
 
1345
- NSMutableData *zipData = [NSMutableData new];
1354
+ // Create the destination file and open a handle for writing
1355
+ [fm createFileAtPath:destPath contents:nil attributes:nil];
1356
+ NSFileHandle *fh = [NSFileHandle fileHandleForWritingAtPath:destPath];
1357
+ if (!fh) {
1358
+ resolve(@{@"success": @NO, @"error": @"Failed to create destination zip file"});
1359
+ return;
1360
+ }
1361
+
1346
1362
  NSMutableArray<NSDictionary *> *centralDirectory = [NSMutableArray new];
1347
1363
 
1348
1364
  NSArray<NSString *> *filesToZip;
@@ -1352,9 +1368,6 @@ RCT_EXPORT_METHOD(zip:(NSString *)sourcePath
1352
1368
  NSMutableArray *files = [NSMutableArray new];
1353
1369
  NSString *file;
1354
1370
  while ((file = [enumerator nextObject])) {
1355
- NSString *fullPath = [sourcePath stringByAppendingPathComponent:file];
1356
- BOOL entryIsDir = NO;
1357
- [fm fileExistsAtPath:fullPath isDirectory:&entryIsDir];
1358
1371
  [files addObject:file];
1359
1372
  }
1360
1373
  filesToZip = files;
@@ -1369,80 +1382,138 @@ RCT_EXPORT_METHOD(zip:(NSString *)sourcePath
1369
1382
  BOOL entryIsDir = NO;
1370
1383
  [fm fileExistsAtPath:fullPath isDirectory:&entryIsDir];
1371
1384
 
1372
- NSData *fileData = entryIsDir ? [NSData data] : [NSData dataWithContentsOfFile:fullPath];
1373
- if (!fileData) continue;
1374
-
1375
1385
  NSData *entryNameData = [relativePath dataUsingEncoding:NSUTF8StringEncoding];
1376
1386
  uint16_t nameLen = (uint16_t)entryNameData.length;
1377
1387
 
1378
- // Deflate compress (skip compression for directories)
1379
- NSData *compressedData;
1380
- uint16_t method;
1388
+ uint32_t localHeaderOffset = (uint32_t)[fh offsetInFile];
1389
+
1390
+ // Write local file header with placeholder CRC/sizes
1391
+ uint32_t sig = CFSwapInt32HostToLittle(0x04034b50);
1392
+ uint16_t version = CFSwapInt16HostToLittle(20);
1393
+ uint16_t flags = 0;
1394
+ uint16_t modTime = 0, modDate = 0;
1395
+ uint16_t extraLen = 0;
1396
+
1397
+ // Placeholders — will be patched after streaming
1398
+ uint16_t method = 0;
1399
+ uint32_t crcLE = 0;
1400
+ uint32_t compSz = 0;
1401
+ uint32_t uncompSz = 0;
1402
+
1403
+ [fh writeData:[NSData dataWithBytes:&sig length:4]];
1404
+ [fh writeData:[NSData dataWithBytes:&version length:2]];
1405
+ [fh writeData:[NSData dataWithBytes:&flags length:2]];
1406
+ [fh writeData:[NSData dataWithBytes:&method length:2]]; // offset +8
1407
+ [fh writeData:[NSData dataWithBytes:&modTime length:2]];
1408
+ [fh writeData:[NSData dataWithBytes:&modDate length:2]];
1409
+ [fh writeData:[NSData dataWithBytes:&crcLE length:4]]; // offset +14
1410
+ [fh writeData:[NSData dataWithBytes:&compSz length:4]]; // offset +18
1411
+ [fh writeData:[NSData dataWithBytes:&uncompSz length:4]]; // offset +22
1412
+ [fh writeData:[NSData dataWithBytes:&nameLen length:2]];
1413
+ [fh writeData:[NSData dataWithBytes:&extraLen length:2]];
1414
+ [fh writeData:entryNameData];
1415
+
1381
1416
  uint32_t crc = 0;
1417
+ uint32_t compressedSize = 0;
1418
+ uint32_t uncompressedSize = 0;
1419
+ uint16_t compressionMethod = 0;
1382
1420
 
1383
- if (entryIsDir || fileData.length == 0) {
1384
- compressedData = fileData;
1385
- method = 0;
1421
+ if (entryIsDir) {
1422
+ // Directory entry — no data to write
1386
1423
  } else {
1387
- // zlib deflate (raw, -15)
1388
- uLongf bound = compressBound((uLong)fileData.length);
1389
- NSMutableData *comp = [NSMutableData dataWithLength:bound];
1424
+ NSInputStream *inputStream = [NSInputStream inputStreamWithFileAtPath:fullPath];
1425
+ [inputStream open];
1426
+
1427
+ if (!inputStream || inputStream.streamStatus == NSStreamStatusError) {
1428
+ [inputStream close];
1429
+ continue;
1430
+ }
1431
+
1432
+ // Stream-compress with zlib deflate
1390
1433
  z_stream strm;
1391
1434
  memset(&strm, 0, sizeof(strm));
1392
- strm.next_in = (Bytef *)fileData.bytes;
1393
- strm.avail_in = (uInt)fileData.length;
1394
- strm.next_out = (Bytef *)comp.mutableBytes;
1395
- strm.avail_out = (uInt)bound;
1396
1435
  deflateInit2(&strm, Z_DEFAULT_COMPRESSION, Z_DEFLATED, -15, 8, Z_DEFAULT_STRATEGY);
1397
- deflate(&strm, Z_FINISH);
1436
+
1437
+ uint8_t inBuf[32768];
1438
+ uint8_t outBuf[32768];
1439
+ int flush = Z_NO_FLUSH;
1440
+
1441
+ while ([inputStream hasBytesAvailable]) {
1442
+ NSInteger bytesRead = [inputStream read:inBuf maxLength:sizeof(inBuf)];
1443
+ if (bytesRead <= 0) break;
1444
+
1445
+ crc = (uint32_t)crc32((uLong)crc, inBuf, (uInt)bytesRead);
1446
+ uncompressedSize += (uint32_t)bytesRead;
1447
+
1448
+ strm.next_in = inBuf;
1449
+ strm.avail_in = (uInt)bytesRead;
1450
+
1451
+ if (![inputStream hasBytesAvailable]) {
1452
+ flush = Z_FINISH;
1453
+ }
1454
+
1455
+ do {
1456
+ strm.next_out = outBuf;
1457
+ strm.avail_out = sizeof(outBuf);
1458
+ deflate(&strm, flush);
1459
+ NSUInteger produced = sizeof(outBuf) - strm.avail_out;
1460
+ if (produced > 0) {
1461
+ [fh writeData:[NSData dataWithBytes:outBuf length:produced]];
1462
+ compressedSize += (uint32_t)produced;
1463
+ }
1464
+ } while (strm.avail_out == 0);
1465
+ }
1466
+
1467
+ // Finalize deflate if we didn't hit Z_FINISH yet
1468
+ if (flush != Z_FINISH) {
1469
+ strm.next_in = NULL;
1470
+ strm.avail_in = 0;
1471
+ do {
1472
+ strm.next_out = outBuf;
1473
+ strm.avail_out = sizeof(outBuf);
1474
+ deflate(&strm, Z_FINISH);
1475
+ NSUInteger produced = sizeof(outBuf) - strm.avail_out;
1476
+ if (produced > 0) {
1477
+ [fh writeData:[NSData dataWithBytes:outBuf length:produced]];
1478
+ compressedSize += (uint32_t)produced;
1479
+ }
1480
+ } while (strm.avail_out == 0);
1481
+ }
1482
+
1398
1483
  deflateEnd(&strm);
1399
- [comp setLength:strm.total_out];
1400
- compressedData = comp;
1401
- method = 8;
1484
+ [inputStream close];
1485
+ compressionMethod = 8;
1402
1486
  }
1403
1487
 
1404
- // CRC32
1405
- crc = (uint32_t)crc32(0L, (const Bytef *)fileData.bytes, (uInt)fileData.length);
1406
-
1407
- uint32_t localHeaderOffset = (uint32_t)zipData.length;
1488
+ // Patch the local file header with actual CRC, sizes, method
1489
+ unsigned long long currentPos = [fh offsetInFile];
1490
+ uint16_t methLE = CFSwapInt16HostToLittle(compressionMethod);
1491
+ uint32_t crcPatch = CFSwapInt32HostToLittle(crc);
1492
+ uint32_t csPatch = CFSwapInt32HostToLittle(compressedSize);
1493
+ uint32_t usPatch = CFSwapInt32HostToLittle(uncompressedSize);
1408
1494
 
1409
- // Local file header
1410
- uint32_t sig = CFSwapInt32HostToLittle(0x04034b50);
1411
- uint16_t version = CFSwapInt16HostToLittle(20);
1412
- uint16_t flags = 0;
1413
- uint16_t meth = CFSwapInt16HostToLittle(method);
1414
- uint16_t modTime = 0, modDate = 0; // no time for simplicity
1415
- uint32_t crcLE = CFSwapInt32HostToLittle(crc);
1416
- uint32_t compSz = CFSwapInt32HostToLittle((uint32_t)compressedData.length);
1417
- uint32_t uncompSz = CFSwapInt32HostToLittle((uint32_t)fileData.length);
1418
- uint16_t extraLen = 0;
1495
+ [fh seekToFileOffset:localHeaderOffset + 8];
1496
+ [fh writeData:[NSData dataWithBytes:&methLE length:2]];
1497
+ [fh seekToFileOffset:localHeaderOffset + 10]; // skip modTime
1498
+ [fh seekToFileOffset:localHeaderOffset + 14];
1499
+ [fh writeData:[NSData dataWithBytes:&crcPatch length:4]];
1500
+ [fh writeData:[NSData dataWithBytes:&csPatch length:4]];
1501
+ [fh writeData:[NSData dataWithBytes:&usPatch length:4]];
1419
1502
 
1420
- [zipData appendBytes:&sig length:4];
1421
- [zipData appendBytes:&version length:2];
1422
- [zipData appendBytes:&flags length:2];
1423
- [zipData appendBytes:&meth length:2];
1424
- [zipData appendBytes:&modTime length:2];
1425
- [zipData appendBytes:&modDate length:2];
1426
- [zipData appendBytes:&crcLE length:4];
1427
- [zipData appendBytes:&compSz length:4];
1428
- [zipData appendBytes:&uncompSz length:4];
1429
- [zipData appendBytes:&nameLen length:2];
1430
- [zipData appendBytes:&extraLen length:2];
1431
- [zipData appendData:entryNameData];
1432
- [zipData appendData:compressedData];
1503
+ [fh seekToFileOffset:currentPos]; // restore position
1433
1504
 
1434
1505
  [centralDirectory addObject:@{
1435
1506
  @"name": entryNameData,
1436
- @"method": @(method),
1507
+ @"method": @(compressionMethod),
1437
1508
  @"crc": @(crc),
1438
- @"compSize": @(compressedData.length),
1439
- @"uncompSize": @(fileData.length),
1509
+ @"compSize": @(compressedSize),
1510
+ @"uncompSize": @(uncompressedSize),
1440
1511
  @"localOffset": @(localHeaderOffset),
1441
1512
  }];
1442
1513
  }
1443
1514
 
1444
1515
  // Central directory
1445
- uint32_t cdOffset = (uint32_t)zipData.length;
1516
+ uint32_t cdOffset = (uint32_t)[fh offsetInFile];
1446
1517
  for (NSDictionary *entry in centralDirectory) {
1447
1518
  NSData *nameData = entry[@"name"];
1448
1519
  uint16_t nameLen = (uint16_t)nameData.length;
@@ -1461,27 +1532,27 @@ RCT_EXPORT_METHOD(zip:(NSString *)sourcePath
1461
1532
  uint32_t extAttr = 0;
1462
1533
  uint32_t localOff = CFSwapInt32HostToLittle((uint32_t)[entry[@"localOffset"] unsignedIntValue]);
1463
1534
 
1464
- [zipData appendBytes:&cdSig length:4];
1465
- [zipData appendBytes:&verMade length:2];
1466
- [zipData appendBytes:&verNeeded length:2];
1467
- [zipData appendBytes:&flags length:2];
1468
- [zipData appendBytes:&meth length:2];
1469
- [zipData appendBytes:&modTime length:2];
1470
- [zipData appendBytes:&modDate length:2];
1471
- [zipData appendBytes:&crcLE length:4];
1472
- [zipData appendBytes:&compSz length:4];
1473
- [zipData appendBytes:&uncompSz length:4];
1474
- [zipData appendBytes:&nameLen length:2];
1475
- [zipData appendBytes:&extraLen length:2];
1476
- [zipData appendBytes:&commentLen length:2];
1477
- [zipData appendBytes:&disk length:2];
1478
- [zipData appendBytes:&intAttr length:2];
1479
- [zipData appendBytes:&extAttr length:4];
1480
- [zipData appendBytes:&localOff length:4];
1481
- [zipData appendData:nameData];
1535
+ [fh writeData:[NSData dataWithBytes:&cdSig length:4]];
1536
+ [fh writeData:[NSData dataWithBytes:&verMade length:2]];
1537
+ [fh writeData:[NSData dataWithBytes:&verNeeded length:2]];
1538
+ [fh writeData:[NSData dataWithBytes:&flags length:2]];
1539
+ [fh writeData:[NSData dataWithBytes:&meth length:2]];
1540
+ [fh writeData:[NSData dataWithBytes:&modTime length:2]];
1541
+ [fh writeData:[NSData dataWithBytes:&modDate length:2]];
1542
+ [fh writeData:[NSData dataWithBytes:&crcLE length:4]];
1543
+ [fh writeData:[NSData dataWithBytes:&compSz length:4]];
1544
+ [fh writeData:[NSData dataWithBytes:&uncompSz length:4]];
1545
+ [fh writeData:[NSData dataWithBytes:&nameLen length:2]];
1546
+ [fh writeData:[NSData dataWithBytes:&extraLen length:2]];
1547
+ [fh writeData:[NSData dataWithBytes:&commentLen length:2]];
1548
+ [fh writeData:[NSData dataWithBytes:&disk length:2]];
1549
+ [fh writeData:[NSData dataWithBytes:&intAttr length:2]];
1550
+ [fh writeData:[NSData dataWithBytes:&extAttr length:4]];
1551
+ [fh writeData:[NSData dataWithBytes:&localOff length:4]];
1552
+ [fh writeData:nameData];
1482
1553
  }
1483
1554
 
1484
- uint32_t cdSize = CFSwapInt32HostToLittle((uint32_t)(zipData.length - cdOffset));
1555
+ uint32_t cdSize = CFSwapInt32HostToLittle((uint32_t)([fh offsetInFile] - cdOffset));
1485
1556
  uint32_t cdOffLE = CFSwapInt32HostToLittle(cdOffset);
1486
1557
  uint16_t numEntries = CFSwapInt16HostToLittle((uint16_t)centralDirectory.count);
1487
1558
  uint16_t commentLen = 0;
@@ -1489,21 +1560,17 @@ RCT_EXPORT_METHOD(zip:(NSString *)sourcePath
1489
1560
  // End of central directory record
1490
1561
  uint32_t eocdSig = CFSwapInt32HostToLittle(0x06054b50);
1491
1562
  uint16_t disk = 0;
1492
- [zipData appendBytes:&eocdSig length:4];
1493
- [zipData appendBytes:&disk length:2];
1494
- [zipData appendBytes:&disk length:2];
1495
- [zipData appendBytes:&numEntries length:2];
1496
- [zipData appendBytes:&numEntries length:2];
1497
- [zipData appendBytes:&cdSize length:4];
1498
- [zipData appendBytes:&cdOffLE length:4];
1499
- [zipData appendBytes:&commentLen length:2];
1500
-
1501
- NSError *writeErr = nil;
1502
- if ([zipData writeToFile:destPath options:NSDataWritingAtomic error:&writeErr]) {
1503
- resolve(@{@"success": @YES, @"zipPath": destPath});
1504
- } else {
1505
- resolve(@{@"success": @NO, @"error": writeErr.localizedDescription ?: @"ZIP_WRITE_ERROR"});
1506
- }
1563
+ [fh writeData:[NSData dataWithBytes:&eocdSig length:4]];
1564
+ [fh writeData:[NSData dataWithBytes:&disk length:2]];
1565
+ [fh writeData:[NSData dataWithBytes:&disk length:2]];
1566
+ [fh writeData:[NSData dataWithBytes:&numEntries length:2]];
1567
+ [fh writeData:[NSData dataWithBytes:&numEntries length:2]];
1568
+ [fh writeData:[NSData dataWithBytes:&cdSize length:4]];
1569
+ [fh writeData:[NSData dataWithBytes:&cdOffLE length:4]];
1570
+ [fh writeData:[NSData dataWithBytes:&commentLen length:2]];
1571
+
1572
+ [fh closeFile];
1573
+ resolve(@{@"success": @YES, @"zipPath": destPath});
1507
1574
  });
1508
1575
  }
1509
1576
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "rn-file-toolkit",
3
- "version": "1.0.2",
4
- "description": "The simplest and most reliable way to download files in React Native with progress, permissions, and file handling built-in.",
3
+ "version": "1.0.4",
4
+ "description": "The ultimate native file management toolkit for React Native. Features background downloads, resumable uploads, filesystem operations, queues, zip/unzip, and media utilities with zero third-party dependencies.",
5
5
  "main": "./lib/commonjs/index.js",
6
6
  "module": "./lib/module/index.js",
7
7
  "types": "./lib/typescript/commonjs/src/index.d.ts",
@@ -105,7 +105,14 @@
105
105
  "mkdir",
106
106
  "ls",
107
107
  "blob-util-alternative",
108
- "react-native-fs-alternative"
108
+ "react-native-fs-alternative",
109
+ "fs",
110
+ "zip",
111
+ "unzip",
112
+ "archive",
113
+ "storage",
114
+ "react-native-file-toolkit",
115
+ "fetch"
109
116
  ],
110
117
  "repository": {
111
118
  "type": "git",