diff --git a/Parse/src/main/java/com/parse/ParseFileController.java b/Parse/src/main/java/com/parse/ParseFileController.java index 4d7f7d455..f8a5a5f23 100644 --- a/Parse/src/main/java/com/parse/ParseFileController.java +++ b/Parse/src/main/java/com/parse/ParseFileController.java @@ -114,7 +114,54 @@ public ParseFile.State then(Task task) throws Exception { return newState; } - }, Task.BACKGROUND_EXECUTOR); + }, ParseExecutors.io()); + } + + public Task saveAsync( + final ParseFile.State state, + final File file, + String sessionToken, + ProgressCallback uploadProgressCallback, + Task cancellationToken) { + if (state.url() != null) { // !isDirty + return Task.forResult(state); + } + if (cancellationToken != null && cancellationToken.isCancelled()) { + return Task.cancelled(); + } + + final ParseRESTCommand command = new ParseRESTFileCommand.Builder() + .fileName(state.name()) + .file(file) + .contentType(state.mimeType()) + .sessionToken(sessionToken) + .build(); + command.enableRetrying(); + + return command.executeAsync( + restClient, + uploadProgressCallback, + null, + cancellationToken + ).onSuccess(new Continuation() { + @Override + public ParseFile.State then(Task task) throws Exception { + JSONObject result = task.getResult(); + ParseFile.State newState = new ParseFile.State.Builder(state) + .name(result.getString("name")) + .url(result.getString("url")) + .build(); + + // Write data to cache + try { + ParseFileUtils.copyFile(file, getCacheFile(newState)); + } catch (IOException e) { + // do nothing + } + + return newState; + } + }, ParseExecutors.io()); } public Task fetchAsync( diff --git a/Parse/src/main/java/com/parse/ParseRESTFileCommand.java b/Parse/src/main/java/com/parse/ParseRESTFileCommand.java index f52bdf676..f6d6a45c9 100644 --- a/Parse/src/main/java/com/parse/ParseRESTFileCommand.java +++ b/Parse/src/main/java/com/parse/ParseRESTFileCommand.java @@ -11,12 +11,16 @@ /** * REST network command for creating & uploading {@link ParseFile}s. */ + +import java.io.File; + /** package */ class ParseRESTFileCommand extends ParseRESTCommand { public static class Builder extends Init { private byte[] data = null; private String contentType = null; + private File file; public Builder() { // We only ever use ParseRESTFileCommand for file uploads, so default to POST. @@ -37,6 +41,11 @@ public Builder contentType(String contentType) { return this; } + public Builder file(File file) { + this.file = file; + return this; + } + @Override /* package */ Builder self() { return this; @@ -49,18 +58,28 @@ public ParseRESTFileCommand build() { private final byte[] data; private final String contentType; + private final File file; public ParseRESTFileCommand(Builder builder) { super(builder); + if (builder.file != null && builder.data != null) { + throw new IllegalArgumentException("File and data can not be set at the same time"); + } this.data = builder.data; this.contentType = builder.contentType; + this.file = builder.file; } @Override protected ParseHttpBody newBody(final ProgressCallback progressCallback) { + // TODO(mengyan): Delete ParseByteArrayHttpBody when we change input byte array to staged file + // in ParseFileController if (progressCallback == null) { - return new ParseByteArrayHttpBody(data, contentType); + return data != null ? + new ParseByteArrayHttpBody(data, contentType) : new ParseFileHttpBody(file, contentType); } - return new ParseCountingByteArrayHttpBody(data, contentType, progressCallback); + return data != null ? + new ParseCountingByteArrayHttpBody(data, contentType, progressCallback) : + new ParseCountingFileHttpBody(file, contentType, progressCallback); } } diff --git a/Parse/src/test/java/com/parse/ParseFileControllerTest.java b/Parse/src/test/java/com/parse/ParseFileControllerTest.java index 5f211f305..4222fda31 100644 --- a/Parse/src/test/java/com/parse/ParseFileControllerTest.java +++ b/Parse/src/test/java/com/parse/ParseFileControllerTest.java @@ -97,7 +97,7 @@ public void testSaveAsyncNotDirty() throws Exception { ParseFile.State state = new ParseFile.State.Builder() .url("http://example.com") .build(); - Task task = controller.saveAsync(state, null, null, null, null); + Task task = controller.saveAsync(state, (byte[])null, null, null, null); task.waitForCompletion(); verify(restClient, times(0)).execute(any(ParseHttpRequest.class)); @@ -113,7 +113,7 @@ public void testSaveAsyncAlreadyCancelled() throws Exception { ParseFile.State state = new ParseFile.State.Builder().build(); Task cancellationToken = Task.cancelled(); - Task task = controller.saveAsync(state, null, null, null, cancellationToken); + Task task = controller.saveAsync(state, (byte[])null, null, null, cancellationToken); task.waitForCompletion(); verify(restClient, times(0)).execute(any(ParseHttpRequest.class)); @@ -121,7 +121,7 @@ public void testSaveAsyncAlreadyCancelled() throws Exception { } @Test - public void testSaveAsyncSuccess() throws Exception { + public void testSaveAsyncSuccessWithByteArray() throws Exception { JSONObject json = new JSONObject(); json.put("name", "new_file_name"); json.put("url", "http://example.com"); @@ -155,7 +155,43 @@ public void testSaveAsyncSuccess() throws Exception { } @Test - public void testSaveAsyncFailure() throws Exception { + public void testSaveAsyncSuccessWithFile() throws Exception { + JSONObject json = new JSONObject(); + json.put("name", "new_file_name"); + json.put("url", "http://example.com"); + String content = json.toString(); + + ParseHttpResponse response = mock(ParseHttpResponse.class); + when(response.getStatusCode()).thenReturn(200); + when(response.getContent()).thenReturn(new ByteArrayInputStream(content.getBytes())); + when(response.getTotalSize()).thenReturn((long) content.length()); + + ParseHttpClient restClient = mock(ParseHttpClient.class); + when(restClient.execute(any(ParseHttpRequest.class))).thenReturn(response); + + File root = temporaryFolder.getRoot(); + ParseFileController controller = new ParseFileController(restClient, root); + + File file = new File(root, "test"); + ParseFileUtils.writeStringToFile(file, "content", "UTF-8"); + ParseFile.State state = new ParseFile.State.Builder() + .name("file_name") + .mimeType("mime_type") + .build(); + Task task = controller.saveAsync(state, file, null, null, null); + ParseFile.State result = ParseTaskUtils.wait(task); + + verify(restClient, times(1)).execute(any(ParseHttpRequest.class)); + assertEquals("new_file_name", result.name()); + assertEquals("http://example.com", result.url()); + File cachedFile = new File(root, "new_file_name"); + assertTrue(cachedFile.exists()); + assertTrue(file.exists()); + assertEquals("content", ParseFileUtils.readFileToString(cachedFile, "UTF-8")); + } + + @Test + public void testSaveAsyncFailureWithByteArray() throws Exception { // TODO(grantland): Remove once we no longer rely on retry logic. ParseRequest.setDefaultInitialRetryDelay(1L); @@ -180,6 +216,34 @@ public void testSaveAsyncFailure() throws Exception { assertEquals(0, root.listFiles().length); } + @Test + public void testSaveAsyncFailureWithFile() throws Exception { + // TODO(grantland): Remove once we no longer rely on retry logic. + ParseRequest.setDefaultInitialRetryDelay(1L); + + ParseHttpClient restClient = mock(ParseHttpClient.class); + when(restClient.execute(any(ParseHttpRequest.class))).thenThrow(new IOException()); + + File root = temporaryFolder.getRoot(); + ParseFileController controller = new ParseFileController(restClient, root); + + File file = temporaryFolder.newFile("test"); + ParseFile.State state = new ParseFile.State.Builder() + .build(); + Task task = controller.saveAsync(state, file, null, null, null); + task.waitForCompletion(); + + // TODO(grantland): Abstract out command runner so we don't have to account for retries. + verify(restClient, times(5)).execute(any(ParseHttpRequest.class)); + assertTrue(task.isFaulted()); + Exception error = task.getError(); + assertThat(error, instanceOf(ParseException.class)); + assertEquals(ParseException.CONNECTION_FAILED, ((ParseException) error).getCode()); + // Make sure the original file is not deleted and there is no cache file in the folder + assertEquals(1, root.listFiles().length); + assertTrue(file.exists()); + } + //endregion //region testFetchAsync