Skip to content

Commit 063ce1b

Browse files
committed
Merge pull request #53 from ParsePlatform/wangmengyan.t8120706_download_ParseFile_content_to_file_instead_of_memory
Download ParseFile content to file instead of memory
2 parents 20a6818 + dd591a9 commit 063ce1b

File tree

5 files changed

+85
-44
lines changed

5 files changed

+85
-44
lines changed

Parse/src/main/java/com/parse/ParseAWSRequest.java

Lines changed: 34 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -8,24 +8,29 @@
88
*/
99
package com.parse;
1010

11-
import java.io.ByteArrayOutputStream;
12-
import java.io.IOException;
11+
import java.io.File;
12+
import java.io.FileOutputStream;
1313
import java.io.InputStream;
14+
import java.util.concurrent.Callable;
1415

1516
import bolts.Task;
1617

1718
/**
1819
* Request returns a byte array of the response and provides a callback the progress of the data
1920
* read from the network.
2021
*/
21-
/** package */ class ParseAWSRequest extends ParseRequest<byte[]> {
22+
/** package */ class ParseAWSRequest extends ParseRequest<Void> {
2223

23-
public ParseAWSRequest(ParseHttpRequest.Method method, String url) {
24+
// The temp file is used to save the ParseFile content when we fetch it from server
25+
private final File tempFile;
26+
27+
public ParseAWSRequest(ParseHttpRequest.Method method, String url, File tempFile) {
2428
super(method, url);
29+
this.tempFile = tempFile;
2530
}
2631

2732
@Override
28-
protected Task<byte[]> onResponseAsync(ParseHttpResponse response,
33+
protected Task<Void> onResponseAsync(final ParseHttpResponse response,
2934
final ProgressCallback downloadProgressCallback) {
3035
int statusCode = response.getStatusCode();
3136
if (statusCode >= 200 && statusCode < 300 || statusCode == 304) {
@@ -40,29 +45,33 @@ protected Task<byte[]> onResponseAsync(ParseHttpResponse response,
4045
return null;
4146
}
4247

43-
long totalSize = response.getTotalSize();
44-
int downloadedSize = 0;
45-
InputStream responseStream = null;
46-
try {
47-
responseStream = response.getContent();
48-
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
48+
return Task.call(new Callable<Void>() {
49+
@Override
50+
public Void call() throws Exception {
51+
long totalSize = response.getTotalSize();
52+
long downloadedSize = 0;
53+
InputStream responseStream = null;
54+
try {
55+
responseStream = response.getContent();
56+
FileOutputStream tempFileStream = new FileOutputStream(tempFile);
4957

50-
int nRead;
51-
byte[] data = new byte[32 << 10]; // 32KB
58+
int nRead;
59+
byte[] data = new byte[32 << 10]; // 32KB
5260

53-
while ((nRead = responseStream.read(data, 0, data.length)) != -1) {
54-
buffer.write(data, 0, nRead);
55-
downloadedSize += nRead;
56-
if (downloadProgressCallback != null && totalSize != -1) {
57-
int progressToReport = Math.round((float) downloadedSize / (float) totalSize * 100.0f);
58-
downloadProgressCallback.done(progressToReport);
61+
while ((nRead = responseStream.read(data, 0, data.length)) != -1) {
62+
tempFileStream.write(data, 0, nRead);
63+
downloadedSize += nRead;
64+
if (downloadProgressCallback != null && totalSize != -1) {
65+
int progressToReport =
66+
Math.round((float) downloadedSize / (float) totalSize * 100.0f);
67+
downloadProgressCallback.done(progressToReport);
68+
}
69+
}
70+
return null;
71+
} finally {
72+
ParseIOUtils.closeQuietly(responseStream);
5973
}
6074
}
61-
return Task.forResult(buffer.toByteArray());
62-
} catch (IOException e) {
63-
return Task.forError(e);
64-
} finally {
65-
ParseIOUtils.closeQuietly(responseStream);
66-
}
75+
}, ParseExecutors.io());
6776
}
6877
}

Parse/src/main/java/com/parse/ParseFileController.java

Lines changed: 30 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,13 @@ public File getCacheFile(ParseFile.State state) {
5656
return new File(cachePath, state.name());
5757
}
5858

59+
/* package for tests */ File getTempFile(ParseFile.State state) {
60+
if (state.url() == null) {
61+
return null;
62+
}
63+
return new File(cachePath, state.url() + ".tmp");
64+
}
65+
5966
public boolean isDataAvailable(ParseFile.State state) {
6067
return getCacheFile(state).exists();
6168
}
@@ -172,46 +179,55 @@ public Task<File> fetchAsync(
172179
if (cancellationToken != null && cancellationToken.isCancelled()) {
173180
return Task.cancelled();
174181
}
175-
final File file = getCacheFile(state);
182+
final File cacheFile = getCacheFile(state);
176183
return Task.call(new Callable<Boolean>() {
177184
@Override
178185
public Boolean call() throws Exception {
179-
return file.exists();
186+
return cacheFile.exists();
180187
}
181-
}, Task.BACKGROUND_EXECUTOR).continueWithTask(new Continuation<Boolean, Task<File>>() {
188+
}, ParseExecutors.io()).continueWithTask(new Continuation<Boolean, Task<File>>() {
182189
@Override
183190
public Task<File> then(Task<Boolean> task) throws Exception {
184191
boolean result = task.getResult();
185192
if (result) {
186-
return Task.forResult(file);
193+
return Task.forResult(cacheFile);
187194
}
188195
if (cancellationToken != null && cancellationToken.isCancelled()) {
189196
return Task.cancelled();
190197
}
191198

199+
// Generate the temp file path for caching ParseFile content based on ParseFile's url
200+
// The reason we do not write to the cacheFile directly is because there is no way we can
201+
// verify if a cacheFile is complete or not. If download is interrupted in the middle, next
202+
// time when we download the ParseFile, since cacheFile has already existed, we will return
203+
// this incomplete cacheFile
204+
final File tempFile = getTempFile(state);
205+
192206
// network
193-
final ParseAWSRequest request = new ParseAWSRequest(ParseHttpRequest.Method.GET, state.url());
207+
final ParseAWSRequest request =
208+
new ParseAWSRequest(ParseHttpRequest.Method.GET, state.url(), tempFile);
194209

195-
// TODO(grantland): Stream response directly to file t5042019
210+
// We do not need to delete the temp file since we always try to overwrite it
196211
return request.executeAsync(
197212
awsClient(),
198213
null,
199214
downloadProgressCallback,
200-
cancellationToken).onSuccess(new Continuation<byte[], File>() {
215+
cancellationToken).continueWithTask(new Continuation<Void, Task<File>>() {
201216
@Override
202-
public File then(Task<byte[]> task) throws Exception {
217+
public Task<File> then(Task<Void> task) throws Exception {
203218
// If the top-level task was cancelled, don't actually set the data -- just move on.
204219
if (cancellationToken != null && cancellationToken.isCancelled()) {
205220
throw new CancellationException();
206221
}
207-
208-
byte[] data = task.getResult();
209-
if (data != null) {
210-
ParseFileUtils.writeByteArrayToFile(file, data);
222+
if (task.isFaulted()) {
223+
ParseFileUtils.deleteQuietly(tempFile);
224+
return task.cast();
211225
}
212-
return file;
226+
227+
ParseFileUtils.moveFile(tempFile, cacheFile);
228+
return Task.forResult(cacheFile);
213229
}
214-
});
230+
}, ParseExecutors.io());
215231
}
216232
});
217233
}

Parse/src/test/java/com/parse/ParseAWSRequestTest.java

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,10 @@
1212

1313
import org.junit.Rule;
1414
import org.junit.rules.ExpectedException;
15+
import org.junit.rules.TemporaryFolder;
1516

1617
import java.io.ByteArrayInputStream;
18+
import java.io.File;
1719
import java.io.InputStream;
1820

1921
import bolts.Task;
@@ -44,8 +46,9 @@ public void test4XXThrowsException() throws Exception {
4446
ParseHttpClient mockHttpClient = mock(ParseHttpClient.class);
4547
when(mockHttpClient.execute(any(ParseHttpRequest.class))).thenReturn(mockResponse);
4648

47-
ParseAWSRequest request = new ParseAWSRequest(ParseHttpRequest.Method.GET, "http://parse.com");
48-
Task<byte[]> task = request.executeAsync(mockHttpClient);
49+
ParseAWSRequest request =
50+
new ParseAWSRequest(ParseHttpRequest.Method.GET, "http://parse.com", null);
51+
Task<Void> task = request.executeAsync(mockHttpClient);
4952
task.waitForCompletion();
5053

5154
assertTrue(task.isFaulted());

Parse/src/test/java/com/parse/ParseFileControllerTest.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -302,13 +302,15 @@ public void testFetchAsyncSuccess() throws Exception {
302302

303303
ParseFile.State state = new ParseFile.State.Builder()
304304
.name("file_name")
305+
.url("url")
305306
.build();
306307
Task<File> task = controller.fetchAsync(state, null, null, null);
307308
File result = ParseTaskUtils.wait(task);
308309

309310
verify(awsClient, times(1)).execute(any(ParseHttpRequest.class));
310311
assertTrue(result.exists());
311312
assertEquals("hello", ParseFileUtils.readFileToString(result, "UTF-8"));
313+
assertFalse(controller.getTempFile(state).exists());
312314
}
313315

314316
@Test
@@ -322,7 +324,9 @@ public void testFetchAsyncFailure() throws Exception {
322324
File root = temporaryFolder.getRoot();
323325
ParseFileController controller = new ParseFileController(null, root).awsClient(awsClient);
324326

327+
// We need to set url to make getTempFile() work and check it
325328
ParseFile.State state = new ParseFile.State.Builder()
329+
.url("test")
326330
.build();
327331
Task<File> task = controller.fetchAsync(state, null, null, null);
328332
task.waitForCompletion();
@@ -334,6 +338,7 @@ public void testFetchAsyncFailure() throws Exception {
334338
assertThat(error, instanceOf(ParseException.class));
335339
assertEquals(ParseException.CONNECTION_FAILED, ((ParseException) error).getCode());
336340
assertEquals(0, root.listFiles().length);
341+
assertFalse(controller.getTempFile(state).exists());
337342
}
338343

339344
//endregion

Parse/src/test/java/com/parse/ParseRequestTest.java

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,12 @@
1212
import org.junit.AfterClass;
1313
import org.junit.Before;
1414
import org.junit.BeforeClass;
15+
import org.junit.Rule;
1516
import org.junit.Test;
17+
import org.junit.rules.TemporaryFolder;
1618

1719
import java.io.ByteArrayInputStream;
20+
import java.io.File;
1821
import java.io.IOException;
1922
import java.util.LinkedList;
2023
import java.util.List;
@@ -34,6 +37,9 @@ public class ParseRequestTest {
3437

3538
private static byte[] data;
3639

40+
@Rule
41+
public TemporaryFolder temporaryFolder = new TemporaryFolder();
42+
3743
@BeforeClass
3844
public static void setUpClass() {
3945
char[] chars = new char[64 << 10]; // 64KB
@@ -78,13 +84,15 @@ public void testDownloadProgress() throws Exception {
7884
ParseHttpClient mockHttpClient = mock(ParseHttpClient.class);
7985
when(mockHttpClient.execute(any(ParseHttpRequest.class))).thenReturn(mockResponse);
8086

81-
ParseAWSRequest request = new ParseAWSRequest(ParseHttpRequest.Method.GET, "localhost");
87+
File tempFile = temporaryFolder.newFile("test");
88+
ParseAWSRequest request =
89+
new ParseAWSRequest(ParseHttpRequest.Method.GET, "localhost", tempFile);
8290
TestProgressCallback downloadProgressCallback = new TestProgressCallback();
83-
Task<byte[]> task = request.executeAsync(mockHttpClient, null, downloadProgressCallback);
91+
Task<Void> task = request.executeAsync(mockHttpClient, null, downloadProgressCallback);
8492

8593
task.waitForCompletion();
8694
assertFalse("Download failed: " + task.getError(), task.isFaulted());
87-
assertEquals(data.length, task.getResult().length);
95+
assertEquals(data.length, ParseFileUtils.readFileToByteArray(tempFile).length);
8896

8997
assertProgressCompletedSuccessfully(downloadProgressCallback);
9098
}

0 commit comments

Comments
 (0)