Skip to content
This repository was archived by the owner on Mar 16, 2019. It is now read-only.

Commit 0e335ab

Browse files
authored
Merge pull request #202 from pkwak-sf/long-multipart-upload
Long multipart upload
2 parents 63bcc8d + cc662f4 commit 0e335ab

File tree

6 files changed

+312
-3
lines changed

6 files changed

+312
-3
lines changed

README.md

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -381,6 +381,48 @@ What if you want to append a file to form data ? Just like [upload a file from s
381381
})
382382
```
383383

384+
When you append files to form-data and you want to continue uploading while the app is in background on Android, you can use IntentService to upload by adding `multipartFileUpload` flag to config.
385+
386+
```js
387+
388+
RNFetchBlob.config({
389+
multipartFileUpload: true,
390+
})
391+
.fetch('POST', 'http://www.example.com/upload-form', {
392+
Authorization : "Bearer access-token",
393+
otherHeader : "foo",
394+
// this is required, otherwise it won't be process as a multipart/form-data request
395+
'Content-Type' : 'multipart/form-data',
396+
}, [
397+
// append field data from file path
398+
{
399+
name : 'avatar',
400+
filename : 'avatar.png',
401+
// Change BASE64 encoded data to a file path with prefix `RNFetchBlob-file://`.
402+
// Or simply wrap the file path with RNFetchBlob.wrap().
403+
data: RNFetchBlob.wrap(PATH_TO_THE_FILE)
404+
},
405+
{
406+
name : 'ringtone',
407+
filename : 'ring.mp3',
408+
// use custom MIME type
409+
type : 'application/mp3',
410+
// upload a file from asset is also possible in version >= 0.6.2
411+
data : RNFetchBlob.wrap(RNFetchBlob.fs.asset('default-ringtone.mp3'))
412+
}
413+
// elements without property `filename` will be sent as plain text
414+
{ name : 'name', data : 'user'},
415+
{ name : 'info', data : JSON.stringify({
416+
417+
tel : '12345678'
418+
})},
419+
]).then((resp) => {
420+
// ...
421+
}).catch((err) => {
422+
// ...
423+
})
424+
```
425+
384426
### Upload/Download progress
385427

386428
In `version >= 0.4.2` it is possible to know the upload/download progress. After `0.7.0` IOS and Android upload progress are also supported.

src/android/src/main/AndroidManifest.xml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@
66
android:label="@string/app_name"
77
android:supportsRtl="true">
88

9+
<service
10+
android:name=".RNFetchBlobService"
11+
>
12+
</service>
913
</application>
1014

1115
</manifest>

src/android/src/main/java/com/RNFetchBlob/RNFetchBlobConfig.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ public class RNFetchBlobConfig {
2020
public long timeout = 60000;
2121
public Boolean increment = false;
2222
public ReadableArray binaryContentTypes = null;
23+
public boolean multipartFileUpload;
2324

2425
RNFetchBlobConfig(ReadableMap options) {
2526
if(options == null)
@@ -46,6 +47,7 @@ public class RNFetchBlobConfig {
4647
if(options.hasKey("timeout")) {
4748
this.timeout = options.getInt("timeout");
4849
}
50+
this.multipartFileUpload = options.hasKey("multipartFileUpload") ? options.getBoolean("multipartFileUpload") : false;
4951
}
5052

5153
}

src/android/src/main/java/com/RNFetchBlob/RNFetchBlobReq.java

Lines changed: 67 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import android.content.IntentFilter;
88
import android.database.Cursor;
99
import android.net.Uri;
10+
import android.os.Bundle;
1011
import android.util.Base64;
1112

1213
import com.RNFetchBlob.Response.RNFetchBlobDefaultResp;
@@ -18,6 +19,7 @@
1819
import com.facebook.react.bridge.ReadableArray;
1920
import com.facebook.react.bridge.ReadableMap;
2021
import com.facebook.react.bridge.ReadableMapKeySetIterator;
22+
import com.facebook.react.bridge.ReadableNativeArray;
2123
import com.facebook.react.bridge.WritableArray;
2224
import com.facebook.react.bridge.WritableMap;
2325
import com.facebook.react.modules.core.DeviceEventManagerModule;
@@ -165,6 +167,39 @@ public void run() {
165167

166168
}
167169

170+
if(this.options.multipartFileUpload) {
171+
HashMap<String, String> mheaders = new HashMap<>();
172+
// set headers
173+
if (headers != null) {
174+
ReadableMapKeySetIterator it = headers.keySetIterator();
175+
while (it.hasNextKey()) {
176+
String key = it.nextKey();
177+
String value = headers.getString(key);
178+
mheaders.put(key, value);
179+
}
180+
}
181+
182+
Bundle bundle = new Bundle();
183+
bundle.putString("taskId", taskId);
184+
bundle.putString("url", url);
185+
bundle.putSerializable("mheaders", mheaders);
186+
bundle.putSerializable("requestBodyArray", ((ReadableNativeArray)rawRequestBodyArray).toArrayList());
187+
188+
Context appCtx = RNFetchBlob.RCTContext.getApplicationContext();
189+
190+
IntentFilter filter = new IntentFilter(RNFetchBlobService.RNFetchBlobServiceBroadcast);
191+
filter.addCategory(RNFetchBlobService.CategoryProgress);
192+
filter.addCategory(RNFetchBlobService.CategorySuccess);
193+
filter.addCategory(RNFetchBlobService.CategoryFail);
194+
appCtx.registerReceiver(this, filter);
195+
196+
Intent intent = new Intent(appCtx, RNFetchBlobService.class);
197+
intent.putExtras(bundle);
198+
appCtx.startService(intent);
199+
200+
return;
201+
}
202+
168203
// find cached result if `key` property exists
169204
String cacheKey = this.taskId;
170205
String ext = this.options.appendExt.isEmpty() ? "" : "." + this.options.appendExt;
@@ -662,8 +697,38 @@ public void onReceive(Context context, Intent intent) {
662697
}
663698

664699
}
700+
} else if (RNFetchBlobService.RNFetchBlobServiceBroadcast.equals(action)) {
701+
String _taskId = intent.getStringExtra(RNFetchBlobService.BroadcastTaskId);
702+
if (this.taskId.equals(_taskId)) {
703+
if (intent.hasCategory(RNFetchBlobService.CategoryProgress)) {
704+
HashMap map = (HashMap) intent.getSerializableExtra(RNFetchBlobService.BroadcastProgressMap);
705+
706+
WritableMap args = Arguments.createMap();
707+
args.putString("taskId", _taskId);
708+
args.putString("written", String.valueOf(map.get(RNFetchBlobService.KeyWritten)));
709+
args.putString("total", String.valueOf(map.get(RNFetchBlobService.KeyTotal)));
710+
711+
// emit event to js context
712+
RNFetchBlob.RCTContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
713+
.emit(RNFetchBlobConst.EVENT_UPLOAD_PROGRESS, args);
714+
} else if (intent.hasCategory(RNFetchBlobService.CategorySuccess)) {
715+
// Could be fail.
716+
try {
717+
byte[] bytes = intent.getByteArrayExtra(RNFetchBlobService.BroadcastMsg);
718+
callback.invoke(null, RNFetchBlobConst.RNFB_RESPONSE_UTF8, new String(bytes, "UTF-8"));
719+
} catch (IOException e) {
720+
callback.invoke("RNFetchBlob failed to encode response data to UTF8 string.", null);
721+
} finally {
722+
// lets unregister.
723+
Context appCtx = RNFetchBlob.RCTContext.getApplicationContext();
724+
appCtx.unregisterReceiver(this);
725+
}
726+
} else if (intent.hasCategory(RNFetchBlobService.CategoryFail)) {
727+
callback.invoke("Request failed.", null, null);
728+
Context appCtx = RNFetchBlob.RCTContext.getApplicationContext();
729+
appCtx.unregisterReceiver(this);
730+
}
731+
}
665732
}
666733
}
667-
668-
669734
}
Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
package com.RNFetchBlob;
2+
3+
import android.app.IntentService;
4+
import android.content.Intent;
5+
import android.net.Uri;
6+
import android.os.Bundle;
7+
import android.text.TextUtils;
8+
9+
import com.facebook.react.modules.network.*;
10+
11+
import java.io.File;
12+
import java.io.IOException;
13+
import java.net.MalformedURLException;
14+
import java.net.URL;
15+
import java.util.ArrayList;
16+
import java.util.HashMap;
17+
import java.util.concurrent.TimeUnit;
18+
19+
import okhttp3.Call;
20+
import okhttp3.Headers;
21+
import okhttp3.MediaType;
22+
import okhttp3.MultipartBody;
23+
import okhttp3.OkHttpClient;
24+
import okhttp3.Request;
25+
import okhttp3.RequestBody;
26+
import okhttp3.Response;
27+
28+
/**
29+
* Created by pkwak on 11/15/16.
30+
* IntentService for POSTing multi-form data. Designed for long-uploads with file
31+
* (but can use data body).
32+
*/
33+
34+
public class RNFetchBlobService extends IntentService implements ProgressListener {
35+
public static String RNFetchBlobServiceBroadcast = "RNFetchBlobServiceBroadcast";
36+
37+
public static String CategorySuccess = "CategorySuccess";
38+
public static String CategoryFail = "CategoryFail";
39+
public static String CategoryProgress = "CategoryProgress";
40+
41+
public static String BroadcastMsg = "BroadcastMsg";
42+
public static String BroadcastProgressMap = "BroadcastProgressMap";
43+
public static String BroadcastTaskId = "BroadcastTaskId";
44+
45+
public static String KeyWritten = "KeyWritten";
46+
public static String KeyTotal = "KeyTotal";
47+
48+
/**
49+
* Creates an IntentService. Invoked by your subclass's constructor.
50+
*/
51+
public RNFetchBlobService() {
52+
super("RNFetchBlobService");
53+
}
54+
55+
private String _taskId = null;
56+
@Override
57+
protected void onHandleIntent(final Intent intent) {
58+
59+
Bundle bundle = intent.getExtras();
60+
_taskId = bundle.getString("taskId");
61+
String url = bundle.getString("url");
62+
HashMap<String, String> mheaders = (HashMap<String, String>)bundle.getSerializable("mheaders");
63+
ArrayList<Object> requestBodyArray = (ArrayList<Object>)bundle.getSerializable("requestBodyArray");
64+
65+
MultipartBody.Builder requestBuilder = new MultipartBody.Builder()
66+
.setType(MultipartBody.FORM);
67+
for(Object bodyPart : requestBodyArray) {
68+
if (bodyPart instanceof HashMap) {
69+
HashMap<String, String> bodyMap = (HashMap<String, String>)bodyPart;
70+
String name = bodyMap.get("name");
71+
String type = bodyMap.get("type");
72+
String filename = bodyMap.get("filename");
73+
String data = bodyMap.get("data");
74+
File file = null;
75+
MediaType mediaType = type != null
76+
? MediaType.parse(type)
77+
: filename == null
78+
? MediaType.parse("text/plain")
79+
: MediaType.parse("application/octet-stream");
80+
if(filename != null && data.startsWith("RNFetchBlob-")) {
81+
try {
82+
String normalizedUri = RNFetchBlobFS.normalizePath(data.replace(RNFetchBlobConst.FILE_PREFIX, ""));
83+
file = new File(String.valueOf(Uri.parse(normalizedUri)));
84+
} catch (Exception e) {
85+
file = null;
86+
}
87+
}
88+
89+
String contentDisposition = "form-data"
90+
+ (!TextUtils.isEmpty(name) ? "; name=" + name : "")
91+
+ (!TextUtils.isEmpty(filename) ? "; filename=" + filename : "");
92+
93+
requestBuilder.addPart(
94+
Headers.of("Content-Disposition", contentDisposition),
95+
file != null
96+
? RequestBody.create(mediaType, file)
97+
: RequestBody.create(mediaType, data)
98+
);
99+
}
100+
}
101+
RequestBody innerRequestBody = requestBuilder.build();
102+
103+
ProgressRequestBody requestBody = new ProgressRequestBody(innerRequestBody, this);
104+
105+
final Request.Builder builder = new Request.Builder();
106+
try {
107+
builder.url(new URL(url));
108+
} catch (MalformedURLException e) {
109+
Intent broadcastIntent = new Intent();
110+
broadcastIntent.setAction(RNFetchBlobServiceBroadcast);
111+
broadcastIntent.addCategory(CategoryFail);
112+
broadcastIntent.putExtra(BroadcastMsg, "Could not create URL : " + e.getMessage().getBytes());
113+
sendBroadcast(broadcastIntent);
114+
return;
115+
}
116+
117+
builder.post(requestBody);
118+
for(String key : mheaders.keySet()) {
119+
builder.addHeader(key, mheaders.get(key));
120+
}
121+
122+
OkHttpClient client = new OkHttpClient.Builder()
123+
.writeTimeout(24, TimeUnit.HOURS)
124+
.readTimeout(24, TimeUnit.HOURS)
125+
.build();
126+
127+
final Call call = client.newCall(builder.build());
128+
call.enqueue(new okhttp3.Callback() {
129+
130+
@Override
131+
public void onFailure(Call call, IOException e) {
132+
Intent broadcastIntent = new Intent();
133+
broadcastIntent.setAction(RNFetchBlobServiceBroadcast);
134+
broadcastIntent.addCategory(CategoryFail);
135+
broadcastIntent.putExtra(BroadcastMsg, e.getMessage().getBytes());
136+
broadcastIntent.putExtra(BroadcastTaskId, _taskId);
137+
sendBroadcast(broadcastIntent);
138+
call.cancel();
139+
}
140+
141+
@Override
142+
public void onResponse(Call call, Response response) throws IOException {
143+
// This is http-response success. Can be 2xx/4xx/5xx/etc.
144+
Intent broadcastIntent = new Intent();
145+
broadcastIntent.setAction(RNFetchBlobServiceBroadcast);
146+
broadcastIntent.addCategory(CategorySuccess);
147+
broadcastIntent.putExtra(BroadcastMsg, response.body().bytes());
148+
broadcastIntent.putExtra(BroadcastTaskId, _taskId);
149+
sendBroadcast(broadcastIntent);
150+
151+
response.body().close();
152+
153+
}
154+
155+
});
156+
}
157+
158+
private int _progressPercent = 0;
159+
private long _lastProgressTime = 0;
160+
@Override
161+
public void onProgress(long bytesWritten, long contentLength, boolean done) {
162+
163+
// no more than once per %
164+
int currentPercent = (int)((bytesWritten * 100) / contentLength);
165+
if (currentPercent <= _progressPercent) {
166+
return;
167+
}
168+
_progressPercent = currentPercent;
169+
170+
// no more than twice a second.
171+
long now = System.currentTimeMillis();
172+
if (_lastProgressTime + 500 > now) {
173+
return;
174+
}
175+
_lastProgressTime = now;
176+
177+
Intent broadcastIntent = new Intent();
178+
broadcastIntent.setAction(RNFetchBlobServiceBroadcast);
179+
broadcastIntent.addCategory(CategoryProgress);
180+
broadcastIntent.putExtra(BroadcastTaskId, _taskId);
181+
HashMap map = new HashMap();
182+
map.put(KeyWritten, Long.valueOf(bytesWritten));
183+
map.put(KeyTotal, Long.valueOf(contentLength));
184+
broadcastIntent.putExtra(BroadcastProgressMap, map);
185+
sendBroadcast(broadcastIntent);
186+
}
187+
}

src/ios/RNFetchBlobNetwork.m

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,9 @@ - (void) sendRequest:(__weak NSDictionary * _Nullable )options
223223
{
224224
defaultConfigObject.timeoutIntervalForRequest = timeout/1000;
225225
}
226+
227+
defaultConfigObject.sessionSendsLaunchEvents = YES;
228+
226229
defaultConfigObject.HTTPMaximumConnectionsPerHost = 10;
227230
session = [NSURLSession sessionWithConfiguration:defaultConfigObject delegate:self delegateQueue:taskQueue];
228231
if(path != nil || [self.options valueForKey:CONFIG_USE_TEMP]!= nil)
@@ -254,7 +257,13 @@ - (void) sendRequest:(__weak NSDictionary * _Nullable )options
254257
respFile = NO;
255258
}
256259

257-
__block NSURLSessionDataTask * task = [session dataTaskWithRequest:req];
260+
__block NSURLSessionDataTask * task;
261+
if (path && req.HTTPMethod == @"POST") {
262+
task = [session uploadTaskWithRequest:req fromFile:path];
263+
} else {
264+
task = [session dataTaskWithRequest:req];
265+
}
266+
258267
[taskTable setObject:task forKey:taskId];
259268
[task resume];
260269

0 commit comments

Comments
 (0)