react-native-blob-util 0.14.1 → 0.15.0

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
@@ -303,6 +303,26 @@ ReactNativeBlobUtil
303
303
 
304
304
  **These files won't be removed automatically, please refer to [Cache File Management](#user-content-cache-file-management)**
305
305
 
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
+
306
326
  #### Upload example : Dropbox [files-upload](https://www.dropbox.com/developers/documentation/http/documentation#files-upload) API
307
327
 
308
328
  `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://....', //
676
696
  );
677
697
  ````
678
698
 
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
+
679
707
  #### copyToInternal
680
708
  Copies an entry form the media storage to the apps internal storage.
681
709
  ````js
@@ -697,8 +725,10 @@ File Access APIs
697
725
  - [dirs](https://github.com/RonRadtke/react-native-blob-util/wiki/File-System-Access-API#dirs)
698
726
  - [createFile](https://github.com/RonRadtke/react-native-blob-util/wiki/File-System-Access-API#createfilepath-data-encodingpromise)
699
727
  - [writeFile (0.6.0)](https://github.com/RonRadtke/react-native-blob-util/wiki/File-System-Access-API#writefilepathstring-contentstring--array-encodingstring-appendbooleanpromise)
728
+ - writeFileWithTransform
700
729
  - [appendFile (0.6.0) ](https://github.com/RonRadtke/react-native-blob-util/wiki/File-System-Access-API#appendfilepathstring-contentstring--arraynumber-encodingstring-promisenumber)
701
730
  - [readFile (0.6.0)](https://github.com/RonRadtke/react-native-blob-util/wiki/File-System-Access-API#readfilepath-encodingpromise)
731
+ - readFileWithTransform
702
732
  - [readStream](https://github.com/RonRadtke/react-native-blob-util/wiki/File-System-Access-API#readstreampath-encoding-buffersize-interval-promisernfbreadstream)
703
733
  - [hash (0.10.9)](https://github.com/RonRadtke/react-native-blob-util/wiki/File-System-Access-API#hashpath-algorithm-promise)
704
734
  - [writeStream](https://github.com/RonRadtke/react-native-blob-util/wiki/File-System-Access-API#writestreampathstring-encodingstringpromise)
@@ -903,6 +933,10 @@ ReactNativeBlobUtil.config({
903
933
  })
904
934
  ```
905
935
 
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
+
906
940
  ## Web API Polyfills
907
941
 
908
942
  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.
@@ -910,6 +944,48 @@ After `0.8.0` we've made some [Web API polyfills](https://github.com/RonRadtke/r
910
944
  - Blob
911
945
  - XMLHttpRequest (Use our implementation if you're going to use it with Blob)
912
946
 
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
+
913
989
  ## Performance Tips
914
990
 
915
991
  **Read Stream and Progress Event Overhead**
@@ -233,11 +233,11 @@ public class ReactNativeBlobUtil extends ReactContextBaseJavaModule {
233
233
  }
234
234
 
235
235
  @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) {
237
237
  threadPool.execute(new Runnable() {
238
238
  @Override
239
239
  public void run() {
240
- ReactNativeBlobUtilFS.readFile(path, encoding, promise);
240
+ ReactNativeBlobUtilFS.readFile(path, encoding, transformFile, promise);
241
241
  }
242
242
  });
243
243
  }
@@ -253,11 +253,11 @@ public class ReactNativeBlobUtil extends ReactContextBaseJavaModule {
253
253
  }
254
254
 
255
255
  @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) {
257
257
  threadPool.execute(new Runnable() {
258
258
  @Override
259
259
  public void run() {
260
- ReactNativeBlobUtilFS.writeFile(path, encoding, data, append, promise);
260
+ ReactNativeBlobUtilFS.writeFile(path, encoding, data, transformFile, append, promise);
261
261
  }
262
262
  });
263
263
  }
@@ -438,8 +438,8 @@ public class ReactNativeBlobUtil extends ReactContextBaseJavaModule {
438
438
  }
439
439
 
440
440
  @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);
443
443
  if(res) promise.resolve("Success");
444
444
  }
445
445
 
@@ -478,7 +478,7 @@ public class ReactNativeBlobUtil extends ReactContextBaseJavaModule {
478
478
  return;
479
479
  }
480
480
 
481
- boolean res = ReactNativeBlobUtilMediaCollection.writeToMediaFile(fileuri, path, promise);
481
+ boolean res = ReactNativeBlobUtilMediaCollection.writeToMediaFile(fileuri, path, false, promise);
482
482
  if(res) promise.resolve(fileuri.toString());
483
483
  }
484
484
 
@@ -8,6 +8,7 @@ import java.util.Locale;
8
8
  class ReactNativeBlobUtilConfig {
9
9
 
10
10
  public Boolean fileCache;
11
+ public Boolean transformFile;
11
12
  public String path;
12
13
  public String appendExt;
13
14
  public ReadableMap addAndroidDownloads;
@@ -26,6 +27,7 @@ class ReactNativeBlobUtilConfig {
26
27
  if (options == null)
27
28
  return;
28
29
  this.fileCache = options.hasKey("fileCache") && options.getBoolean("fileCache");
30
+ this.transformFile = options.hasKey("transformFile") ? options.getBoolean("transformFile") : false;
29
31
  this.path = options.hasKey("path") ? options.getString("path") : null;
30
32
  this.appendExt = options.hasKey("appendExt") ? options.getString("appendExt") : "";
31
33
  this.trusty = options.hasKey("trusty") && options.getBoolean("trusty");
@@ -54,6 +54,7 @@ class ReactNativeBlobUtilFS {
54
54
  static boolean writeFile(String path, String encoding, String data, final boolean append) {
55
55
  try {
56
56
  int written;
57
+ path = ReactNativeBlobUtilUtils.normalizePath(path);
57
58
  File f = new File(path);
58
59
  File dir = f.getParentFile();
59
60
  if (!f.exists()) {
@@ -121,7 +122,7 @@ class ReactNativeBlobUtilFS {
121
122
  * @param data Array passed from JS context.
122
123
  * @param promise RCT Promise
123
124
  */
124
- 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) {
125
126
  try {
126
127
  int written;
127
128
  File f = new File(path);
@@ -169,6 +170,12 @@ class ReactNativeBlobUtilFS {
169
170
  }
170
171
  } else {
171
172
  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
+ }
172
179
  FileOutputStream fout = new FileOutputStream(f, append);
173
180
  try {
174
181
  fout.write(bytes);
@@ -237,7 +244,7 @@ class ReactNativeBlobUtilFS {
237
244
  * @param encoding Encoding of read stream.
238
245
  * @param promise JS promise
239
246
  */
240
- static void readFile(String path, String encoding, final Promise promise) {
247
+ static void readFile(String path, String encoding, final boolean transformFile, final Promise promise) {
241
248
  String resolved = ReactNativeBlobUtilUtils.normalizePath(path);
242
249
  if (resolved != null)
243
250
  path = resolved;
@@ -280,6 +287,13 @@ class ReactNativeBlobUtilFS {
280
287
  return;
281
288
  }
282
289
 
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
+
283
297
  switch (encoding.toLowerCase(Locale.ROOT)) {
284
298
  case "base64":
285
299
  promise.resolve(Base64.encodeToString(bytes, Base64.NO_WRAP));
@@ -441,6 +455,7 @@ class ReactNativeBlobUtilFS {
441
455
  * @param promise JS promise
442
456
  */
443
457
  static void mkdir(String path, Promise promise) {
458
+ path = ReactNativeBlobUtilUtils.normalizePath(path);
444
459
  File dest = new File(path);
445
460
  if (dest.exists()) {
446
461
  promise.reject("EEXIST", (dest.isDirectory() ? "Folder" : "File") + " '" + path + "' already exists");
@@ -468,6 +483,7 @@ class ReactNativeBlobUtilFS {
468
483
  */
469
484
  static void cp(String path, String dest, Callback callback) {
470
485
  path = ReactNativeBlobUtilUtils.normalizePath(path);
486
+ dest = ReactNativeBlobUtilUtils.normalizePath(dest);
471
487
  InputStream in = null;
472
488
  OutputStream out = null;
473
489
  String message = "";
@@ -524,6 +540,8 @@ class ReactNativeBlobUtilFS {
524
540
  * @param callback JS context callback
525
541
  */
526
542
  static void mv(String path, String dest, Callback callback) {
543
+ path = ReactNativeBlobUtilUtils.normalizePath(path);
544
+ dest = ReactNativeBlobUtilUtils.normalizePath(dest);
527
545
  File src = new File(path);
528
546
  if (!src.exists()) {
529
547
  callback.invoke("Source file at path `" + path + "` does not exist");
@@ -627,6 +645,7 @@ class ReactNativeBlobUtilFS {
627
645
  static void slice(String path, String dest, int start, int end, String encode, Promise promise) {
628
646
  try {
629
647
  path = ReactNativeBlobUtilUtils.normalizePath(path);
648
+ dest = ReactNativeBlobUtilUtils.normalizePath(dest);
630
649
  File source = new File(path);
631
650
  if (source.isDirectory()) {
632
651
  promise.reject("EISDIR", "Expecting a file but '" + path + "' is a directory");
@@ -790,6 +809,8 @@ class ReactNativeBlobUtilFS {
790
809
  promise.reject("EINVAL", "Invalid algorithm '" + algorithm + "', must be one of md5, sha1, sha224, sha256, sha384, sha512");
791
810
  return;
792
811
  }
812
+
813
+ path = ReactNativeBlobUtilUtils.normalizePath(path);
793
814
 
794
815
  File file = new File(path);
795
816
 
@@ -837,6 +858,7 @@ class ReactNativeBlobUtilFS {
837
858
  */
838
859
  static void createFile(String path, String data, String encoding, Promise promise) {
839
860
  try {
861
+ path = ReactNativeBlobUtilUtils.normalizePath(path);
840
862
  File dest = new File(path);
841
863
  boolean created = dest.createNewFile();
842
864
  if (encoding.equals(ReactNativeBlobUtilConst.DATA_ENCODE_URI)) {
@@ -879,6 +901,7 @@ class ReactNativeBlobUtilFS {
879
901
  */
880
902
  static void createFileASCII(String path, ReadableArray data, Promise promise) {
881
903
  try {
904
+ path = ReactNativeBlobUtilUtils.normalizePath(path);
882
905
  File dest = new File(path);
883
906
  boolean created = dest.createNewFile();
884
907
  if (!created) {
@@ -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
+ }
@@ -124,7 +124,7 @@ public class ReactNativeBlobUtilMediaCollection {
124
124
  return null;
125
125
  }
126
126
 
127
- public static boolean writeToMediaFile(Uri fileUri, String data, Promise promise) {
127
+ public static boolean writeToMediaFile(Uri fileUri, String data, boolean transformFile, Promise promise) {
128
128
  if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
129
129
  try {
130
130
  Context appCtx = ReactNativeBlobUtil.RCTContext.getApplicationContext();
@@ -151,16 +151,31 @@ public class ReactNativeBlobUtilMediaCollection {
151
151
  promise.reject("ENOENT", "No such file ('" + normalizedData + "')");
152
152
  return false;
153
153
  }
154
- byte[] buf = new byte[10240];
155
- int read;
154
+
156
155
 
157
156
  FileInputStream fin = new FileInputStream(src);
158
157
  FileOutputStream out = new FileOutputStream(descr.getFileDescriptor());
159
158
 
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
+ }
162
176
  }
163
177
 
178
+
164
179
  fin.close();
165
180
  out.close();
166
181
  descr.close();
@@ -87,6 +87,12 @@ public class ReactNativeBlobUtilReq extends BroadcastReceiver implements Runnabl
87
87
  BASE64
88
88
  }
89
89
 
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
+
90
96
  public static HashMap<String, Call> taskTable = new HashMap<>();
91
97
  public static HashMap<String, Long> androidDownloadManagerTaskTable = new HashMap<>();
92
98
  static HashMap<String, ReactNativeBlobUtilProgressConfig> progressReport = new HashMap<>();
@@ -124,7 +130,9 @@ public class ReactNativeBlobUtilReq extends BroadcastReceiver implements Runnabl
124
130
  this.rawRequestBodyArray = arrayBody;
125
131
  this.client = client;
126
132
 
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())
128
136
  responseType = ResponseType.FileStorage;
129
137
  else
130
138
  responseType = ResponseType.KeepInMemory;
@@ -557,6 +565,26 @@ public class ReactNativeBlobUtilReq extends BroadcastReceiver implements Runnabl
557
565
  // response data directly pass to JS context as string.
558
566
  else {
559
567
  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
+ }
560
588
  if (responseFormat == ResponseFormat.BASE64) {
561
589
  callback.invoke(null, ReactNativeBlobUtilConst.RNFB_RESPONSE_BASE64, android.util.Base64.encodeToString(b, Base64.NO_WRAP));
562
590
  return;
@@ -72,6 +72,10 @@ public class ReactNativeBlobUtilFileResp extends ResponseBody {
72
72
 
73
73
  @Override
74
74
  public long contentLength() {
75
+ if (originalBody.contentLength() > Integer.MAX_VALUE) {
76
+ // This is a workaround for a bug Okio buffer where it can't handle larger than int.
77
+ return Integer.MAX_VALUE;
78
+ }
75
79
  return originalBody.contentLength();
76
80
  }
77
81
 
@@ -3,7 +3,7 @@ import fs from "../fs";
3
3
  import Blob from "../polyfill/Blob";
4
4
  import ReactNativeBlobUtilSession from "./ReactNativeBlobUtilSession";
5
5
  import URIUtil from "../utils/uri";
6
-
6
+ import base64 from "base-64";
7
7
  /**
8
8
  * ReactNativeBlobUtil response object class.
9
9
  */
package/fs.js CHANGED
@@ -160,7 +160,20 @@ function readFile(path: string, encoding: string = 'utf8'): Promise<any> {
160
160
  if (typeof path !== 'string') {
161
161
  return Promise.reject(addCode('EINVAL', new TypeError('Missing argument "path" ')));
162
162
  }
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);
164
177
  }
165
178
 
166
179
  /**
@@ -186,7 +199,31 @@ function writeFile(path: string, data: string | Array<number>, encoding: ?string
186
199
  return Promise.reject(addCode('EINVAL', new TypeError(`"data" must be a String when encoding is "utf8" or "base64", but it is "${typeof data}"`)));
187
200
  }
188
201
  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)
190
227
  }
191
228
  }
192
229
 
@@ -206,7 +243,7 @@ function appendFile(path: string, data: string | Array<number>, encoding?: strin
206
243
  return Promise.reject(addCode('EINVAL'), new TypeError(`"data" must be a String when encoding is "utf8" or "base64", but it is "${typeof data}"`));
207
244
  }
208
245
  else
209
- return ReactNativeBlobUtil.writeFile(path, encoding, data, true);
246
+ return ReactNativeBlobUtil.writeFile(path, encoding, data, false, true);
210
247
  }
211
248
  }
212
249
 
@@ -415,6 +452,8 @@ export default {
415
452
  cp,
416
453
  writeStream,
417
454
  writeFile,
455
+ writeFileWithTransform,
456
+ readFileWithTransform,
418
457
  appendFile,
419
458
  pathForAppGroup,
420
459
  syncPathAppGroup,
package/index.d.ts CHANGED
@@ -399,6 +399,14 @@ export interface FS {
399
399
  */
400
400
  writeFile(path: string, data: string | number[], encoding?: Encoding): Promise<void>;
401
401
 
402
+ /**
403
+ * Processes the data and then writes to the file.
404
+ * @param path Path of the file.
405
+ * @param data Data to write to the file.
406
+ * @param encoding Encoding of data (Optional).
407
+ */
408
+ writeFileWithTransform(path: string, data: string | number[], encoding?: Encoding): Promise<void>;
409
+
402
410
  appendFile(path: string, data: string | number[], encoding?: Encoding | "uri"): Promise<number>;
403
411
 
404
412
  /**
@@ -408,6 +416,13 @@ export interface FS {
408
416
  */
409
417
  readFile(path: string, encoding: Encoding, bufferSize?: number): Promise<any>;
410
418
 
419
+ /**
420
+ * Reads from a file and then processes the data before returning
421
+ * @param path Path of the file.
422
+ * @param encoding Encoding of read stream.
423
+ */
424
+ readFileWithTransform(path: string, encoding: Encoding, bufferSize?: number): Promise<any>;
425
+
411
426
  /**
412
427
  * Check if file exists and if it is a folder.
413
428
  * @param path Path to check
@@ -698,6 +713,13 @@ export interface ReactNativeBlobUtilConfig {
698
713
  */
699
714
  fileCache?: boolean;
700
715
 
716
+ /**
717
+ * Set this property to true if you want the data to be processed before it gets written onto disk.
718
+ * This only has effect if the FileTransformer has been registered and the library is configured to write
719
+ * response onto disk.
720
+ */
721
+ transformFile?: boolean;
722
+
701
723
  /**
702
724
  * Set this property to change temp file extension that created by fetch response data.
703
725
  */
@@ -808,6 +830,13 @@ export interface MediaCollection {
808
830
  */
809
831
  writeToMediafile(uri: string, path: string): Promise<string>
810
832
 
833
+ /**
834
+ * Copies and transforms an existing file to a mediastore file. Make sure FileTransformer is set
835
+ * @param uri URI of the destination mediastore file
836
+ * @param path Path to the existing file which should be copied
837
+ */
838
+ writeToMediafileWithTransform(uri: string, path: string): Promise<string>
839
+
811
840
  /**
812
841
  * Copies a file from the mediastore to the apps internal storage
813
842
  * @param contenturi URI of the mediastore file
@@ -252,9 +252,9 @@ RCT_EXPORT_METHOD(exists:(NSString *)path callback:(RCTResponseSenderBlock)callb
252
252
  }
253
253
 
254
254
  #pragma mark - fs.writeFile
255
- RCT_EXPORT_METHOD(writeFile:(NSString *)path encoding:(NSString *)encoding data:(NSString *)data append:(BOOL)append resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject)
255
+ RCT_EXPORT_METHOD(writeFile:(NSString *)path encoding:(NSString *)encoding data:(NSString *)data transformFile:(BOOL)transformFile append:(BOOL)append resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject)
256
256
  {
257
- [ReactNativeBlobUtilFS writeFile:path encoding:[NSString stringWithString:encoding] data:data append:append resolver:resolve rejecter:reject];
257
+ [ReactNativeBlobUtilFS writeFile:path encoding:[NSString stringWithString:encoding] data:data transformFile:transformFile append:append resolver:resolve rejecter:reject];
258
258
  }
259
259
 
260
260
  #pragma mark - fs.writeArray
@@ -494,11 +494,12 @@ RCT_EXPORT_METHOD(mkdir:(NSString *)path resolver:(RCTPromiseResolveBlock)resolv
494
494
  #pragma mark - fs.readFile
495
495
  RCT_EXPORT_METHOD(readFile:(NSString *)path
496
496
  encoding:(NSString *)encoding
497
+ transformFile:(BOOL) transformFile
497
498
  resolver:(RCTPromiseResolveBlock)resolve
498
499
  rejecter:(RCTPromiseRejectBlock)reject)
499
500
  {
500
501
 
501
- [ReactNativeBlobUtilFS readFile:path encoding:encoding onComplete:^(NSData * content, NSString * code, NSString * err) {
502
+ [ReactNativeBlobUtilFS readFile:path encoding:encoding transformFile:transformFile onComplete:^(NSData * content, NSString * code, NSString * err) {
502
503
  if(err != nil) {
503
504
  reject(code, err, nil);
504
505
  return;
@@ -8,6 +8,7 @@
8
8
 
9
9
  /* Begin PBXBuildFile section */
10
10
  8C4801A6200CF71700FED7ED /* ReactNativeBlobUtilRequest.m in Sources */ = {isa = PBXBuildFile; fileRef = 8C4801A5200CF71700FED7ED /* ReactNativeBlobUtilRequest.m */; };
11
+ 9FD8D3C326F1736000009F35 /* ReactNativeBlobUtilFileTransformer.m in Sources */ = {isa = PBXBuildFile; fileRef = 9FD8D3C226F1736000009F35 /* ReactNativeBlobUtilFileTransformer.m */; };
11
12
  A158F4271D052E49006FFD38 /* ReactNativeBlobUtilFS.m in Sources */ = {isa = PBXBuildFile; fileRef = A158F4261D052E49006FFD38 /* ReactNativeBlobUtilFS.m */; };
12
13
  A158F42D1D0535BB006FFD38 /* ReactNativeBlobUtilConst.m in Sources */ = {isa = PBXBuildFile; fileRef = A158F42C1D0535BB006FFD38 /* ReactNativeBlobUtilConst.m */; };
13
14
  A158F4301D0539DB006FFD38 /* ReactNativeBlobUtilNetwork.m in Sources */ = {isa = PBXBuildFile; fileRef = A158F42F1D0539DB006FFD38 /* ReactNativeBlobUtilNetwork.m */; };
@@ -32,6 +33,8 @@
32
33
  /* Begin PBXFileReference section */
33
34
  8C4801A4200CF71700FED7ED /* ReactNativeBlobUtilRequest.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ReactNativeBlobUtilRequest.h; sourceTree = "<group>"; };
34
35
  8C4801A5200CF71700FED7ED /* ReactNativeBlobUtilRequest.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ReactNativeBlobUtilRequest.m; sourceTree = "<group>"; };
36
+ 9FD8D3C126F1709A00009F35 /* ReactNativeBlobUtilFileTransformer.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ReactNativeBlobUtilFileTransformer.h; sourceTree = "<group>"; };
37
+ 9FD8D3C226F1736000009F35 /* ReactNativeBlobUtilFileTransformer.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ReactNativeBlobUtilFileTransformer.m; sourceTree = "<group>"; };
35
38
  A158F4261D052E49006FFD38 /* ReactNativeBlobUtilFS.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ReactNativeBlobUtilFS.m; sourceTree = "<group>"; };
36
39
  A158F4281D052E57006FFD38 /* ReactNativeBlobUtilFS.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ReactNativeBlobUtilFS.h; sourceTree = "<group>"; };
37
40
  A158F4291D0534A9006FFD38 /* ReactNativeBlobUtilConst.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ReactNativeBlobUtilConst.h; sourceTree = "<group>"; };
@@ -69,6 +72,8 @@
69
72
  A15C30051CD25C330074CB35 = {
70
73
  isa = PBXGroup;
71
74
  children = (
75
+ 9FD8D3C226F1736000009F35 /* ReactNativeBlobUtilFileTransformer.m */,
76
+ 9FD8D3C126F1709A00009F35 /* ReactNativeBlobUtilFileTransformer.h */,
72
77
  A19B48241D98102400E6868A /* ReactNativeBlobUtilProgress.m */,
73
78
  A19B48231D98100800E6868A /* ReactNativeBlobUtilProgress.h */,
74
79
  A1F950181D7E9134002A95A6 /* IOS7Polyfill.h */,
@@ -156,6 +161,7 @@
156
161
  A166D1AA1CE0647A00273590 /* ReactNativeBlobUtil.h in Sources */,
157
162
  8C4801A6200CF71700FED7ED /* ReactNativeBlobUtilRequest.m in Sources */,
158
163
  A158F42D1D0535BB006FFD38 /* ReactNativeBlobUtilConst.m in Sources */,
164
+ 9FD8D3C326F1736000009F35 /* ReactNativeBlobUtilFileTransformer.m in Sources */,
159
165
  A158F4271D052E49006FFD38 /* ReactNativeBlobUtilFS.m in Sources */,
160
166
  A158F4301D0539DB006FFD38 /* ReactNativeBlobUtilNetwork.m in Sources */,
161
167
  A19B48251D98102400E6868A /* ReactNativeBlobUtilProgress.m in Sources */,
@@ -29,6 +29,7 @@ extern NSString *const AL_PREFIX;
29
29
 
30
30
  // config
31
31
  extern NSString *const CONFIG_USE_TEMP;
32
+ extern NSString *const CONFIG_TRANSFORM_FILE;
32
33
  extern NSString *const CONFIG_FILE_PATH;
33
34
  extern NSString *const CONFIG_FILE_EXT;
34
35
  extern NSString *const CONFIG_TRUSTY;
@@ -13,6 +13,7 @@ NSString *const AL_PREFIX = @"assets-library://";
13
13
 
14
14
  // fetch configs
15
15
  NSString *const CONFIG_USE_TEMP = @"fileCache";
16
+ NSString *const CONFIG_TRANSFORM_FILE = @"transformFile";
16
17
  NSString *const CONFIG_FILE_PATH = @"path";
17
18
  NSString *const CONFIG_FILE_EXT = @"appendExt";
18
19
  NSString *const CONFIG_TRUSTY = @"trusty";
@@ -72,7 +72,8 @@
72
72
  + (NSDictionary *) stat:(NSString *) path error:(NSError **) error;
73
73
  + (void) exists:(NSString *) path callback:(RCTResponseSenderBlock)callback;
74
74
  + (void) writeFileArray:(NSString *)path data:(NSArray *)data append:(BOOL)append resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject;
75
- + (void) writeFile:(NSString *)path encoding:(NSString *)encoding data:(NSString *)data append:(BOOL)append resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject;
75
+ + (void) writeFile:(NSString *)path encoding:(NSString *)encoding data:(NSString *)data transformFile:(BOOL)transformFile append:(BOOL)append resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject;
76
+ + (void) readFile:(NSString *)path encoding:(NSString *)encoding transformFile:(BOOL)transformFile onComplete:(void (^)(NSData * content, NSString* code, NSString * errMsg))onComplete;
76
77
  + (void) readFile:(NSString *)path encoding:(NSString *)encoding onComplete:(void (^)(NSData * content, NSString* code, NSString * errMsg))onComplete;
77
78
  + (void) readAssetFile:(NSData *)assetUrl completionBlock:(void(^)(NSData * content))completionBlock failBlock:(void(^)(NSError * err))failBlock;
78
79
  + (void) slice:(NSString *)path
@@ -10,6 +10,7 @@
10
10
  #import "ReactNativeBlobUtil.h"
11
11
  #import "ReactNativeBlobUtilFS.h"
12
12
  #import "ReactNativeBlobUtilConst.h"
13
+ #import "ReactNativeBlobUtilFileTransformer.h"
13
14
  #import "IOS7Polyfill.h"
14
15
  @import AssetsLibrary;
15
16
 
@@ -347,6 +348,7 @@ NSMutableDictionary *fileStreams = nil;
347
348
  + (void) writeFile:(NSString *)path
348
349
  encoding:(NSString *)encoding
349
350
  data:(NSString *)data
351
+ transformFile:(BOOL)transformFile
350
352
  append:(BOOL)append
351
353
  resolver:(RCTPromiseResolveBlock)resolve
352
354
  rejecter:(RCTPromiseRejectBlock)reject
@@ -394,6 +396,16 @@ NSMutableDictionary *fileStreams = nil;
394
396
  else {
395
397
  content = [data dataUsingEncoding:NSUTF8StringEncoding];
396
398
  }
399
+
400
+ if (transformFile) {
401
+ NSObject<FileTransformer>* fileTransformer = [ReactNativeBlobUtilFileTransformer getFileTransformer];
402
+ if (fileTransformer) {
403
+ content = [fileTransformer onWriteFile:content];
404
+ } else {
405
+ return reject(@"EUNSPECIFIED",@"Transform specified but transformer not set", nil);
406
+ }
407
+ }
408
+
397
409
  if(append == YES) {
398
410
  [fileHandle seekToEndOfFile];
399
411
  [fileHandle writeData:content];
@@ -488,6 +500,7 @@ NSMutableDictionary *fileStreams = nil;
488
500
 
489
501
  + (void) readFile:(NSString *)path
490
502
  encoding:(NSString *)encoding
503
+ transformFile:(BOOL) transformFile
491
504
  onComplete:(void (^)(NSData * content, NSString * codeStr, NSString * errMsg))onComplete
492
505
  {
493
506
  [[self class] getPathFromUri:path completionHandler:^(NSString *path, ALAssetRepresentation *asset) {
@@ -522,6 +535,22 @@ NSMutableDictionary *fileStreams = nil;
522
535
 
523
536
  }
524
537
 
538
+ if (transformFile) {
539
+ NSObject<FileTransformer>* fileTransformer = [ReactNativeBlobUtilFileTransformer getFileTransformer];
540
+ if (fileTransformer) {
541
+ @try{
542
+ fileContent = [fileTransformer onReadFile:fileContent];
543
+ } @catch (NSException * ex)
544
+ {
545
+ onComplete(nil, @"EUNSPECIFIED", [NSString stringWithFormat:@"Exception on File Transformer: '%@' ", [ex description]]);
546
+ return;
547
+ }
548
+ } else {
549
+ onComplete(nil, @"EUNSPECIFIED", @"Transform specified but transformer not set");
550
+ return;
551
+ }
552
+ }
553
+
525
554
  if(encoding != nil)
526
555
  {
527
556
  if([[encoding lowercaseString] isEqualToString:@"utf8"])
@@ -0,0 +1,24 @@
1
+ // ReactNativeBlobUtilFileTransformer.h
2
+ // ReactNativeBlobUtil
3
+ //
4
+
5
+ #ifndef ReactNativeBlobUtilDataConverter_h
6
+ #define ReactNativeBlobUtilDataConverter_h
7
+
8
+ #import <UIKit/UIKit.h>
9
+
10
+
11
+ @protocol FileTransformer <NSObject>
12
+ - (NSData*) onWriteFile:(NSData*)data;
13
+ - (NSData*) onReadFile:(NSData*)data;
14
+ @end
15
+
16
+ @interface ReactNativeBlobUtilFileTransformer : NSObject
17
+
18
+ + (void) setFileTransformer:(NSObject<FileTransformer>*) fileTransformer;
19
+ + (NSObject<FileTransformer>*) getFileTransformer;
20
+
21
+ @end
22
+
23
+
24
+ #endif /* ReactNativeBlobUtilFileTransformer_h */
@@ -0,0 +1,21 @@
1
+ // ReactNativeBlobUtilFileTransformer.m
2
+ // ReactNativeBlobUtil
3
+ //
4
+
5
+ #import "ReactNativeBlobUtilFileTransformer.h"
6
+
7
+ static NSObject<FileTransformer> *sharedFileTransformer;
8
+
9
+ @implementation ReactNativeBlobUtilFileTransformer
10
+
11
+ + (void) setFileTransformer:(NSObject<FileTransformer> *)fileTransformer {
12
+ sharedFileTransformer = fileTransformer;
13
+ }
14
+
15
+ + (NSObject<FileTransformer>*) getFileTransformer {
16
+ return sharedFileTransformer;
17
+ }
18
+
19
+ @end
20
+
21
+ // PHOENIX:PATCH-PACKAGE END
@@ -119,7 +119,7 @@
119
119
  if([orgPath hasPrefix:AL_PREFIX])
120
120
  {
121
121
 
122
- [ReactNativeBlobUtilFS readFile:orgPath encoding:nil onComplete:^(id content, NSString* code, NSString * err) {
122
+ [ReactNativeBlobUtilFS readFile:orgPath encoding:nil transformFile:false onComplete:^(id content, NSString* code, NSString * err) {
123
123
  if(err != nil)
124
124
  {
125
125
  onComplete(nil, nil);
@@ -223,7 +223,7 @@
223
223
  NSString * orgPath = [content substringFromIndex:[FILE_PREFIX length]];
224
224
  orgPath = [ReactNativeBlobUtilFS getPathOfAsset:orgPath];
225
225
 
226
- [ReactNativeBlobUtilFS readFile:orgPath encoding:nil onComplete:^(NSData *content, NSString* code, NSString * err) {
226
+ [ReactNativeBlobUtilFS readFile:orgPath encoding:nil transformFile:false onComplete:^(NSData *content, NSString* code, NSString * err) {
227
227
  if(err != nil)
228
228
  {
229
229
  onComplete(formData, YES);
@@ -10,6 +10,7 @@
10
10
 
11
11
  #import "ReactNativeBlobUtilFS.h"
12
12
  #import "ReactNativeBlobUtilConst.h"
13
+ #import "ReactNativeBlobUtilFileTransformer.h"
13
14
  #import "ReactNativeBlobUtilReqBuilder.h"
14
15
 
15
16
  #import "IOS7Polyfill.h"
@@ -156,6 +157,12 @@ typedef NS_ENUM(NSUInteger, ResponseFormat) {
156
157
  } else {
157
158
  destPath = [ReactNativeBlobUtilFS getTempPath:cacheKey withExtension:[self.options valueForKey:CONFIG_FILE_EXT]];
158
159
  }
160
+
161
+ // We still need to initialize this as a placeholder in memory before we perform
162
+ // the conversion
163
+ if ([self ShouldTransformFile]) {
164
+ respData = [[NSMutableData alloc] init];
165
+ }
159
166
  } else {
160
167
  respData = [[NSMutableData alloc] init];
161
168
  respFile = NO;
@@ -333,7 +340,9 @@ typedef NS_ENUM(NSUInteger, ResponseFormat) {
333
340
  chunkString = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
334
341
  }
335
342
 
336
- if (respFile) {
343
+ // If we need to process the data, we defer writing into the file until the we have all the data, at which point
344
+ // we can perform the processing and then write into the file
345
+ if (respFile && ![self ShouldTransformFile]) {
337
346
  [writeStream write:[data bytes] maxLength:[data length]];
338
347
  } else {
339
348
  [respData appendData:data];
@@ -390,6 +399,22 @@ typedef NS_ENUM(NSUInteger, ResponseFormat) {
390
399
  }
391
400
 
392
401
  if (respFile) {
402
+ if ([self ShouldTransformFile]) {
403
+ // At this point, we have the data that we deferred to write into a file. We can now
404
+ // perform the conversion and write the converted data into the file.
405
+ NSObject<FileTransformer>* fileTransformer = [ReactNativeBlobUtilFileTransformer getFileTransformer];
406
+ if (!fileTransformer) {
407
+ errMsg = @"Transform file specified but file transfomer not set";
408
+ } else {
409
+ @try{
410
+ NSData* transformedData = [fileTransformer onWriteFile:respData];
411
+ [writeStream write:[transformedData bytes] maxLength:[transformedData length]];
412
+ } @catch(NSException * ex)
413
+ {
414
+ errMsg = [NSString stringWithFormat:@"Exception on File Transformer: '%@' ", [ex description]];
415
+ }
416
+ }
417
+ }
393
418
  [writeStream close];
394
419
  rnfbRespType = RESP_TYPE_PATH;
395
420
  respStr = destPath;
@@ -430,6 +455,10 @@ typedef NS_ENUM(NSUInteger, ResponseFormat) {
430
455
 
431
456
  }
432
457
 
458
+ - (BOOL) ShouldTransformFile{
459
+ return [[options valueForKey:CONFIG_TRANSFORM_FILE] boolValue];
460
+ }
461
+
433
462
  // upload progress handler
434
463
  - (void) URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didSendBodyData:(int64_t)bytesSent totalBytesSent:(int64_t)totalBytesWritten totalBytesExpectedToSend:(int64_t)totalBytesExpectedToWrite
435
464
  {
@@ -9,7 +9,11 @@ function createMediafile(fd: filedescriptor, mediatype: string): Promise {
9
9
  }
10
10
 
11
11
  function writeToMediafile(uri: string, path: string) {
12
- return ReactNativeBlobUtil.writeToMediaFile(uri, path);
12
+ return ReactNativeBlobUtil.writeToMediaFile(uri, path, false);
13
+ }
14
+
15
+ function writeToMediafileWithTransform(uri: string, path: string) {
16
+ return ReactNativeBlobUtil.writeToMediaFile(uri, path, true);
13
17
  }
14
18
 
15
19
  function copyToInternal(contenturi: string, destpath: string) {
@@ -27,6 +31,7 @@ function copyToMediaStore(fd: filedescriptor, mediatype: string, path: string) {
27
31
  export default {
28
32
  createMediafile,
29
33
  writeToMediafile,
34
+ writeToMediafileWithTransform,
30
35
  copyToInternal,
31
36
  getBlob,
32
37
  copyToMediaStore
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-blob-util",
3
- "version": "0.14.1",
3
+ "version": "0.15.0",
4
4
  "description": "A module provides upload, download, and files access API. Supports file stream read/write for process large files.",
5
5
  "main": "index.js",
6
6
  "scripts": {
package/polyfill/Fetch.js CHANGED
@@ -59,10 +59,12 @@ class ReactNativeBlobUtilFetchPolyfill {
59
59
  // task.then is not, so we have to extend task.then with progress and
60
60
  // cancel function
61
61
  let progressHandler, uploadHandler, cancelHandler;
62
+ let scopedTask = null;
62
63
  let statefulPromise = promise
63
64
  .then((body) => {
64
65
  let task = RNconfig(config)
65
66
  .fetch(options.method, url, options.headers, body);
67
+ scopedTask = task;
66
68
  if (progressHandler)
67
69
  task.progress(progressHandler);
68
70
  if (uploadHandler)
@@ -87,8 +89,8 @@ class ReactNativeBlobUtilFetchPolyfill {
87
89
  };
88
90
  statefulPromise.cancel = () => {
89
91
  cancelHandler = true;
90
- if (task.cancel)
91
- task.cancel();
92
+ if (scopedTask && scopedTask.cancel)
93
+ scopedTask.cancel();
92
94
  };
93
95
 
94
96
  return statefulPromise;
package/types.js CHANGED
@@ -3,6 +3,7 @@ export type ReactNativeBlobUtilConfig = {
3
3
  Progress: any,
4
4
  UploadProgress: any,
5
5
  fileCache : bool,
6
+ transformFile: boolean;
6
7
  path : string,
7
8
  appendExt : string,
8
9
  session : string,