Brak opisu

RNFetchBlobFS.java 43KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126
  1. package com.RNFetchBlob;
  2. import android.content.res.AssetFileDescriptor;
  3. import android.media.MediaScannerConnection;
  4. import android.net.Uri;
  5. import android.os.AsyncTask;
  6. import android.os.Build;
  7. import android.os.Environment;
  8. import android.os.StatFs;
  9. import android.os.SystemClock;
  10. import android.util.Base64;
  11. import com.RNFetchBlob.Utils.PathResolver;
  12. import com.facebook.react.bridge.Arguments;
  13. import com.facebook.react.bridge.Callback;
  14. import com.facebook.react.bridge.Promise;
  15. import com.facebook.react.bridge.ReactApplicationContext;
  16. import com.facebook.react.bridge.ReadableArray;
  17. import com.facebook.react.bridge.WritableArray;
  18. import com.facebook.react.bridge.WritableMap;
  19. import com.facebook.react.modules.core.DeviceEventManagerModule;
  20. import java.io.*;
  21. import java.nio.charset.Charset;
  22. import java.security.MessageDigest;
  23. import java.util.ArrayList;
  24. import java.util.HashMap;
  25. import java.util.Map;
  26. import java.util.UUID;
  27. class RNFetchBlobFS {
  28. private ReactApplicationContext mCtx;
  29. private DeviceEventManagerModule.RCTDeviceEventEmitter emitter;
  30. private String encoding = "base64";
  31. private OutputStream writeStreamInstance = null;
  32. private static HashMap<String, RNFetchBlobFS> fileStreams = new HashMap<>();
  33. RNFetchBlobFS(ReactApplicationContext ctx) {
  34. this.mCtx = ctx;
  35. this.emitter = ctx.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class);
  36. }
  37. /**
  38. * Write string with encoding to file
  39. * @param path Destination file path.
  40. * @param encoding Encoding of the string.
  41. * @param data Array passed from JS context.
  42. * @param promise RCT Promise
  43. */
  44. static void writeFile(String path, String encoding, String data, final boolean append, final Promise promise) {
  45. try {
  46. int written;
  47. File f = new File(path);
  48. File dir = f.getParentFile();
  49. if(!f.exists()) {
  50. if(dir != null && !dir.exists()) {
  51. if (!dir.mkdirs()) {
  52. promise.reject("EUNSPECIFIED", "Failed to create parent directory of '" + path + "'");
  53. return;
  54. }
  55. }
  56. if(!f.createNewFile()) {
  57. promise.reject("ENOENT", "File '" + path + "' does not exist and could not be created");
  58. return;
  59. }
  60. }
  61. FileOutputStream fout = new FileOutputStream(f, append);
  62. // write data from a file
  63. if(encoding.equalsIgnoreCase(RNFetchBlobConst.DATA_ENCODE_URI)) {
  64. String normalizedData = normalizePath(data);
  65. File src = new File(normalizedData);
  66. if (!src.exists()) {
  67. promise.reject("ENOENT", "No such file '" + path + "' " + "('" + normalizedData + "')");
  68. fout.close();
  69. return;
  70. }
  71. FileInputStream fin = new FileInputStream(src);
  72. byte[] buffer = new byte [10240];
  73. int read;
  74. written = 0;
  75. while((read = fin.read(buffer)) > 0) {
  76. fout.write(buffer, 0, read);
  77. written += read;
  78. }
  79. fin.close();
  80. }
  81. else {
  82. byte[] bytes = stringToBytes(data, encoding);
  83. fout.write(bytes);
  84. written = bytes.length;
  85. }
  86. fout.close();
  87. promise.resolve(written);
  88. } catch (FileNotFoundException e) {
  89. // According to https://docs.oracle.com/javase/7/docs/api/java/io/FileOutputStream.html
  90. promise.reject("ENOENT", "File '" + path + "' does not exist and could not be created, or it is a directory");
  91. } catch (Exception e) {
  92. promise.reject("EUNSPECIFIED", e.getLocalizedMessage());
  93. }
  94. }
  95. /**
  96. * Write array of bytes into file
  97. * @param path Destination file path.
  98. * @param data Array passed from JS context.
  99. * @param promise RCT Promise
  100. */
  101. static void writeFile(String path, ReadableArray data, final boolean append, final Promise promise) {
  102. try {
  103. File f = new File(path);
  104. File dir = f.getParentFile();
  105. if(!f.exists()) {
  106. if(dir != null && !dir.exists()) {
  107. if (!dir.mkdirs()) {
  108. promise.reject("ENOTDIR", "Failed to create parent directory of '" + path + "'");
  109. return;
  110. }
  111. }
  112. if(!f.createNewFile()) {
  113. promise.reject("ENOENT", "File '" + path + "' does not exist and could not be created");
  114. return;
  115. }
  116. }
  117. FileOutputStream os = new FileOutputStream(f, append);
  118. byte[] bytes = new byte[data.size()];
  119. for(int i=0;i<data.size();i++) {
  120. bytes[i] = (byte) data.getInt(i);
  121. }
  122. os.write(bytes);
  123. os.close();
  124. promise.resolve(data.size());
  125. } catch (FileNotFoundException e) {
  126. // According to https://docs.oracle.com/javase/7/docs/api/java/io/FileOutputStream.html
  127. promise.reject("ENOENT", "File '" + path + "' does not exist and could not be created");
  128. } catch (Exception e) {
  129. promise.reject("EUNSPECIFIED", e.getLocalizedMessage());
  130. }
  131. }
  132. /**
  133. * Read file with a buffer that has the same size as the target file.
  134. * @param path Path of the file.
  135. * @param encoding Encoding of read stream.
  136. * @param promise JS promise
  137. */
  138. static void readFile(String path, String encoding, final Promise promise) {
  139. String resolved = normalizePath(path);
  140. if(resolved != null)
  141. path = resolved;
  142. try {
  143. byte[] bytes;
  144. int bytesRead;
  145. int length; // max. array length limited to "int", also see https://stackoverflow.com/a/10787175/544779
  146. if(resolved != null && resolved.startsWith(RNFetchBlobConst.FILE_PREFIX_BUNDLE_ASSET)) {
  147. String assetName = path.replace(RNFetchBlobConst.FILE_PREFIX_BUNDLE_ASSET, "");
  148. // This fails should an asset file be >2GB
  149. length = (int) RNFetchBlob.RCTContext.getAssets().openFd(assetName).getLength();
  150. bytes = new byte[length];
  151. InputStream in = RNFetchBlob.RCTContext.getAssets().open(assetName);
  152. bytesRead = in.read(bytes, 0, length);
  153. in.close();
  154. }
  155. // issue 287
  156. else if(resolved == null) {
  157. InputStream in = RNFetchBlob.RCTContext.getContentResolver().openInputStream(Uri.parse(path));
  158. // TODO See https://developer.android.com/reference/java/io/InputStream.html#available()
  159. // Quote: "Note that while some implementations of InputStream will return the total number of bytes
  160. // in the stream, many will not. It is never correct to use the return value of this method to
  161. // allocate a buffer intended to hold all data in this stream."
  162. length = in.available();
  163. bytes = new byte[length];
  164. bytesRead = in.read(bytes);
  165. in.close();
  166. }
  167. else {
  168. File f = new File(path);
  169. length = (int) f.length();
  170. bytes = new byte[length];
  171. FileInputStream in = new FileInputStream(f);
  172. bytesRead = in.read(bytes);
  173. in.close();
  174. }
  175. if (bytesRead < length) {
  176. promise.reject("EUNSPECIFIED", "Read only " + bytesRead + " bytes of " + length);
  177. return;
  178. }
  179. switch (encoding.toLowerCase()) {
  180. case "base64" :
  181. promise.resolve(Base64.encodeToString(bytes, Base64.NO_WRAP));
  182. break;
  183. case "ascii" :
  184. WritableArray asciiResult = Arguments.createArray();
  185. for (byte b : bytes) {
  186. asciiResult.pushInt((int) b);
  187. }
  188. promise.resolve(asciiResult);
  189. break;
  190. case "utf8" :
  191. promise.resolve(new String(bytes));
  192. break;
  193. default:
  194. promise.resolve(new String(bytes));
  195. break;
  196. }
  197. }
  198. catch(FileNotFoundException err) {
  199. String msg = err.getLocalizedMessage();
  200. if (msg.contains("EISDIR")) {
  201. promise.reject("EISDIR", "Expecting a file but '" + path + "' is a directory; " + msg);
  202. } else {
  203. promise.reject("ENOENT", "No such file '" + path + "'; " + msg);
  204. }
  205. }
  206. catch(Exception err) {
  207. promise.reject("EUNSPECIFIED", err.getLocalizedMessage());
  208. }
  209. }
  210. /**
  211. * Static method that returns system folders to JS context
  212. * @param ctx React Native application context
  213. */
  214. static Map<String, Object> getSystemfolders(ReactApplicationContext ctx) {
  215. Map<String, Object> res = new HashMap<>();
  216. res.put("DocumentDir", ctx.getFilesDir().getAbsolutePath());
  217. res.put("CacheDir", ctx.getCacheDir().getAbsolutePath());
  218. res.put("DCIMDir", Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM).getAbsolutePath());
  219. res.put("PictureDir", Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES).getAbsolutePath());
  220. res.put("MusicDir", Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MUSIC).getAbsolutePath());
  221. res.put("DownloadDir", Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).getAbsolutePath());
  222. res.put("MovieDir", Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MOVIES).getAbsolutePath());
  223. res.put("RingtoneDir", Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_RINGTONES).getAbsolutePath());
  224. String state;
  225. state = Environment.getExternalStorageState();
  226. if (state.equals(Environment.MEDIA_MOUNTED)) {
  227. res.put("SDCardDir", Environment.getExternalStorageDirectory().getAbsolutePath());
  228. File externalDirectory = ctx.getExternalFilesDir(null);
  229. if (externalDirectory != null) {
  230. res.put("SDCardApplicationDir", externalDirectory.getParentFile().getAbsolutePath());
  231. } else {
  232. res.put("SDCardApplicationDir", "");
  233. }
  234. }
  235. res.put("MainBundleDir", ctx.getApplicationInfo().dataDir);
  236. return res;
  237. }
  238. static public void getSDCardDir(Promise promise) {
  239. if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
  240. promise.resolve(Environment.getExternalStorageDirectory().getAbsolutePath());
  241. } else {
  242. promise.reject("RNFetchBlob.getSDCardDir", "External storage not mounted");
  243. }
  244. }
  245. static public void getSDCardApplicationDir(ReactApplicationContext ctx, Promise promise) {
  246. if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
  247. try {
  248. final String path = ctx.getExternalFilesDir(null).getParentFile().getAbsolutePath();
  249. promise.resolve(path);
  250. } catch (Exception e) {
  251. promise.reject("RNFetchBlob.getSDCardApplicationDir", e.getLocalizedMessage());
  252. }
  253. } else {
  254. promise.reject("RNFetchBlob.getSDCardApplicationDir", "External storage not mounted");
  255. }
  256. }
  257. /**
  258. * Static method that returns a temp file path
  259. * @param taskId An unique string for identify
  260. * @return String
  261. */
  262. static String getTmpPath(String taskId) {
  263. return RNFetchBlob.RCTContext.getFilesDir() + "/RNFetchBlobTmp_" + taskId;
  264. }
  265. /**
  266. * Create a file stream for read
  267. * @param path File stream target path
  268. * @param encoding File stream decoder, should be one of `base64`, `utf8`, `ascii`
  269. * @param bufferSize Buffer size of read stream, default to 4096 (4095 when encode is `base64`)
  270. */
  271. void readStream(String path, String encoding, int bufferSize, int tick, final String streamId) {
  272. String resolved = normalizePath(path);
  273. if(resolved != null)
  274. path = resolved;
  275. try {
  276. int chunkSize = encoding.equalsIgnoreCase("base64") ? 4095 : 4096;
  277. if(bufferSize > 0)
  278. chunkSize = bufferSize;
  279. InputStream fs;
  280. if(resolved != null && path.startsWith(RNFetchBlobConst.FILE_PREFIX_BUNDLE_ASSET)) {
  281. fs = RNFetchBlob.RCTContext.getAssets().open(path.replace(RNFetchBlobConst.FILE_PREFIX_BUNDLE_ASSET, ""));
  282. }
  283. // fix issue 287
  284. else if(resolved == null) {
  285. fs = RNFetchBlob.RCTContext.getContentResolver().openInputStream(Uri.parse(path));
  286. }
  287. else {
  288. fs = new FileInputStream(new File(path));
  289. }
  290. byte[] buffer = new byte[chunkSize];
  291. int cursor = 0;
  292. boolean error = false;
  293. if (encoding.equalsIgnoreCase("utf8")) {
  294. while ((cursor = fs.read(buffer)) != -1) {
  295. String chunk = new String(buffer, 0, cursor);
  296. emitStreamEvent(streamId, "data", chunk);
  297. if(tick > 0)
  298. SystemClock.sleep(tick);
  299. }
  300. } else if (encoding.equalsIgnoreCase("ascii")) {
  301. while ((cursor = fs.read(buffer)) != -1) {
  302. WritableArray chunk = Arguments.createArray();
  303. for(int i =0;i<cursor;i++)
  304. {
  305. chunk.pushInt((int)buffer[i]);
  306. }
  307. emitStreamEvent(streamId, "data", chunk);
  308. if(tick > 0)
  309. SystemClock.sleep(tick);
  310. }
  311. } else if (encoding.equalsIgnoreCase("base64")) {
  312. while ((cursor = fs.read(buffer)) != -1) {
  313. if(cursor < chunkSize) {
  314. byte[] copy = new byte[cursor];
  315. System.arraycopy(buffer, 0, copy, 0, cursor);
  316. emitStreamEvent(streamId, "data", Base64.encodeToString(copy, Base64.NO_WRAP));
  317. }
  318. else
  319. emitStreamEvent(streamId, "data", Base64.encodeToString(buffer, Base64.NO_WRAP));
  320. if(tick > 0)
  321. SystemClock.sleep(tick);
  322. }
  323. } else {
  324. emitStreamEvent(
  325. streamId,
  326. "error",
  327. "EINVAL",
  328. "Unrecognized encoding `" + encoding + "`, should be one of `base64`, `utf8`, `ascii`"
  329. );
  330. error = true;
  331. }
  332. if(!error)
  333. emitStreamEvent(streamId, "end", "");
  334. fs.close();
  335. buffer = null;
  336. } catch (FileNotFoundException err) {
  337. emitStreamEvent(
  338. streamId,
  339. "error",
  340. "ENOENT",
  341. "No such file '" + path + "'"
  342. );
  343. } catch (Exception err) {
  344. emitStreamEvent(
  345. streamId,
  346. "error",
  347. "EUNSPECIFIED",
  348. "Failed to convert data to " + encoding + " encoded string. This might be because this encoding cannot be used for this data."
  349. );
  350. err.printStackTrace();
  351. }
  352. }
  353. /**
  354. * Create a write stream and store its instance in RNFetchBlobFS.fileStreams
  355. * @param path Target file path
  356. * @param encoding Should be one of `base64`, `utf8`, `ascii`
  357. * @param append Flag represents if the file stream overwrite existing content
  358. * @param callback Callback
  359. */
  360. void writeStream(String path, String encoding, boolean append, Callback callback) {
  361. try {
  362. File dest = new File(path);
  363. File dir = dest.getParentFile();
  364. if(!dest.exists()) {
  365. if(dir != null && !dir.exists()) {
  366. if (!dir.mkdirs()) {
  367. callback.invoke("ENOTDIR", "Failed to create parent directory of '" + path + "'");
  368. return;
  369. }
  370. }
  371. if(!dest.createNewFile()) {
  372. callback.invoke("ENOENT", "File '" + path + "' does not exist and could not be created");
  373. return;
  374. }
  375. } else if(dest.isDirectory()) {
  376. callback.invoke("EISDIR", "Expecting a file but '" + path + "' is a directory");
  377. return;
  378. }
  379. OutputStream fs = new FileOutputStream(path, append);
  380. this.encoding = encoding;
  381. String streamId = UUID.randomUUID().toString();
  382. RNFetchBlobFS.fileStreams.put(streamId, this);
  383. this.writeStreamInstance = fs;
  384. callback.invoke(null, null, streamId);
  385. } catch(Exception err) {
  386. callback.invoke("EUNSPECIFIED", "Failed to create write stream at path `" + path + "`; " + err.getLocalizedMessage());
  387. }
  388. }
  389. /**
  390. * Write a chunk of data into a file stream.
  391. * @param streamId File stream ID
  392. * @param data Data chunk in string format
  393. * @param callback JS context callback
  394. */
  395. static void writeChunk(String streamId, String data, Callback callback) {
  396. RNFetchBlobFS fs = fileStreams.get(streamId);
  397. OutputStream stream = fs.writeStreamInstance;
  398. byte[] chunk = RNFetchBlobFS.stringToBytes(data, fs.encoding);
  399. try {
  400. stream.write(chunk);
  401. callback.invoke();
  402. } catch (Exception e) {
  403. callback.invoke(e.getLocalizedMessage());
  404. }
  405. }
  406. /**
  407. * Write data using ascii array
  408. * @param streamId File stream ID
  409. * @param data Data chunk in ascii array format
  410. * @param callback JS context callback
  411. */
  412. static void writeArrayChunk(String streamId, ReadableArray data, Callback callback) {
  413. try {
  414. RNFetchBlobFS fs = fileStreams.get(streamId);
  415. OutputStream stream = fs.writeStreamInstance;
  416. byte[] chunk = new byte[data.size()];
  417. for(int i =0; i< data.size();i++) {
  418. chunk[i] = (byte) data.getInt(i);
  419. }
  420. stream.write(chunk);
  421. callback.invoke();
  422. } catch (Exception e) {
  423. callback.invoke(e.getLocalizedMessage());
  424. }
  425. }
  426. /**
  427. * Close file write stream by ID
  428. * @param streamId Stream ID
  429. * @param callback JS context callback
  430. */
  431. static void closeStream(String streamId, Callback callback) {
  432. try {
  433. RNFetchBlobFS fs = fileStreams.get(streamId);
  434. OutputStream stream = fs.writeStreamInstance;
  435. fileStreams.remove(streamId);
  436. stream.close();
  437. callback.invoke();
  438. } catch(Exception err) {
  439. callback.invoke(err.getLocalizedMessage());
  440. }
  441. }
  442. /**
  443. * Unlink file at path
  444. * @param path Path of target
  445. * @param callback JS context callback
  446. */
  447. static void unlink(String path, Callback callback) {
  448. try {
  449. String normalizedPath = normalizePath(path);
  450. RNFetchBlobFS.deleteRecursive(new File(normalizedPath));
  451. callback.invoke(null, true);
  452. } catch(Exception err) {
  453. callback.invoke(err.getLocalizedMessage(), false);
  454. }
  455. }
  456. private static void deleteRecursive(File fileOrDirectory) throws IOException {
  457. if (fileOrDirectory.isDirectory()) {
  458. File[] files = fileOrDirectory.listFiles();
  459. if (files == null) {
  460. throw new NullPointerException("Received null trying to list files of directory '" + fileOrDirectory + "'");
  461. } else {
  462. for (File child : files) {
  463. deleteRecursive(child);
  464. }
  465. }
  466. }
  467. boolean result = fileOrDirectory.delete();
  468. if (!result) {
  469. throw new IOException("Failed to delete '" + fileOrDirectory + "'");
  470. }
  471. }
  472. /**
  473. * Make a folder
  474. * @param path Source path
  475. * @param promise JS promise
  476. */
  477. static void mkdir(String path, Promise promise) {
  478. File dest = new File(path);
  479. if(dest.exists()) {
  480. promise.reject("EEXIST", dest.isDirectory() ? "Folder" : "File" + " '" + path + "' already exists");
  481. return;
  482. }
  483. try {
  484. boolean result = dest.mkdirs();
  485. if (!result) {
  486. promise.reject("EUNSPECIFIED", "mkdir failed to create some or all directories in '" + path + "'");
  487. return;
  488. }
  489. } catch (Exception e) {
  490. promise.reject("EUNSPECIFIED", e.getLocalizedMessage());
  491. return;
  492. }
  493. promise.resolve(true);
  494. }
  495. /**
  496. * Copy file to destination path
  497. * @param path Source path
  498. * @param dest Target path
  499. * @param callback JS context callback
  500. */
  501. static void cp(String path, String dest, Callback callback) {
  502. path = normalizePath(path);
  503. InputStream in = null;
  504. OutputStream out = null;
  505. String message = "";
  506. try {
  507. if(!isPathExists(path)) {
  508. callback.invoke("Source file at path`" + path + "` does not exist");
  509. return;
  510. }
  511. if(!new File(dest).exists()) {
  512. boolean result = new File(dest).createNewFile();
  513. if (!result) {
  514. callback.invoke("Destination file at '" + dest + "' already exists");
  515. return;
  516. }
  517. }
  518. in = inputStreamFromPath(path);
  519. out = new FileOutputStream(dest);
  520. byte[] buf = new byte[10240];
  521. int len;
  522. while ((len = in.read(buf)) > 0) {
  523. out.write(buf, 0, len);
  524. }
  525. } catch (Exception err) {
  526. message += err.getLocalizedMessage();
  527. } finally {
  528. try {
  529. if (in != null) {
  530. in.close();
  531. }
  532. if (out != null) {
  533. out.close();
  534. }
  535. } catch (Exception e) {
  536. message += e.getLocalizedMessage();
  537. }
  538. }
  539. // Only call the callback once to prevent the app from crashing
  540. // with an 'Illegal callback invocation from native module' exception.
  541. if (message != "") {
  542. callback.invoke(message);
  543. } else {
  544. callback.invoke();
  545. }
  546. }
  547. /**
  548. * Move file
  549. * @param path Source file path
  550. * @param dest Destination file path
  551. * @param callback JS context callback
  552. */
  553. static void mv(String path, String dest, Callback callback) {
  554. File src = new File(path);
  555. if(!src.exists()) {
  556. callback.invoke("Source file at path `" + path + "` does not exist");
  557. return;
  558. }
  559. try {
  560. InputStream in = new FileInputStream(path);
  561. OutputStream out = new FileOutputStream(dest);
  562. //read source path to byte buffer. Write from input to output stream
  563. byte[] buffer = new byte[1024];
  564. int read;
  565. while ((read = in.read(buffer)) != -1) { //read is successful
  566. out.write(buffer, 0, read);
  567. }
  568. in.close();
  569. out.flush();
  570. src.delete(); //remove original file
  571. } catch (FileNotFoundException exception) {
  572. callback.invoke("Source file not found.");
  573. return;
  574. } catch (Exception e) {
  575. callback.invoke(e.toString());
  576. return;
  577. }
  578. callback.invoke();
  579. }
  580. /**
  581. * Check if the path exists, also check if it is a folder when exists.
  582. * @param path Path to check
  583. * @param callback JS context callback
  584. */
  585. static void exists(String path, Callback callback) {
  586. if(isAsset(path)) {
  587. try {
  588. String filename = path.replace(RNFetchBlobConst.FILE_PREFIX_BUNDLE_ASSET, "");
  589. com.RNFetchBlob.RNFetchBlob.RCTContext.getAssets().openFd(filename);
  590. callback.invoke(true, false);
  591. } catch (IOException e) {
  592. callback.invoke(false, false);
  593. }
  594. }
  595. else {
  596. path = normalizePath(path);
  597. boolean exist = new File(path).exists();
  598. boolean isDir = new File(path).isDirectory();
  599. callback.invoke(exist, isDir);
  600. }
  601. }
  602. /**
  603. * List content of folder
  604. * @param path Target folder
  605. * @param callback JS context callback
  606. */
  607. static void ls(String path, Promise promise) {
  608. try {
  609. path = normalizePath(path);
  610. File src = new File(path);
  611. if (!src.exists()) {
  612. promise.reject("ENOENT", "No such file '" + path + "'");
  613. return;
  614. }
  615. if (!src.isDirectory()) {
  616. promise.reject("ENOTDIR", "Not a directory '" + path + "'");
  617. return;
  618. }
  619. String[] files = new File(path).list();
  620. WritableArray arg = Arguments.createArray();
  621. // File => list(): "If this abstract pathname does not denote a directory, then this method returns null."
  622. // We excluded that possibility above - ignore the "can produce NullPointerException" warning of the IDE.
  623. for (String i : files) {
  624. arg.pushString(i);
  625. }
  626. promise.resolve(arg);
  627. } catch (Exception e) {
  628. e.printStackTrace();
  629. promise.reject("EUNSPECIFIED", e.getLocalizedMessage());
  630. }
  631. }
  632. /**
  633. * Create a file by slicing given file path
  634. * @param path Source file path
  635. * @param dest Destination of created file
  636. * @param start Start byte offset in source file
  637. * @param end End byte offset
  638. * @param encode NOT IMPLEMENTED
  639. */
  640. static void slice(String path, String dest, int start, int end, String encode, Promise promise) {
  641. try {
  642. path = normalizePath(path);
  643. File source = new File(path);
  644. if(source.isDirectory()){
  645. promise.reject("EISDIR", "Expecting a file but '" + path + "' is a directory");
  646. return;
  647. }
  648. if(!source.exists()){
  649. promise.reject("ENOENT", "No such file '" + path + "'");
  650. return;
  651. }
  652. int size = (int) source.length();
  653. int max = Math.min(size, end);
  654. int expected = max - start;
  655. int now = 0;
  656. FileInputStream in = new FileInputStream(new File(path));
  657. FileOutputStream out = new FileOutputStream(new File(dest));
  658. int skipped = (int) in.skip(start);
  659. if (skipped != start) {
  660. promise.reject("EUNSPECIFIED", "Skipped " + skipped + " instead of the specified " + start + " bytes, size is " + size);
  661. return;
  662. }
  663. byte[] buffer = new byte[10240];
  664. while(now < expected) {
  665. int read = in.read(buffer, 0, 10240);
  666. int remain = expected - now;
  667. if(read <= 0) {
  668. break;
  669. }
  670. out.write(buffer, 0, (int) Math.min(remain, read));
  671. now += read;
  672. }
  673. in.close();
  674. out.flush();
  675. out.close();
  676. promise.resolve(dest);
  677. } catch (Exception e) {
  678. e.printStackTrace();
  679. promise.reject("EUNSPECIFIED", e.getLocalizedMessage());
  680. }
  681. }
  682. static void lstat(String path, final Callback callback) {
  683. path = normalizePath(path);
  684. new AsyncTask<String, Integer, Integer>() {
  685. @Override
  686. protected Integer doInBackground(String ...args) {
  687. WritableArray res = Arguments.createArray();
  688. if(args[0] == null) {
  689. callback.invoke("the path specified for lstat is either `null` or `undefined`.");
  690. return 0;
  691. }
  692. File src = new File(args[0]);
  693. if(!src.exists()) {
  694. callback.invoke("failed to lstat path `" + args[0] + "` because it does not exist or it is not a folder");
  695. return 0;
  696. }
  697. if(src.isDirectory()) {
  698. String [] files = src.list();
  699. // File => list(): "If this abstract pathname does not denote a directory, then this method returns null."
  700. // We excluded that possibility above - ignore the "can produce NullPointerException" warning of the IDE.
  701. for(String p : files) {
  702. res.pushMap(statFile(src.getPath() + "/" + p));
  703. }
  704. }
  705. else {
  706. res.pushMap(statFile(src.getAbsolutePath()));
  707. }
  708. callback.invoke(null, res);
  709. return 0;
  710. }
  711. }.execute(path);
  712. }
  713. /**
  714. * show status of a file or directory
  715. * @param path Path
  716. * @param callback Callback
  717. */
  718. static void stat(String path, Callback callback) {
  719. try {
  720. path = normalizePath(path);
  721. WritableMap result = statFile(path);
  722. if(result == null)
  723. callback.invoke("failed to stat path `" + path + "` because it does not exist or it is not a folder", null);
  724. else
  725. callback.invoke(null, result);
  726. } catch(Exception err) {
  727. callback.invoke(err.getLocalizedMessage());
  728. }
  729. }
  730. /**
  731. * Basic stat method
  732. * @param path Path
  733. * @return Stat Result of a file or path
  734. */
  735. static WritableMap statFile(String path) {
  736. try {
  737. path = normalizePath(path);
  738. WritableMap stat = Arguments.createMap();
  739. if(isAsset(path)) {
  740. String name = path.replace(RNFetchBlobConst.FILE_PREFIX_BUNDLE_ASSET, "");
  741. AssetFileDescriptor fd = RNFetchBlob.RCTContext.getAssets().openFd(name);
  742. stat.putString("filename", name);
  743. stat.putString("path", path);
  744. stat.putString("type", "asset");
  745. stat.putString("size", String.valueOf(fd.getLength()));
  746. stat.putInt("lastModified", 0);
  747. }
  748. else {
  749. File target = new File(path);
  750. if (!target.exists()) {
  751. return null;
  752. }
  753. stat.putString("filename", target.getName());
  754. stat.putString("path", target.getPath());
  755. stat.putString("type", target.isDirectory() ? "directory" : "file");
  756. stat.putString("size", String.valueOf(target.length()));
  757. String lastModified = String.valueOf(target.lastModified());
  758. stat.putString("lastModified", lastModified);
  759. }
  760. return stat;
  761. } catch(Exception err) {
  762. return null;
  763. }
  764. }
  765. /**
  766. * Media scanner scan file
  767. * @param path Path to file
  768. * @param mimes Array of MIME type strings
  769. * @param callback Callback for results
  770. */
  771. void scanFile(String [] path, String[] mimes, final Callback callback) {
  772. try {
  773. MediaScannerConnection.scanFile(mCtx, path, mimes, new MediaScannerConnection.OnScanCompletedListener() {
  774. @Override
  775. public void onScanCompleted(String s, Uri uri) {
  776. callback.invoke(null, true);
  777. }
  778. });
  779. } catch(Exception err) {
  780. callback.invoke(err.getLocalizedMessage(), null);
  781. }
  782. }
  783. static void hash(String path, String algorithm, Promise promise) {
  784. try {
  785. Map<String, String> algorithms = new HashMap<>();
  786. algorithms.put("md5", "MD5");
  787. algorithms.put("sha1", "SHA-1");
  788. algorithms.put("sha224", "SHA-224");
  789. algorithms.put("sha256", "SHA-256");
  790. algorithms.put("sha384", "SHA-384");
  791. algorithms.put("sha512", "SHA-512");
  792. if (!algorithms.containsKey(algorithm)) {
  793. promise.reject("EINVAL", "Invalid algorithm '" + algorithm + "', must be one of md5, sha1, sha224, sha256, sha384, sha512");
  794. return;
  795. }
  796. File file = new File(path);
  797. if (file.isDirectory()) {
  798. promise.reject("EISDIR", "Expecting a file but '" + path + "' is a directory");
  799. return;
  800. }
  801. if (!file.exists()) {
  802. promise.reject("ENOENT", "No such file '" + path + "'");
  803. return;
  804. }
  805. MessageDigest md = MessageDigest.getInstance(algorithms.get(algorithm));
  806. FileInputStream inputStream = new FileInputStream(path);
  807. byte[] buffer = new byte[(int)file.length()];
  808. int read;
  809. while ((read = inputStream.read(buffer)) != -1) {
  810. md.update(buffer, 0, read);
  811. }
  812. StringBuilder hexString = new StringBuilder();
  813. for (byte digestByte : md.digest())
  814. hexString.append(String.format("%02x", digestByte));
  815. promise.resolve(hexString.toString());
  816. } catch (Exception e) {
  817. e.printStackTrace();
  818. promise.reject("EUNSPECIFIED", e.getLocalizedMessage());
  819. }
  820. }
  821. /**
  822. * Create new file at path
  823. * @param path The destination path of the new file.
  824. * @param data Initial data of the new file.
  825. * @param encoding Encoding of initial data.
  826. * @param promise Promise for Javascript
  827. */
  828. static void createFile(String path, String data, String encoding, Promise promise) {
  829. try {
  830. File dest = new File(path);
  831. boolean created = dest.createNewFile();
  832. if(encoding.equals(RNFetchBlobConst.DATA_ENCODE_URI)) {
  833. String orgPath = data.replace(RNFetchBlobConst.FILE_PREFIX, "");
  834. File src = new File(orgPath);
  835. if(!src.exists()) {
  836. promise.reject("ENOENT", "Source file : " + data + " does not exist");
  837. return ;
  838. }
  839. FileInputStream fin = new FileInputStream(src);
  840. OutputStream ostream = new FileOutputStream(dest);
  841. byte[] buffer = new byte[10240];
  842. int read = fin.read(buffer);
  843. while (read > 0) {
  844. ostream.write(buffer, 0, read);
  845. read = fin.read(buffer);
  846. }
  847. fin.close();
  848. ostream.close();
  849. } else {
  850. if (!created) {
  851. promise.reject("EEXIST", "File `" + path + "` already exists");
  852. return;
  853. }
  854. OutputStream ostream = new FileOutputStream(dest);
  855. ostream.write(RNFetchBlobFS.stringToBytes(data, encoding));
  856. }
  857. promise.resolve(path);
  858. } catch(Exception err) {
  859. promise.reject("EUNSPECIFIED", err.getLocalizedMessage());
  860. }
  861. }
  862. /**
  863. * Create file for ASCII encoding
  864. * @param path Path of new file.
  865. * @param data Content of new file
  866. * @param promise JS Promise
  867. */
  868. static void createFileASCII(String path, ReadableArray data, Promise promise) {
  869. try {
  870. File dest = new File(path);
  871. boolean created = dest.createNewFile();
  872. if(!created) {
  873. promise.reject("EEXIST", "File at path `" + path + "` already exists");
  874. return;
  875. }
  876. OutputStream ostream = new FileOutputStream(dest);
  877. byte[] chunk = new byte[data.size()];
  878. for(int i=0; i<data.size(); i++) {
  879. chunk[i] = (byte) data.getInt(i);
  880. }
  881. ostream.write(chunk);
  882. promise.resolve(path);
  883. } catch(Exception err) {
  884. promise.reject("EUNSPECIFIED", err.getLocalizedMessage());
  885. }
  886. }
  887. static void df(Callback callback) {
  888. StatFs stat = new StatFs(Environment.getDataDirectory().getPath());
  889. WritableMap args = Arguments.createMap();
  890. if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
  891. args.putString("internal_free", String.valueOf(stat.getFreeBytes()));
  892. args.putString("internal_total", String.valueOf(stat.getTotalBytes()));
  893. StatFs statEx = new StatFs(Environment.getExternalStorageDirectory().getPath());
  894. args.putString("external_free", String.valueOf(statEx.getFreeBytes()));
  895. args.putString("external_total", String.valueOf(statEx.getTotalBytes()));
  896. }
  897. callback.invoke(null ,args);
  898. }
  899. /**
  900. * Remove files in session.
  901. * @param paths An array of file paths.
  902. * @param callback JS contest callback
  903. */
  904. static void removeSession(ReadableArray paths, final Callback callback) {
  905. AsyncTask<ReadableArray, Integer, Integer> task = new AsyncTask<ReadableArray, Integer, Integer>() {
  906. @Override
  907. protected Integer doInBackground(ReadableArray ...paths) {
  908. try {
  909. ArrayList<String> failuresToDelete = new ArrayList<>();
  910. for (int i = 0; i < paths[0].size(); i++) {
  911. String fileName = paths[0].getString(i);
  912. File f = new File(fileName);
  913. if (f.exists()) {
  914. boolean result = f.delete();
  915. if (!result) {
  916. failuresToDelete.add(fileName);
  917. }
  918. }
  919. }
  920. if (failuresToDelete.isEmpty()) {
  921. callback.invoke(null, true);
  922. } else {
  923. StringBuilder listString = new StringBuilder();
  924. listString.append("Failed to delete: ");
  925. for (String s : failuresToDelete) {
  926. listString.append(s).append(", ");
  927. }
  928. callback.invoke(listString.toString());
  929. }
  930. } catch(Exception err) {
  931. callback.invoke(err.getLocalizedMessage());
  932. }
  933. return paths[0].size();
  934. }
  935. };
  936. task.execute(paths);
  937. }
  938. /**
  939. * String to byte converter method
  940. * @param data Raw data in string format
  941. * @param encoding Decoder name
  942. * @return Converted data byte array
  943. */
  944. private static byte[] stringToBytes(String data, String encoding) {
  945. if(encoding.equalsIgnoreCase("ascii")) {
  946. return data.getBytes(Charset.forName("US-ASCII"));
  947. }
  948. else if(encoding.toLowerCase().contains("base64")) {
  949. return Base64.decode(data, Base64.NO_WRAP);
  950. }
  951. else if(encoding.equalsIgnoreCase("utf8")) {
  952. return data.getBytes(Charset.forName("UTF-8"));
  953. }
  954. return data.getBytes(Charset.forName("US-ASCII"));
  955. }
  956. /**
  957. * Private method for emit read stream event.
  958. * @param streamName ID of the read stream
  959. * @param event Event name, `data`, `end`, `error`, etc.
  960. * @param data Event data
  961. */
  962. private void emitStreamEvent(String streamName, String event, String data) {
  963. WritableMap eventData = Arguments.createMap();
  964. eventData.putString("event", event);
  965. eventData.putString("detail", data);
  966. this.emitter.emit(streamName, eventData);
  967. }
  968. // "event" always is "data"...
  969. private void emitStreamEvent(String streamName, String event, WritableArray data) {
  970. WritableMap eventData = Arguments.createMap();
  971. eventData.putString("event", event);
  972. eventData.putArray("detail", data);
  973. this.emitter.emit(streamName, eventData);
  974. }
  975. // "event" always is "error"...
  976. private void emitStreamEvent(String streamName, String event, String code, String message) {
  977. WritableMap eventData = Arguments.createMap();
  978. eventData.putString("event", event);
  979. eventData.putString("code", code);
  980. eventData.putString("detail", message);
  981. this.emitter.emit(streamName, eventData);
  982. }
  983. /**
  984. * Get input stream of the given path, when the path is a string starts with bundle-assets://
  985. * the stream is created by Assets Manager, otherwise use FileInputStream.
  986. * @param path The file to open stream
  987. * @return InputStream instance
  988. * @throws IOException If the given file does not exist or is a directory FileInputStream will throw a FileNotFoundException
  989. */
  990. private static InputStream inputStreamFromPath(String path) throws IOException {
  991. if (path.startsWith(RNFetchBlobConst.FILE_PREFIX_BUNDLE_ASSET)) {
  992. return RNFetchBlob.RCTContext.getAssets().open(path.replace(RNFetchBlobConst.FILE_PREFIX_BUNDLE_ASSET, ""));
  993. }
  994. return new FileInputStream(new File(path));
  995. }
  996. /**
  997. * Check if the asset or the file exists
  998. * @param path A file path URI string
  999. * @return A boolean value represents if the path exists.
  1000. */
  1001. private static boolean isPathExists(String path) {
  1002. if(path.startsWith(RNFetchBlobConst.FILE_PREFIX_BUNDLE_ASSET)) {
  1003. try {
  1004. RNFetchBlob.RCTContext.getAssets().open(path.replace(com.RNFetchBlob.RNFetchBlobConst.FILE_PREFIX_BUNDLE_ASSET, ""));
  1005. } catch (IOException e) {
  1006. return false;
  1007. }
  1008. return true;
  1009. }
  1010. else {
  1011. return new File(path).exists();
  1012. }
  1013. }
  1014. static boolean isAsset(String path) {
  1015. return path != null && path.startsWith(RNFetchBlobConst.FILE_PREFIX_BUNDLE_ASSET);
  1016. }
  1017. /**
  1018. * Normalize the path, remove URI scheme (xxx://) so that we can handle it.
  1019. * @param path URI string.
  1020. * @return Normalized string
  1021. */
  1022. static String normalizePath(String path) {
  1023. if(path == null)
  1024. return null;
  1025. if(!path.matches("\\w+\\:.*"))
  1026. return path;
  1027. if(path.startsWith("file://")) {
  1028. return path.replace("file://", "");
  1029. }
  1030. Uri uri = Uri.parse(path);
  1031. if(path.startsWith(RNFetchBlobConst.FILE_PREFIX_BUNDLE_ASSET)) {
  1032. return path;
  1033. }
  1034. else
  1035. return PathResolver.getRealPathFromURI(RNFetchBlob.RCTContext, uri);
  1036. }
  1037. }