No Description

RNFetchBlobBody.java 14KB

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