package com.RNFetchBlob; import android.support.annotation.NonNull; import android.util.Base64; import com.facebook.react.bridge.Arguments; import com.facebook.react.bridge.ReactApplicationContext; import com.facebook.react.bridge.ReadableArray; import com.facebook.react.bridge.ReadableMap; import com.facebook.react.bridge.WritableMap; import com.facebook.react.modules.core.DeviceEventManagerModule; import java.io.ByteArrayInputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; import okhttp3.MediaType; import okhttp3.RequestBody; import okio.BufferedSink; class RNFetchBlobBody extends RequestBody{ private InputStream requestStream; private long contentLength = 0; private ReadableArray form; private String mTaskId; private String rawBody; private RNFetchBlobReq.RequestType requestType; private MediaType mime; private File bodyCache; int reported = 0; private Boolean chunkedEncoding = false; RNFetchBlobBody(String taskId) { this.mTaskId = taskId; } RNFetchBlobBody chunkedEncoding(boolean val) { this.chunkedEncoding = val; return this; } RNFetchBlobBody setMIME(MediaType mime) { this.mime = mime; return this; } RNFetchBlobBody setRequestType(RNFetchBlobReq.RequestType type) { this.requestType = type; return this; } /** * Set request body * @param body A string represents the request body * @return object itself */ RNFetchBlobBody setBody(String body) { this.rawBody = body; if(rawBody == null) { this.rawBody = ""; requestType = RNFetchBlobReq.RequestType.AsIs; } try { switch (requestType) { case SingleFile: requestStream = getReuqestStream(); contentLength = requestStream.available(); break; case AsIs: contentLength = this.rawBody.getBytes().length; requestStream = new ByteArrayInputStream(this.rawBody.getBytes()); break; case Others: break; } } catch(Exception ex) { ex.printStackTrace(); RNFetchBlobUtils.emitWarningEvent("RNFetchBlob failed to create single content request body :" + ex.getLocalizedMessage() + "\r\n"); } return this; } /** * Set request body (Array) * @param body A Readable array contains form data * @return object itself */ RNFetchBlobBody setBody(ReadableArray body) { this.form = body; try { bodyCache = createMultipartBodyCache(); requestStream = new FileInputStream(bodyCache); contentLength = bodyCache.length(); } catch(Exception ex) { ex.printStackTrace(); RNFetchBlobUtils.emitWarningEvent("RNFetchBlob failed to create request multipart body :" + ex.getLocalizedMessage()); } return this; } @Override public long contentLength() { return chunkedEncoding ? -1 : contentLength; } @Override public MediaType contentType() { return mime; } @Override public void writeTo(@NonNull BufferedSink sink) { try { pipeStreamToSink(requestStream, sink); } catch(Exception ex) { RNFetchBlobUtils.emitWarningEvent(ex.getLocalizedMessage()); ex.printStackTrace(); } } boolean clearRequestBody() { try { if (bodyCache != null && bodyCache.exists()) { bodyCache.delete(); } } catch(Exception e) { RNFetchBlobUtils.emitWarningEvent(e.getLocalizedMessage()); return false; } return true; } private InputStream getReuqestStream() throws Exception { // upload from storage if (rawBody.startsWith(RNFetchBlobConst.FILE_PREFIX)) { String orgPath = rawBody.substring(RNFetchBlobConst.FILE_PREFIX.length()); orgPath = RNFetchBlobFS.normalizePath(orgPath); // upload file from assets if (RNFetchBlobFS.isAsset(orgPath)) { try { String assetName = orgPath.replace(RNFetchBlobConst.FILE_PREFIX_BUNDLE_ASSET, ""); return RNFetchBlob.RCTContext.getAssets().open(assetName); } catch (Exception e) { throw new Exception("error when getting request stream from asset : " +e.getLocalizedMessage()); } } else { File f = new File(RNFetchBlobFS.normalizePath(orgPath)); try { if(!f.exists()) f.createNewFile(); return new FileInputStream(f); } catch (Exception e) { throw new Exception("error when getting request stream: " +e.getLocalizedMessage()); } } } // base 64 encoded else { try { byte[] bytes = Base64.decode(rawBody, 0); return new ByteArrayInputStream(bytes); } catch(Exception ex) { throw new Exception("error when getting request stream: " + ex.getLocalizedMessage()); } } } /** * Create a temp file that contains content of multipart form data content * @return The cache file object * @throws IOException */ private File createMultipartBodyCache() throws IOException { String boundary = "RNFetchBlob-" + mTaskId; File outputDir = RNFetchBlob.RCTContext.getCacheDir(); // context being the Activity pointer File outputFile = File.createTempFile("rnfb-form-tmp", "", outputDir); FileOutputStream os = new FileOutputStream(outputFile); ArrayList fields = countFormDataLength(); ReactApplicationContext ctx = RNFetchBlob.RCTContext; for(FormField field : fields) { String data = field.data; String name = field.name; // skip invalid fields if(name == null || data == null) continue; // form begin String header = "--" + boundary + "\r\n"; if (field.filename != null) { header += "Content-Disposition: form-data; name=\"" + name + "\"; filename=\"" + field.filename + "\"\r\n"; header += "Content-Type: " + field.mime + "\r\n\r\n"; os.write(header.getBytes()); // file field header end // upload from storage if (data.startsWith(RNFetchBlobConst.FILE_PREFIX)) { String orgPath = data.substring(RNFetchBlobConst.FILE_PREFIX.length()); orgPath = RNFetchBlobFS.normalizePath(orgPath); // path starts with content:// if (RNFetchBlobFS.isAsset(orgPath)) { try { String assetName = orgPath.replace(RNFetchBlobConst.FILE_PREFIX_BUNDLE_ASSET, ""); InputStream in = ctx.getAssets().open(assetName); pipeStreamToFileStream(in, os); } catch (IOException e) { RNFetchBlobUtils.emitWarningEvent("Failed to create form data asset :" + orgPath + ", " + e.getLocalizedMessage() ); } } // data from normal files else { File file = new File(RNFetchBlobFS.normalizePath(orgPath)); if(file.exists()) { FileInputStream fs = new FileInputStream(file); pipeStreamToFileStream(fs, os); } else { RNFetchBlobUtils.emitWarningEvent("Failed to create form data from path :" + orgPath + ", file not exists."); } } } // base64 embedded file content else { byte[] b = Base64.decode(data, 0); os.write(b); } } // data field else { header += "Content-Disposition: form-data; name=\"" + name + "\"\r\n"; header += "Content-Type: " + field.mime + "\r\n\r\n"; os.write(header.getBytes()); byte[] fieldData = field.data.getBytes(); os.write(fieldData); } // form end os.write("\r\n".getBytes()); } // close the form byte[] end = ("--" + boundary + "--\r\n").getBytes(); os.write(end); os.flush(); os.close(); return outputFile; } /** * Pipe input stream to request body output stream * @param stream The input stream * @param sink The request body buffer sink * @throws IOException */ private void pipeStreamToSink(InputStream stream, BufferedSink sink) throws IOException { byte[] chunk = new byte[10240]; int totalWritten = 0; int read; while((read = stream.read(chunk, 0, 10240)) > 0) { sink.write(chunk, 0, read); totalWritten += read; emitUploadProgress(totalWritten); } stream.close(); } /** * Pipe input stream to a file * @param is The input stream * @param os The output stream to a file * @throws IOException */ private void pipeStreamToFileStream(InputStream is, FileOutputStream os) throws IOException { byte[] buf = new byte[10240]; int len; while ((len = is.read(buf)) > 0) { os.write(buf, 0, len); } is.close(); } /** * Compute approximate content length for form data * @return ArrayList */ private ArrayList countFormDataLength() { long total = 0; ArrayList list = new ArrayList<>(); ReactApplicationContext ctx = RNFetchBlob.RCTContext; for(int i = 0;i < form.size(); i++) { FormField field = new FormField(form.getMap(i)); list.add(field); if(field.data == null) { RNFetchBlobUtils.emitWarningEvent("RNFetchBlob multipart request builder has found a field without `data` property, the field `"+ field.name +"` will be removed implicitly."); } else if (field.filename != null) { String data = field.data; // upload from storage if (data.startsWith(RNFetchBlobConst.FILE_PREFIX)) { String orgPath = data.substring(RNFetchBlobConst.FILE_PREFIX.length()); orgPath = RNFetchBlobFS.normalizePath(orgPath); // path starts with asset:// if (RNFetchBlobFS.isAsset(orgPath)) { try { String assetName = orgPath.replace(RNFetchBlobConst.FILE_PREFIX_BUNDLE_ASSET, ""); long length = ctx.getAssets().open(assetName).available(); total += length; } catch (IOException e) { RNFetchBlobUtils.emitWarningEvent(e.getLocalizedMessage()); } } // general files else { File file = new File(RNFetchBlobFS.normalizePath(orgPath)); total += file.length(); } } // base64 embedded file content else { byte[] bytes = Base64.decode(data, 0); total += bytes.length; } } // data field else { total += field.data.getBytes().length; } } contentLength = total; return list; } /** * Since ReadableMap could only be access once, we have to store the field into a map for * repeatedly access. */ private class FormField { public String name; String filename; String mime; public String data; FormField(ReadableMap rawData) { if(rawData.hasKey("name")) name = rawData.getString("name"); if(rawData.hasKey("filename")) filename = rawData.getString("filename"); if(rawData.hasKey("type")) mime = rawData.getString("type"); else { mime = filename == null ? "text/plain" : "application/octet-stream"; } if(rawData.hasKey("data")) { data = rawData.getString("data"); } } } /** * Emit progress event * @param written Integer */ private void emitUploadProgress(int written) { RNFetchBlobProgressConfig config = RNFetchBlobReq.getReportUploadProgress(mTaskId); if(config != null && contentLength != 0 && config.shouldReport((float)written/contentLength)) { WritableMap args = Arguments.createMap(); args.putString("taskId", mTaskId); args.putString("written", String.valueOf(written)); args.putString("total", String.valueOf(contentLength)); // emit event to js context RNFetchBlob.RCTContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class) .emit(RNFetchBlobConst.EVENT_UPLOAD_PROGRESS, args); } } }