Skip to content

Commit 2053273

Browse files
authored
Merge pull request joltup#128 from mmathewsTableau/master
Adding File Transforms
2 parents 3e7edf4 + 7fe83d7 commit 2053273

21 files changed

+356
-25
lines changed

README.md

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -303,6 +303,26 @@ ReactNativeBlobUtil
303303

304304
**These files won't be removed automatically, please refer to [Cache File Management](#user-content-cache-file-management)**
305305

306+
**Use File Transformer**
307+
308+
If you need to perform any processing on the bytes prior to it being written into storage (e.g. if you want it to be encrypted) then you can use `transform` option. NOTE: you will need to set a transformer on the libray (see [Setting a File Transformer](#Setting-A-File-Transformer))
309+
310+
```js
311+
ReactNativeBlobUtil
312+
.config({
313+
// response data will be saved to this path if it has access right.
314+
path: dirs.DocumentDir + '/path-to-file.anything',
315+
transform: true
316+
})
317+
.fetch('GET', 'http://www.example.com/file/example.zip', {
318+
//some headers ..
319+
})
320+
.then((res) => {
321+
// the path should be dirs.DocumentDir + 'path-to-file.anything'
322+
console.log('The file saved to ', res.path())
323+
})
324+
```
325+
306326
#### Upload example : Dropbox [files-upload](https://www.dropbox.com/developers/documentation/http/documentation#files-upload) API
307327

308328
`react-native-blob-util` will convert the base64 string in `body` to binary format using native API, this process is done in a separated thread so that it won't block your GUI.
@@ -676,6 +696,14 @@ await ReactNativeBlobUtil.MediaCollection.writeToMediafile('content://....', //
676696
);
677697
````
678698

699+
Copies and tranforms data from a file in the apps storage to an existing entry of the Media Store. NOTE: you must set a transformer on the file in order for the transformation to happen (see [Setting a File Transformer](#Setting-A-File-Transformer)).
700+
701+
````js
702+
await ReactNativeBlobUtil.MediaCollection.writeToMediafileWithTransform('content://....', // content uri of the entry in the media storage
703+
localpath // path to the file that should be copied
704+
);
705+
````
706+
679707
#### copyToInternal
680708
Copies an entry form the media storage to the apps internal storage.
681709
````js
@@ -697,8 +725,10 @@ File Access APIs
697725
- [dirs](https://github.com/RonRadtke/react-native-blob-util/wiki/File-System-Access-API#dirs)
698726
- [createFile](https://github.com/RonRadtke/react-native-blob-util/wiki/File-System-Access-API#createfilepath-data-encodingpromise)
699727
- [writeFile (0.6.0)](https://github.com/RonRadtke/react-native-blob-util/wiki/File-System-Access-API#writefilepathstring-contentstring--array-encodingstring-appendbooleanpromise)
728+
- writeFileWithTransform
700729
- [appendFile (0.6.0) ](https://github.com/RonRadtke/react-native-blob-util/wiki/File-System-Access-API#appendfilepathstring-contentstring--arraynumber-encodingstring-promisenumber)
701730
- [readFile (0.6.0)](https://github.com/RonRadtke/react-native-blob-util/wiki/File-System-Access-API#readfilepath-encodingpromise)
731+
- readFileWithTransform
702732
- [readStream](https://github.com/RonRadtke/react-native-blob-util/wiki/File-System-Access-API#readstreampath-encoding-buffersize-interval-promisernfbreadstream)
703733
- [hash (0.10.9)](https://github.com/RonRadtke/react-native-blob-util/wiki/File-System-Access-API#hashpath-algorithm-promise)
704734
- [writeStream](https://github.com/RonRadtke/react-native-blob-util/wiki/File-System-Access-API#writestreampathstring-encodingstringpromise)
@@ -903,13 +933,59 @@ ReactNativeBlobUtil.config({
903933
})
904934
```
905935

936+
### Transform Files
937+
938+
Sometimes you may need the files to be transformed after reading from storage or before writing into storage (eg encryption/decyrption). In order to perform the transformations, use `readFileWithTransform` and `writeFileWithTransform`. NOTE: you must set a transformer on the file in order for the transformation to happen (see [Setting a File Transformer](#Setting-A-File-Transformer)).
939+
906940
## Web API Polyfills
907941

908942
After `0.8.0` we've made some [Web API polyfills](https://github.com/RonRadtke/react-native-blob-util/wiki/Web-API-Polyfills-(experimental)) that makes some browser-based library available in RN.
909943

910944
- Blob
911945
- XMLHttpRequest (Use our implementation if you're going to use it with Blob)
912946

947+
948+
## Setting A File Transformer
949+
950+
Setting a file transformer will allow you to specify how data should be transformed whenever the library is writing into storage or reading from storage. A use case for this is if you want the files handled by this library to be encrypted.
951+
952+
If you want to use a file transformer, you must implement an interface defined in:
953+
954+
[ReactNativeBlobUtilFileTransformer.h (iOS)](/ios/ReactNativeBlobUtilFileTransformer.h)
955+
956+
[ReactNativeBlobUtilFileTransformer.java (Android)](/android/src/main/java/com/ReactNativeBlobUtil/ReactNativeBlobUtilFileTransformer.java)
957+
958+
Then you set the File Transformer during app startup
959+
960+
Android:
961+
```java
962+
public class MainApplication extends Application implements ReactApplication {
963+
...
964+
@Override
965+
public void onCreate() {
966+
...
967+
ReactNativeBlobUtilFileTransformer.sharedFileTransformer = new MyCustomEncryptor();
968+
...
969+
}
970+
```
971+
972+
iOS:
973+
```m
974+
@implementation AppDelegate
975+
...
976+
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
977+
{
978+
...
979+
[ReactNativeBlobUtilFileTransformer setFileTransformer: MyCustomEncryptor.new];
980+
...
981+
}
982+
```
983+
984+
Here are the places where the transformer would apply
985+
- Reading a file from the file system
986+
- Writing a file into the file system
987+
- Http response is downloaded to storage directly
988+
913989
## Performance Tips
914990

915991
**Read Stream and Progress Event Overhead**

android/src/main/java/com/ReactNativeBlobUtil/ReactNativeBlobUtil.java

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -233,11 +233,11 @@ public void removeSession(ReadableArray paths, Callback callback) {
233233
}
234234

235235
@ReactMethod
236-
public void readFile(final String path, final String encoding, final Promise promise) {
236+
public void readFile(final String path, final String encoding, final boolean transformFile, final Promise promise) {
237237
threadPool.execute(new Runnable() {
238238
@Override
239239
public void run() {
240-
ReactNativeBlobUtilFS.readFile(path, encoding, promise);
240+
ReactNativeBlobUtilFS.readFile(path, encoding, transformFile, promise);
241241
}
242242
});
243243
}
@@ -253,11 +253,11 @@ public void run() {
253253
}
254254

255255
@ReactMethod
256-
public void writeFile(final String path, final String encoding, final String data, final boolean append, final Promise promise) {
256+
public void writeFile(final String path, final String encoding, final String data, final boolean transformFile, final boolean append, final Promise promise) {
257257
threadPool.execute(new Runnable() {
258258
@Override
259259
public void run() {
260-
ReactNativeBlobUtilFS.writeFile(path, encoding, data, append, promise);
260+
ReactNativeBlobUtilFS.writeFile(path, encoding, data, transformFile, append, promise);
261261
}
262262
});
263263
}
@@ -438,8 +438,8 @@ public void createMediaFile(ReadableMap filedata, String mt, Promise promise) {
438438
}
439439

440440
@ReactMethod
441-
public void writeToMediaFile(String fileUri, String path, Promise promise) {
442-
boolean res = ReactNativeBlobUtilMediaCollection.writeToMediaFile(Uri.parse(fileUri), path, promise);
441+
public void writeToMediaFile(String fileUri, String path, boolean transformFile, Promise promise) {
442+
boolean res = ReactNativeBlobUtilMediaCollection.writeToMediaFile(Uri.parse(fileUri), path, transformFile, promise);
443443
if(res) promise.resolve("Success");
444444
}
445445

@@ -478,7 +478,7 @@ public void copyToMediaStore(ReadableMap filedata, String mt, String path, Promi
478478
return;
479479
}
480480

481-
boolean res = ReactNativeBlobUtilMediaCollection.writeToMediaFile(fileuri, path, promise);
481+
boolean res = ReactNativeBlobUtilMediaCollection.writeToMediaFile(fileuri, path, false, promise);
482482
if(res) promise.resolve(fileuri.toString());
483483
}
484484

android/src/main/java/com/ReactNativeBlobUtil/ReactNativeBlobUtilConfig.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
class ReactNativeBlobUtilConfig {
99

1010
public Boolean fileCache;
11+
public Boolean transformFile;
1112
public String path;
1213
public String appendExt;
1314
public ReadableMap addAndroidDownloads;
@@ -26,6 +27,7 @@ class ReactNativeBlobUtilConfig {
2627
if (options == null)
2728
return;
2829
this.fileCache = options.hasKey("fileCache") && options.getBoolean("fileCache");
30+
this.transformFile = options.hasKey("transformFile") ? options.getBoolean("transformFile") : false;
2931
this.path = options.hasKey("path") ? options.getString("path") : null;
3032
this.appendExt = options.hasKey("appendExt") ? options.getString("appendExt") : "";
3133
this.trusty = options.hasKey("trusty") && options.getBoolean("trusty");

android/src/main/java/com/ReactNativeBlobUtil/ReactNativeBlobUtilFS.java

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ static boolean writeFile(String path, String encoding, String data, final boolea
122122
* @param data Array passed from JS context.
123123
* @param promise RCT Promise
124124
*/
125-
static void writeFile(String path, String encoding, String data, final boolean append, final Promise promise) {
125+
static void writeFile(String path, String encoding, String data, final boolean transformFile, final boolean append, final Promise promise) {
126126
try {
127127
int written;
128128
File f = new File(path);
@@ -170,6 +170,12 @@ static void writeFile(String path, String encoding, String data, final boolean a
170170
}
171171
} else {
172172
byte[] bytes = ReactNativeBlobUtilUtils.stringToBytes(data, encoding);
173+
if (transformFile) {
174+
if (ReactNativeBlobUtilFileTransformer.sharedFileTransformer == null) {
175+
throw new IllegalStateException("Write file with transform was specified but the shared file transformer is not set");
176+
}
177+
bytes = ReactNativeBlobUtilFileTransformer.sharedFileTransformer.onWriteFile(bytes);
178+
}
173179
FileOutputStream fout = new FileOutputStream(f, append);
174180
try {
175181
fout.write(bytes);
@@ -238,7 +244,7 @@ static void writeFile(String path, ReadableArray data, final boolean append, fin
238244
* @param encoding Encoding of read stream.
239245
* @param promise JS promise
240246
*/
241-
static void readFile(String path, String encoding, final Promise promise) {
247+
static void readFile(String path, String encoding, final boolean transformFile, final Promise promise) {
242248
String resolved = ReactNativeBlobUtilUtils.normalizePath(path);
243249
if (resolved != null)
244250
path = resolved;
@@ -281,6 +287,13 @@ else if (resolved == null) {
281287
return;
282288
}
283289

290+
if (transformFile) {
291+
if (ReactNativeBlobUtilFileTransformer.sharedFileTransformer == null) {
292+
throw new IllegalStateException("Read file with transform was specified but the shared file transformer is not set");
293+
}
294+
bytes = ReactNativeBlobUtilFileTransformer.sharedFileTransformer.onReadFile(bytes);
295+
}
296+
284297
switch (encoding.toLowerCase(Locale.ROOT)) {
285298
case "base64":
286299
promise.resolve(Base64.encodeToString(bytes, Base64.NO_WRAP));
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package com.ReactNativeBlobUtil;
2+
3+
public class ReactNativeBlobUtilFileTransformer {
4+
public interface FileTransformer {
5+
public byte[] onWriteFile(byte[] data);
6+
public byte[] onReadFile(byte[] data);
7+
}
8+
9+
public static ReactNativeBlobUtilFileTransformer.FileTransformer sharedFileTransformer;
10+
}

android/src/main/java/com/ReactNativeBlobUtil/ReactNativeBlobUtilMediaCollection.java

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ public static Uri createNewMediaFile(FileDescription file, MediaType mt, ReactAp
124124
return null;
125125
}
126126

127-
public static boolean writeToMediaFile(Uri fileUri, String data, Promise promise) {
127+
public static boolean writeToMediaFile(Uri fileUri, String data, boolean transformFile, Promise promise) {
128128
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
129129
try {
130130
Context appCtx = ReactNativeBlobUtil.RCTContext.getApplicationContext();
@@ -151,16 +151,31 @@ public static boolean writeToMediaFile(Uri fileUri, String data, Promise promise
151151
promise.reject("ENOENT", "No such file ('" + normalizedData + "')");
152152
return false;
153153
}
154-
byte[] buf = new byte[10240];
155-
int read;
154+
156155

157156
FileInputStream fin = new FileInputStream(src);
158157
FileOutputStream out = new FileOutputStream(descr.getFileDescriptor());
159158

160-
while ((read = fin.read(buf)) > 0) {
161-
out.write(buf, 0, read);
159+
if (transformFile) {
160+
// in order to transform file, we must load the entire file onto memory
161+
int length = (int) src.length();
162+
byte[] bytes = new byte[length];
163+
fin.read(bytes);
164+
if (ReactNativeBlobUtilFileTransformer.sharedFileTransformer == null) {
165+
throw new IllegalStateException("Write to media file with transform was specified but the shared file transformer is not set");
166+
}
167+
byte[] transformedBytes = ReactNativeBlobUtilFileTransformer.sharedFileTransformer.onWriteFile(bytes);
168+
out.write(transformedBytes);
169+
} else {
170+
byte[] buf = new byte[10240];
171+
int read;
172+
173+
while ((read = fin.read(buf)) > 0) {
174+
out.write(buf, 0, read);
175+
}
162176
}
163177

178+
164179
fin.close();
165180
out.close();
166181
descr.close();

android/src/main/java/com/ReactNativeBlobUtil/ReactNativeBlobUtilReq.java

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,12 @@ enum ResponseFormat {
8787
BASE64
8888
}
8989

90+
private boolean shouldTransformFile() {
91+
return this.options.transformFile &&
92+
// Can only process if it's written to a file
93+
(this.options.fileCache || this.options.path != null);
94+
}
95+
9096
public static HashMap<String, Call> taskTable = new HashMap<>();
9197
public static HashMap<String, Long> androidDownloadManagerTaskTable = new HashMap<>();
9298
static HashMap<String, ReactNativeBlobUtilProgressConfig> progressReport = new HashMap<>();
@@ -124,7 +130,9 @@ public ReactNativeBlobUtilReq(ReadableMap options, String taskId, String method,
124130
this.rawRequestBodyArray = arrayBody;
125131
this.client = client;
126132

127-
if (this.options.fileCache || this.options.path != null)
133+
// If transformFile is specified, we first want to get the response back in memory so we can
134+
// encrypt it wholesale and at that point, write it into the file storage.
135+
if((this.options.fileCache || this.options.path != null) && !this.shouldTransformFile())
128136
responseType = ResponseType.FileStorage;
129137
else
130138
responseType = ResponseType.KeepInMemory;
@@ -557,6 +565,26 @@ private void done(Response resp) {
557565
// response data directly pass to JS context as string.
558566
else {
559567
byte[] b = resp.body().bytes();
568+
// If process file option is turned on, we first keep response in memory and then stream that content
569+
// after processing
570+
if (this.shouldTransformFile()) {
571+
if (ReactNativeBlobUtilFileTransformer.sharedFileTransformer == null) {
572+
throw new IllegalStateException("Write file with transform was specified but the shared file transformer is not set");
573+
}
574+
this.destPath = this.destPath.replace("?append=true", "");
575+
File file = new File(this.destPath);
576+
if (!file.exists()) {
577+
file.createNewFile();
578+
}
579+
try (FileOutputStream fos = new FileOutputStream(file)) {
580+
fos.write(ReactNativeBlobUtilFileTransformer.sharedFileTransformer.onWriteFile(b));
581+
} catch(Exception e) {
582+
callback.invoke("Error from file transformer:" + e.getLocalizedMessage(), null);
583+
return;
584+
}
585+
callback.invoke(null, ReactNativeBlobUtilConst.RNFB_RESPONSE_PATH, this.destPath);
586+
return;
587+
}
560588
if (responseFormat == ResponseFormat.BASE64) {
561589
callback.invoke(null, ReactNativeBlobUtilConst.RNFB_RESPONSE_BASE64, android.util.Base64.encodeToString(b, Base64.NO_WRAP));
562590
return;

fs.js

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,20 @@ function readFile(path: string, encoding: string = 'utf8'): Promise<any> {
160160
if (typeof path !== 'string') {
161161
return Promise.reject(addCode('EINVAL', new TypeError('Missing argument "path" ')));
162162
}
163-
return ReactNativeBlobUtil.readFile(path, encoding);
163+
return ReactNativeBlobUtil.readFile(path, encoding, false);
164+
}
165+
166+
/**
167+
* Reads the file, then transforms it before returning the content
168+
* @param {string} path Path of the file.
169+
* @param {'base64' | 'utf8' | 'ascii'} encoding Encoding of read stream.
170+
* @return {Promise<Array<number> | string>}
171+
*/
172+
function readFileWithTransform(path: string, encoding: string = 'utf8'): Promise<any> {
173+
if (typeof path !== 'string') {
174+
return Promise.reject(addCode('EINVAL', new TypeError('Missing argument "path" ')))
175+
}
176+
return ReactNativeBlobUtil.readFile(path, encoding, true);
164177
}
165178

166179
/**
@@ -186,7 +199,31 @@ function writeFile(path: string, data: string | Array<number>, encoding: ?string
186199
return Promise.reject(addCode('EINVAL', new TypeError(`"data" must be a String when encoding is "utf8" or "base64", but it is "${typeof data}"`)));
187200
}
188201
else
189-
return ReactNativeBlobUtil.writeFile(path, encoding, data, false);
202+
return ReactNativeBlobUtil.writeFile(path, encoding, data, false, false);
203+
}
204+
}
205+
206+
/**
207+
* Transforms the data and then writes to the file.
208+
* @param {string} path Path of the file.
209+
* @param {string | number[]} data Data to write to the file.
210+
* @param {string} encoding Encoding of data (Optional).
211+
* @return {Promise}
212+
*/
213+
function writeFileWithTransform(path: string, data: string | Array<number>, encoding: ?string = 'utf8'): Promise {
214+
if (typeof path !== 'string') {
215+
return Promise.reject(addCode('EINVAL', new TypeError('Missing argument "path" ')))
216+
}
217+
if (encoding.toLocaleLowerCase() === 'ascii') {
218+
return Promise.reject(addCode('EINVAL', new TypeError('ascii is not supported for converted files')))
219+
}
220+
else {
221+
if (typeof data !== 'string') {
222+
return Promise.reject(addCode('EINVAL', new TypeError(`"data" must be a String when encoding is "utf8" or "base64", but it is "${typeof data}"`)))
223+
}
224+
225+
else
226+
return ReactNativeBlobUtil.writeFile(path, encoding, data, true, false)
190227
}
191228
}
192229

@@ -415,6 +452,8 @@ export default {
415452
cp,
416453
writeStream,
417454
writeFile,
455+
writeFileWithTransform,
456+
readFileWithTransform,
418457
appendFile,
419458
pathForAppGroup,
420459
syncPathAppGroup,

0 commit comments

Comments
 (0)