No Description

RNFetchBlobBody.java 14KB

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