Skip to content

Commit

Permalink
Merge pull request joltup#128 from mmathewsTableau/master
Browse files Browse the repository at this point in the history
Adding File Transforms
  • Loading branch information
RonRadtke authored Mar 20, 2022
2 parents 3e7edf4 + 7fe83d7 commit 2053273
Show file tree
Hide file tree
Showing 21 changed files with 356 additions and 25 deletions.
76 changes: 76 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,26 @@ ReactNativeBlobUtil

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

**Use File Transformer**

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))

```js
ReactNativeBlobUtil
.config({
// response data will be saved to this path if it has access right.
path: dirs.DocumentDir + '/path-to-file.anything',
transform: true
})
.fetch('GET', 'http://www.example.com/file/example.zip', {
//some headers ..
})
.then((res) => {
// the path should be dirs.DocumentDir + 'path-to-file.anything'
console.log('The file saved to ', res.path())
})
```

#### Upload example : Dropbox [files-upload](https://www.dropbox.com/developers/documentation/http/documentation#files-upload) API

`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.
Expand Down Expand Up @@ -676,6 +696,14 @@ await ReactNativeBlobUtil.MediaCollection.writeToMediafile('content://....', //
);
````

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)).

````js
await ReactNativeBlobUtil.MediaCollection.writeToMediafileWithTransform('content://....', // content uri of the entry in the media storage
localpath // path to the file that should be copied
);
````

#### copyToInternal
Copies an entry form the media storage to the apps internal storage.
````js
Expand All @@ -697,8 +725,10 @@ File Access APIs
- [dirs](https://github.com/RonRadtke/react-native-blob-util/wiki/File-System-Access-API#dirs)
- [createFile](https://github.com/RonRadtke/react-native-blob-util/wiki/File-System-Access-API#createfilepath-data-encodingpromise)
- [writeFile (0.6.0)](https://github.com/RonRadtke/react-native-blob-util/wiki/File-System-Access-API#writefilepathstring-contentstring--array-encodingstring-appendbooleanpromise)
- writeFileWithTransform
- [appendFile (0.6.0) ](https://github.com/RonRadtke/react-native-blob-util/wiki/File-System-Access-API#appendfilepathstring-contentstring--arraynumber-encodingstring-promisenumber)
- [readFile (0.6.0)](https://github.com/RonRadtke/react-native-blob-util/wiki/File-System-Access-API#readfilepath-encodingpromise)
- readFileWithTransform
- [readStream](https://github.com/RonRadtke/react-native-blob-util/wiki/File-System-Access-API#readstreampath-encoding-buffersize-interval-promisernfbreadstream)
- [hash (0.10.9)](https://github.com/RonRadtke/react-native-blob-util/wiki/File-System-Access-API#hashpath-algorithm-promise)
- [writeStream](https://github.com/RonRadtke/react-native-blob-util/wiki/File-System-Access-API#writestreampathstring-encodingstringpromise)
Expand Down Expand Up @@ -903,13 +933,59 @@ ReactNativeBlobUtil.config({
})
```

### Transform Files

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)).

## Web API Polyfills

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.

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


## Setting A File Transformer

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.

If you want to use a file transformer, you must implement an interface defined in:

[ReactNativeBlobUtilFileTransformer.h (iOS)](/ios/ReactNativeBlobUtilFileTransformer.h)

[ReactNativeBlobUtilFileTransformer.java (Android)](/android/src/main/java/com/ReactNativeBlobUtil/ReactNativeBlobUtilFileTransformer.java)

Then you set the File Transformer during app startup

Android:
```java
public class MainApplication extends Application implements ReactApplication {
...
@Override
public void onCreate() {
...
ReactNativeBlobUtilFileTransformer.sharedFileTransformer = new MyCustomEncryptor();
...
}
```

iOS:
```m
@implementation AppDelegate
...
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
...
[ReactNativeBlobUtilFileTransformer setFileTransformer: MyCustomEncryptor.new];
...
}
```

Here are the places where the transformer would apply
- Reading a file from the file system
- Writing a file into the file system
- Http response is downloaded to storage directly

## Performance Tips

**Read Stream and Progress Event Overhead**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -233,11 +233,11 @@ public void removeSession(ReadableArray paths, Callback callback) {
}

@ReactMethod
public void readFile(final String path, final String encoding, final Promise promise) {
public void readFile(final String path, final String encoding, final boolean transformFile, final Promise promise) {
threadPool.execute(new Runnable() {
@Override
public void run() {
ReactNativeBlobUtilFS.readFile(path, encoding, promise);
ReactNativeBlobUtilFS.readFile(path, encoding, transformFile, promise);
}
});
}
Expand All @@ -253,11 +253,11 @@ public void run() {
}

@ReactMethod
public void writeFile(final String path, final String encoding, final String data, final boolean append, final Promise promise) {
public void writeFile(final String path, final String encoding, final String data, final boolean transformFile, final boolean append, final Promise promise) {
threadPool.execute(new Runnable() {
@Override
public void run() {
ReactNativeBlobUtilFS.writeFile(path, encoding, data, append, promise);
ReactNativeBlobUtilFS.writeFile(path, encoding, data, transformFile, append, promise);
}
});
}
Expand Down Expand Up @@ -438,8 +438,8 @@ public void createMediaFile(ReadableMap filedata, String mt, Promise promise) {
}

@ReactMethod
public void writeToMediaFile(String fileUri, String path, Promise promise) {
boolean res = ReactNativeBlobUtilMediaCollection.writeToMediaFile(Uri.parse(fileUri), path, promise);
public void writeToMediaFile(String fileUri, String path, boolean transformFile, Promise promise) {
boolean res = ReactNativeBlobUtilMediaCollection.writeToMediaFile(Uri.parse(fileUri), path, transformFile, promise);
if(res) promise.resolve("Success");
}

Expand Down Expand Up @@ -478,7 +478,7 @@ public void copyToMediaStore(ReadableMap filedata, String mt, String path, Promi
return;
}

boolean res = ReactNativeBlobUtilMediaCollection.writeToMediaFile(fileuri, path, promise);
boolean res = ReactNativeBlobUtilMediaCollection.writeToMediaFile(fileuri, path, false, promise);
if(res) promise.resolve(fileuri.toString());
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
class ReactNativeBlobUtilConfig {

public Boolean fileCache;
public Boolean transformFile;
public String path;
public String appendExt;
public ReadableMap addAndroidDownloads;
Expand All @@ -26,6 +27,7 @@ class ReactNativeBlobUtilConfig {
if (options == null)
return;
this.fileCache = options.hasKey("fileCache") && options.getBoolean("fileCache");
this.transformFile = options.hasKey("transformFile") ? options.getBoolean("transformFile") : false;
this.path = options.hasKey("path") ? options.getString("path") : null;
this.appendExt = options.hasKey("appendExt") ? options.getString("appendExt") : "";
this.trusty = options.hasKey("trusty") && options.getBoolean("trusty");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ static boolean writeFile(String path, String encoding, String data, final boolea
* @param data Array passed from JS context.
* @param promise RCT Promise
*/
static void writeFile(String path, String encoding, String data, final boolean append, final Promise promise) {
static void writeFile(String path, String encoding, String data, final boolean transformFile, final boolean append, final Promise promise) {
try {
int written;
File f = new File(path);
Expand Down Expand Up @@ -170,6 +170,12 @@ static void writeFile(String path, String encoding, String data, final boolean a
}
} else {
byte[] bytes = ReactNativeBlobUtilUtils.stringToBytes(data, encoding);
if (transformFile) {
if (ReactNativeBlobUtilFileTransformer.sharedFileTransformer == null) {
throw new IllegalStateException("Write file with transform was specified but the shared file transformer is not set");
}
bytes = ReactNativeBlobUtilFileTransformer.sharedFileTransformer.onWriteFile(bytes);
}
FileOutputStream fout = new FileOutputStream(f, append);
try {
fout.write(bytes);
Expand Down Expand Up @@ -238,7 +244,7 @@ static void writeFile(String path, ReadableArray data, final boolean append, fin
* @param encoding Encoding of read stream.
* @param promise JS promise
*/
static void readFile(String path, String encoding, final Promise promise) {
static void readFile(String path, String encoding, final boolean transformFile, final Promise promise) {
String resolved = ReactNativeBlobUtilUtils.normalizePath(path);
if (resolved != null)
path = resolved;
Expand Down Expand Up @@ -281,6 +287,13 @@ else if (resolved == null) {
return;
}

if (transformFile) {
if (ReactNativeBlobUtilFileTransformer.sharedFileTransformer == null) {
throw new IllegalStateException("Read file with transform was specified but the shared file transformer is not set");
}
bytes = ReactNativeBlobUtilFileTransformer.sharedFileTransformer.onReadFile(bytes);
}

switch (encoding.toLowerCase(Locale.ROOT)) {
case "base64":
promise.resolve(Base64.encodeToString(bytes, Base64.NO_WRAP));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.ReactNativeBlobUtil;

public class ReactNativeBlobUtilFileTransformer {
public interface FileTransformer {
public byte[] onWriteFile(byte[] data);
public byte[] onReadFile(byte[] data);
}

public static ReactNativeBlobUtilFileTransformer.FileTransformer sharedFileTransformer;
}
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ public static Uri createNewMediaFile(FileDescription file, MediaType mt, ReactAp
return null;
}

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


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

while ((read = fin.read(buf)) > 0) {
out.write(buf, 0, read);
if (transformFile) {
// in order to transform file, we must load the entire file onto memory
int length = (int) src.length();
byte[] bytes = new byte[length];
fin.read(bytes);
if (ReactNativeBlobUtilFileTransformer.sharedFileTransformer == null) {
throw new IllegalStateException("Write to media file with transform was specified but the shared file transformer is not set");
}
byte[] transformedBytes = ReactNativeBlobUtilFileTransformer.sharedFileTransformer.onWriteFile(bytes);
out.write(transformedBytes);
} else {
byte[] buf = new byte[10240];
int read;

while ((read = fin.read(buf)) > 0) {
out.write(buf, 0, read);
}
}


fin.close();
out.close();
descr.close();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,12 @@ enum ResponseFormat {
BASE64
}

private boolean shouldTransformFile() {
return this.options.transformFile &&
// Can only process if it's written to a file
(this.options.fileCache || this.options.path != null);
}

public static HashMap<String, Call> taskTable = new HashMap<>();
public static HashMap<String, Long> androidDownloadManagerTaskTable = new HashMap<>();
static HashMap<String, ReactNativeBlobUtilProgressConfig> progressReport = new HashMap<>();
Expand Down Expand Up @@ -124,7 +130,9 @@ public ReactNativeBlobUtilReq(ReadableMap options, String taskId, String method,
this.rawRequestBodyArray = arrayBody;
this.client = client;

if (this.options.fileCache || this.options.path != null)
// If transformFile is specified, we first want to get the response back in memory so we can
// encrypt it wholesale and at that point, write it into the file storage.
if((this.options.fileCache || this.options.path != null) && !this.shouldTransformFile())
responseType = ResponseType.FileStorage;
else
responseType = ResponseType.KeepInMemory;
Expand Down Expand Up @@ -557,6 +565,26 @@ private void done(Response resp) {
// response data directly pass to JS context as string.
else {
byte[] b = resp.body().bytes();
// If process file option is turned on, we first keep response in memory and then stream that content
// after processing
if (this.shouldTransformFile()) {
if (ReactNativeBlobUtilFileTransformer.sharedFileTransformer == null) {
throw new IllegalStateException("Write file with transform was specified but the shared file transformer is not set");
}
this.destPath = this.destPath.replace("?append=true", "");
File file = new File(this.destPath);
if (!file.exists()) {
file.createNewFile();
}
try (FileOutputStream fos = new FileOutputStream(file)) {
fos.write(ReactNativeBlobUtilFileTransformer.sharedFileTransformer.onWriteFile(b));
} catch(Exception e) {
callback.invoke("Error from file transformer:" + e.getLocalizedMessage(), null);
return;
}
callback.invoke(null, ReactNativeBlobUtilConst.RNFB_RESPONSE_PATH, this.destPath);
return;
}
if (responseFormat == ResponseFormat.BASE64) {
callback.invoke(null, ReactNativeBlobUtilConst.RNFB_RESPONSE_BASE64, android.util.Base64.encodeToString(b, Base64.NO_WRAP));
return;
Expand Down
43 changes: 41 additions & 2 deletions fs.js
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,20 @@ function readFile(path: string, encoding: string = 'utf8'): Promise<any> {
if (typeof path !== 'string') {
return Promise.reject(addCode('EINVAL', new TypeError('Missing argument "path" ')));
}
return ReactNativeBlobUtil.readFile(path, encoding);
return ReactNativeBlobUtil.readFile(path, encoding, false);
}

/**
* Reads the file, then transforms it before returning the content
* @param {string} path Path of the file.
* @param {'base64' | 'utf8' | 'ascii'} encoding Encoding of read stream.
* @return {Promise<Array<number> | string>}
*/
function readFileWithTransform(path: string, encoding: string = 'utf8'): Promise<any> {
if (typeof path !== 'string') {
return Promise.reject(addCode('EINVAL', new TypeError('Missing argument "path" ')))
}
return ReactNativeBlobUtil.readFile(path, encoding, true);
}

/**
Expand All @@ -186,7 +199,31 @@ function writeFile(path: string, data: string | Array<number>, encoding: ?string
return Promise.reject(addCode('EINVAL', new TypeError(`"data" must be a String when encoding is "utf8" or "base64", but it is "${typeof data}"`)));
}
else
return ReactNativeBlobUtil.writeFile(path, encoding, data, false);
return ReactNativeBlobUtil.writeFile(path, encoding, data, false, false);
}
}

/**
* Transforms the data and then writes to the file.
* @param {string} path Path of the file.
* @param {string | number[]} data Data to write to the file.
* @param {string} encoding Encoding of data (Optional).
* @return {Promise}
*/
function writeFileWithTransform(path: string, data: string | Array<number>, encoding: ?string = 'utf8'): Promise {
if (typeof path !== 'string') {
return Promise.reject(addCode('EINVAL', new TypeError('Missing argument "path" ')))
}
if (encoding.toLocaleLowerCase() === 'ascii') {
return Promise.reject(addCode('EINVAL', new TypeError('ascii is not supported for converted files')))
}
else {
if (typeof data !== 'string') {
return Promise.reject(addCode('EINVAL', new TypeError(`"data" must be a String when encoding is "utf8" or "base64", but it is "${typeof data}"`)))
}

else
return ReactNativeBlobUtil.writeFile(path, encoding, data, true, false)
}
}

Expand Down Expand Up @@ -415,6 +452,8 @@ export default {
cp,
writeStream,
writeFile,
writeFileWithTransform,
readFileWithTransform,
appendFile,
pathForAppGroup,
syncPathAppGroup,
Expand Down
Loading

0 comments on commit 2053273

Please sign in to comment.