Ingen beskrivning

RNFetchBlobBody.java 16KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421
  1. package com.RNFetchBlob;
  2. import android.support.annotation.NonNull;
  3. import android.util.Base64;
  4. import com.facebook.react.bridge.Arguments;
  5. import com.facebook.react.bridge.ReactApplicationContext;
  6. import com.facebook.react.bridge.ReadableArray;
  7. import com.facebook.react.bridge.ReadableMap;
  8. import com.facebook.react.bridge.WritableMap;
  9. import com.facebook.react.modules.core.DeviceEventManagerModule;
  10. import java.io.ByteArrayInputStream;
  11. import java.io.File;
  12. import java.io.FileInputStream;
  13. import java.io.FileOutputStream;
  14. import java.io.IOException;
  15. import java.io.InputStream;
  16. import java.util.ArrayList;
  17. import android.net.Uri;
  18. import okhttp3.MediaType;
  19. import okhttp3.RequestBody;
  20. import okio.BufferedSink;
  21. class RNFetchBlobBody extends RequestBody{
  22. private InputStream requestStream;
  23. private long contentLength = 0;
  24. private ReadableArray form;
  25. private String mTaskId;
  26. private String rawBody;
  27. private RNFetchBlobReq.RequestType requestType;
  28. private MediaType mime;
  29. private File bodyCache;
  30. int reported = 0;
  31. private Boolean chunkedEncoding = false;
  32. RNFetchBlobBody(String taskId) {
  33. this.mTaskId = taskId;
  34. }
  35. RNFetchBlobBody chunkedEncoding(boolean val) {
  36. this.chunkedEncoding = val;
  37. return this;
  38. }
  39. RNFetchBlobBody setMIME(MediaType mime) {
  40. this.mime = mime;
  41. return this;
  42. }
  43. RNFetchBlobBody setRequestType(RNFetchBlobReq.RequestType type) {
  44. this.requestType = type;
  45. return this;
  46. }
  47. /**
  48. * Set request body
  49. * @param body A string represents the request body
  50. * @return object itself
  51. */
  52. RNFetchBlobBody setBody(String body) {
  53. this.rawBody = body;
  54. if(rawBody == null) {
  55. this.rawBody = "";
  56. requestType = RNFetchBlobReq.RequestType.AsIs;
  57. }
  58. try {
  59. switch (requestType) {
  60. case SingleFile:
  61. requestStream = getRequestStream();
  62. contentLength = requestStream.available();
  63. break;
  64. case AsIs:
  65. contentLength = this.rawBody.getBytes().length;
  66. requestStream = new ByteArrayInputStream(this.rawBody.getBytes());
  67. break;
  68. case Others:
  69. break;
  70. }
  71. } catch(Exception ex) {
  72. ex.printStackTrace();
  73. RNFetchBlobUtils.emitWarningEvent("RNFetchBlob failed to create single content request body :" + ex.getLocalizedMessage() + "\r\n");
  74. }
  75. return this;
  76. }
  77. /**
  78. * Set request body (Array)
  79. * @param body A Readable array contains form data
  80. * @return object itself
  81. */
  82. RNFetchBlobBody setBody(ReadableArray body) {
  83. this.form = body;
  84. try {
  85. bodyCache = createMultipartBodyCache();
  86. requestStream = new FileInputStream(bodyCache);
  87. contentLength = bodyCache.length();
  88. } catch(Exception ex) {
  89. ex.printStackTrace();
  90. RNFetchBlobUtils.emitWarningEvent("RNFetchBlob failed to create request multipart body :" + ex.getLocalizedMessage());
  91. }
  92. return this;
  93. }
  94. @Override
  95. public long contentLength() {
  96. return chunkedEncoding ? -1 : contentLength;
  97. }
  98. @Override
  99. public MediaType contentType() {
  100. return mime;
  101. }
  102. @Override
  103. public void writeTo(@NonNull BufferedSink sink) {
  104. try {
  105. pipeStreamToSink(requestStream, sink);
  106. } catch(Exception ex) {
  107. RNFetchBlobUtils.emitWarningEvent(ex.getLocalizedMessage());
  108. ex.printStackTrace();
  109. }
  110. }
  111. boolean clearRequestBody() {
  112. try {
  113. if (bodyCache != null && bodyCache.exists()) {
  114. bodyCache.delete();
  115. }
  116. } catch(Exception e) {
  117. RNFetchBlobUtils.emitWarningEvent(e.getLocalizedMessage());
  118. return false;
  119. }
  120. return true;
  121. }
  122. private InputStream getRequestStream() throws Exception {
  123. // upload from storage
  124. if (rawBody.startsWith(RNFetchBlobConst.FILE_PREFIX)) {
  125. String orgPath = rawBody.substring(RNFetchBlobConst.FILE_PREFIX.length());
  126. orgPath = RNFetchBlobFS.normalizePath(orgPath);
  127. // upload file from assets
  128. if (RNFetchBlobFS.isAsset(orgPath)) {
  129. try {
  130. String assetName = orgPath.replace(RNFetchBlobConst.FILE_PREFIX_BUNDLE_ASSET, "");
  131. return RNFetchBlob.RCTContext.getAssets().open(assetName);
  132. } catch (Exception e) {
  133. throw new Exception("error when getting request stream from asset : " +e.getLocalizedMessage());
  134. }
  135. } else {
  136. File f = new File(RNFetchBlobFS.normalizePath(orgPath));
  137. try {
  138. if(!f.exists())
  139. f.createNewFile();
  140. return new FileInputStream(f);
  141. } catch (Exception e) {
  142. throw new Exception("error when getting request stream: " +e.getLocalizedMessage());
  143. }
  144. }
  145. } else if (rawBody.startsWith(RNFetchBlobConst.CONTENT_PREFIX)) {
  146. String contentURI = rawBody.substring(RNFetchBlobConst.CONTENT_PREFIX.length());
  147. try {
  148. return RNFetchBlob.RCTContext.getContentResolver().openInputStream(Uri.parse(contentURI));
  149. } catch (Exception e) {
  150. throw new Exception("error when getting request stream for content URI: " + contentURI, e);
  151. }
  152. }
  153. // base 64 encoded
  154. else {
  155. try {
  156. byte[] bytes = Base64.decode(rawBody, 0);
  157. return new ByteArrayInputStream(bytes);
  158. } catch(Exception ex) {
  159. throw new Exception("error when getting request stream: " + ex.getLocalizedMessage());
  160. }
  161. }
  162. }
  163. /**
  164. * Create a temp file that contains content of multipart form data content
  165. * @return The cache file object
  166. * @throws IOException
  167. */
  168. private File createMultipartBodyCache() throws IOException {
  169. String boundary = "RNFetchBlob-" + mTaskId;
  170. File outputDir = RNFetchBlob.RCTContext.getCacheDir(); // context being the Activity pointer
  171. File outputFile = File.createTempFile("rnfb-form-tmp", "", outputDir);
  172. FileOutputStream os = new FileOutputStream(outputFile);
  173. ArrayList<FormField> fields = countFormDataLength();
  174. ReactApplicationContext ctx = RNFetchBlob.RCTContext;
  175. for(FormField field : fields) {
  176. String data = field.data;
  177. String name = field.name;
  178. // skip invalid fields
  179. if(name == null || data == null)
  180. continue;
  181. // form begin
  182. String header = "--" + boundary + "\r\n";
  183. if (field.filename != null) {
  184. header += "Content-Disposition: form-data; name=\"" + name + "\"; filename=\"" + field.filename + "\"\r\n";
  185. header += "Content-Type: " + field.mime + "\r\n\r\n";
  186. os.write(header.getBytes());
  187. // file field header end
  188. // upload from storage
  189. if (data.startsWith(RNFetchBlobConst.FILE_PREFIX)) {
  190. String orgPath = data.substring(RNFetchBlobConst.FILE_PREFIX.length());
  191. orgPath = RNFetchBlobFS.normalizePath(orgPath);
  192. // path starts with content://
  193. if (RNFetchBlobFS.isAsset(orgPath)) {
  194. try {
  195. String assetName = orgPath.replace(RNFetchBlobConst.FILE_PREFIX_BUNDLE_ASSET, "");
  196. InputStream in = ctx.getAssets().open(assetName);
  197. pipeStreamToFileStream(in, os);
  198. } catch (IOException e) {
  199. RNFetchBlobUtils.emitWarningEvent("Failed to create form data asset :" + orgPath + ", " + e.getLocalizedMessage() );
  200. }
  201. }
  202. // data from normal files
  203. else {
  204. File file = new File(RNFetchBlobFS.normalizePath(orgPath));
  205. if(file.exists()) {
  206. FileInputStream fs = new FileInputStream(file);
  207. pipeStreamToFileStream(fs, os);
  208. }
  209. else {
  210. RNFetchBlobUtils.emitWarningEvent("Failed to create form data from path :" + orgPath + ", file not exists.");
  211. }
  212. }
  213. } else if (data.startsWith(RNFetchBlobConst.CONTENT_PREFIX)) {
  214. String contentURI = data.substring(RNFetchBlobConst.CONTENT_PREFIX.length());
  215. InputStream is = null;
  216. try {
  217. is = ctx.getContentResolver().openInputStream(Uri.parse(contentURI));
  218. pipeStreamToFileStream(is, os);
  219. } catch(Exception e) {
  220. RNFetchBlobUtils.emitWarningEvent(
  221. "Failed to create form data from content URI:" + contentURI + ", " + e.getLocalizedMessage());
  222. } finally {
  223. if (is != null) {
  224. is.close();
  225. }
  226. }
  227. }
  228. // base64 embedded file content
  229. else {
  230. byte[] b = Base64.decode(data, 0);
  231. os.write(b);
  232. }
  233. }
  234. // data field
  235. else {
  236. header += "Content-Disposition: form-data; name=\"" + name + "\"\r\n";
  237. header += "Content-Type: " + field.mime + "\r\n\r\n";
  238. os.write(header.getBytes());
  239. byte[] fieldData = field.data.getBytes();
  240. os.write(fieldData);
  241. }
  242. // form end
  243. os.write("\r\n".getBytes());
  244. }
  245. // close the form
  246. byte[] end = ("--" + boundary + "--\r\n").getBytes();
  247. os.write(end);
  248. os.flush();
  249. os.close();
  250. return outputFile;
  251. }
  252. /**
  253. * Pipe input stream to request body output stream
  254. * @param stream The input stream
  255. * @param sink The request body buffer sink
  256. * @throws IOException
  257. */
  258. private void pipeStreamToSink(InputStream stream, BufferedSink sink) throws IOException {
  259. byte[] chunk = new byte[10240];
  260. int totalWritten = 0;
  261. int read;
  262. while((read = stream.read(chunk, 0, 10240)) > 0) {
  263. sink.write(chunk, 0, read);
  264. totalWritten += read;
  265. emitUploadProgress(totalWritten);
  266. }
  267. stream.close();
  268. }
  269. /**
  270. * Pipe input stream to a file
  271. * @param is The input stream
  272. * @param os The output stream to a file
  273. * @throws IOException
  274. */
  275. private void pipeStreamToFileStream(InputStream is, FileOutputStream os) throws IOException {
  276. byte[] buf = new byte[10240];
  277. int len;
  278. while ((len = is.read(buf)) > 0) {
  279. os.write(buf, 0, len);
  280. }
  281. is.close();
  282. }
  283. /**
  284. * Compute approximate content length for form data
  285. * @return ArrayList<FormField>
  286. */
  287. private ArrayList<FormField> countFormDataLength() throws IOException {
  288. long total = 0;
  289. ArrayList<FormField> list = new ArrayList<>();
  290. ReactApplicationContext ctx = RNFetchBlob.RCTContext;
  291. for(int i = 0;i < form.size(); i++) {
  292. FormField field = new FormField(form.getMap(i));
  293. list.add(field);
  294. if(field.data == null) {
  295. RNFetchBlobUtils.emitWarningEvent("RNFetchBlob multipart request builder has found a field without `data` property, the field `"+ field.name +"` will be removed implicitly.");
  296. }
  297. else if (field.filename != null) {
  298. String data = field.data;
  299. // upload from storage
  300. if (data.startsWith(RNFetchBlobConst.FILE_PREFIX)) {
  301. String orgPath = data.substring(RNFetchBlobConst.FILE_PREFIX.length());
  302. orgPath = RNFetchBlobFS.normalizePath(orgPath);
  303. // path starts with asset://
  304. if (RNFetchBlobFS.isAsset(orgPath)) {
  305. try {
  306. String assetName = orgPath.replace(RNFetchBlobConst.FILE_PREFIX_BUNDLE_ASSET, "");
  307. long length = ctx.getAssets().open(assetName).available();
  308. total += length;
  309. } catch (IOException e) {
  310. RNFetchBlobUtils.emitWarningEvent(e.getLocalizedMessage());
  311. }
  312. }
  313. // general files
  314. else {
  315. File file = new File(RNFetchBlobFS.normalizePath(orgPath));
  316. total += file.length();
  317. }
  318. } else if (data.startsWith(RNFetchBlobConst.CONTENT_PREFIX)) {
  319. String contentURI = data.substring(RNFetchBlobConst.CONTENT_PREFIX.length());
  320. InputStream is = null;
  321. try {
  322. is = ctx.getContentResolver().openInputStream(Uri.parse(contentURI));
  323. long length = is.available();
  324. total += length;
  325. } catch(Exception e) {
  326. RNFetchBlobUtils.emitWarningEvent(
  327. "Failed to estimate form data length from content URI:" + contentURI + ", " + e.getLocalizedMessage());
  328. } finally {
  329. if (is != null) {
  330. is.close();
  331. }
  332. }
  333. }
  334. // base64 embedded file content
  335. else {
  336. byte[] bytes = Base64.decode(data, 0);
  337. total += bytes.length;
  338. }
  339. }
  340. // data field
  341. else {
  342. total += field.data.getBytes().length;
  343. }
  344. }
  345. contentLength = total;
  346. return list;
  347. }
  348. /**
  349. * Since ReadableMap could only be access once, we have to store the field into a map for
  350. * repeatedly access.
  351. */
  352. private class FormField {
  353. public String name;
  354. String filename;
  355. String mime;
  356. public String data;
  357. FormField(ReadableMap rawData) {
  358. if(rawData.hasKey("name"))
  359. name = rawData.getString("name");
  360. if(rawData.hasKey("filename"))
  361. filename = rawData.getString("filename");
  362. if(rawData.hasKey("type"))
  363. mime = rawData.getString("type");
  364. else {
  365. mime = filename == null ? "text/plain" : "application/octet-stream";
  366. }
  367. if(rawData.hasKey("data")) {
  368. data = rawData.getString("data");
  369. }
  370. }
  371. }
  372. /**
  373. * Emit progress event
  374. * @param written Integer
  375. */
  376. private void emitUploadProgress(int written) {
  377. RNFetchBlobProgressConfig config = RNFetchBlobReq.getReportUploadProgress(mTaskId);
  378. if(config != null && contentLength != 0 && config.shouldReport((float)written/contentLength)) {
  379. WritableMap args = Arguments.createMap();
  380. args.putString("taskId", mTaskId);
  381. args.putString("written", String.valueOf(written));
  382. args.putString("total", String.valueOf(contentLength));
  383. // emit event to js context
  384. RNFetchBlob.RCTContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
  385. .emit(RNFetchBlobConst.EVENT_UPLOAD_PROGRESS, args);
  386. }
  387. }
  388. }