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 +173 -89
- package/android/src/main/java/com/filetoolkit/FileToolkitModule.kt +19 -10
- package/ios/FileToolkit.mm +158 -91
- package/package.json +10 -3
package/README.md
CHANGED
|
@@ -1,68 +1,119 @@
|
|
|
1
|
-
|
|
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
|
+
[](https://www.npmjs.com/package/rn-file-toolkit)
|
|
6
|
+
[](https://www.npmjs.com/package/rn-file-toolkit)
|
|
7
|
+
[](https://www.typescriptlang.org/)
|
|
8
|
+
[](https://github.com/chavan-labs/rn-file-toolkit)
|
|
9
|
+
[](https://github.com/chavan-labs/rn-file-toolkit/blob/main/LICENSE)
|
|
10
|
+
</div>
|
|
2
11
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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
|
-
|
|
35
|
+
## 🚀 Why rn-file-toolkit?
|
|
10
36
|
|
|
11
|
-
|
|
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
|
|
16
|
-
|
|
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
|
-
|
|
19
|
-
- **
|
|
20
|
-
- **
|
|
21
|
-
- **
|
|
22
|
-
- **
|
|
23
|
-
- **
|
|
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
|
-
```
|
|
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`
|
|
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
|
|
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({
|
|
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)}
|
|
57
|
-
<Text>
|
|
58
|
-
<
|
|
59
|
-
<
|
|
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
|
|
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
|
-
###
|
|
76
|
-
For programmatic, queue-aware background downloads.
|
|
126
|
+
### Background Downloads
|
|
127
|
+
For programmatic, queue-aware background downloads outside of React components.
|
|
77
128
|
|
|
78
|
-
```
|
|
129
|
+
```typescript
|
|
79
130
|
import { download, setQueueOptions } from 'rn-file-toolkit';
|
|
80
131
|
|
|
81
|
-
//
|
|
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,
|
|
88
|
-
priority: 'high',
|
|
138
|
+
queue: true, // Join the managed queue
|
|
139
|
+
priority: 'high', // 'high' | 'normal'
|
|
89
140
|
retry: { attempts: 3, delay: 1000 },
|
|
90
|
-
|
|
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
|
-
###
|
|
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
|
-
```
|
|
148
|
+
```typescript
|
|
99
149
|
import { upload } from 'rn-file-toolkit';
|
|
100
150
|
|
|
101
151
|
const result = await upload({
|
|
102
|
-
url: 'https://example.com/
|
|
103
|
-
filePath: '/path/to/
|
|
104
|
-
fieldName: '
|
|
105
|
-
parameters: { userId: '123' },
|
|
106
|
-
onProgress: (
|
|
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
|
-
###
|
|
111
|
-
Perform native filesystem operations
|
|
160
|
+
### File System (FS)
|
|
161
|
+
Perform native filesystem operations securely.
|
|
112
162
|
|
|
113
|
-
```
|
|
163
|
+
```typescript
|
|
114
164
|
import { fs } from 'rn-file-toolkit';
|
|
115
165
|
|
|
116
|
-
|
|
117
|
-
await fs.
|
|
118
|
-
await fs.
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
|
182
|
+
### Zip & Unzip Archives
|
|
183
|
+
Compress and extract archives directly on the device.
|
|
126
184
|
|
|
127
|
-
```
|
|
185
|
+
```typescript
|
|
128
186
|
import { unzip, zip } from 'rn-file-toolkit';
|
|
129
187
|
|
|
130
|
-
|
|
131
|
-
await
|
|
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
|
-
```
|
|
198
|
+
```typescript
|
|
137
199
|
import { saveBase64AsFile, urlToBase64, shareFile, openFile } from 'rn-file-toolkit';
|
|
138
200
|
|
|
139
|
-
//
|
|
140
|
-
await saveBase64AsFile({
|
|
201
|
+
// Base64 to File
|
|
202
|
+
await saveBase64AsFile({
|
|
203
|
+
base64Data: 'data:image/png;base64,...',
|
|
204
|
+
destination: 'documents',
|
|
205
|
+
fileName: 'image.png'
|
|
206
|
+
});
|
|
141
207
|
|
|
142
|
-
//
|
|
143
|
-
await urlToBase64({ url: 'https://example.com/
|
|
208
|
+
// URL to Base64 (Great for caching small images)
|
|
209
|
+
const b64 = await urlToBase64({ url: 'https://example.com/icon.png' });
|
|
144
210
|
|
|
145
|
-
// Share
|
|
146
|
-
await shareFile({ filePath: '/path/to/
|
|
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/
|
|
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
|
|
155
|
-
|
|
156
|
-
|
|
|
157
|
-
|
|
|
158
|
-
| `DownloadOptions`
|
|
159
|
-
| `
|
|
160
|
-
| `
|
|
161
|
-
| `
|
|
162
|
-
| `
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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")
|
package/ios/FileToolkit.mm
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
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
|
|
1384
|
-
|
|
1385
|
-
method = 0;
|
|
1421
|
+
if (entryIsDir) {
|
|
1422
|
+
// Directory entry — no data to write
|
|
1386
1423
|
} else {
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
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
|
-
|
|
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
|
-
[
|
|
1400
|
-
|
|
1401
|
-
method = 8;
|
|
1484
|
+
[inputStream close];
|
|
1485
|
+
compressionMethod = 8;
|
|
1402
1486
|
}
|
|
1403
1487
|
|
|
1404
|
-
//
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
uint32_t
|
|
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
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
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
|
-
[
|
|
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": @(
|
|
1507
|
+
@"method": @(compressionMethod),
|
|
1437
1508
|
@"crc": @(crc),
|
|
1438
|
-
@"compSize": @(
|
|
1439
|
-
@"uncompSize": @(
|
|
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)
|
|
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
|
-
[
|
|
1465
|
-
[
|
|
1466
|
-
[
|
|
1467
|
-
[
|
|
1468
|
-
[
|
|
1469
|
-
[
|
|
1470
|
-
[
|
|
1471
|
-
[
|
|
1472
|
-
[
|
|
1473
|
-
[
|
|
1474
|
-
[
|
|
1475
|
-
[
|
|
1476
|
-
[
|
|
1477
|
-
[
|
|
1478
|
-
[
|
|
1479
|
-
[
|
|
1480
|
-
[
|
|
1481
|
-
[
|
|
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)(
|
|
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
|
-
[
|
|
1493
|
-
[
|
|
1494
|
-
[
|
|
1495
|
-
[
|
|
1496
|
-
[
|
|
1497
|
-
[
|
|
1498
|
-
[
|
|
1499
|
-
[
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
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.
|
|
4
|
-
"description": "The
|
|
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",
|