Skip to content

Commit

Permalink
Upload files using a content uri
Browse files Browse the repository at this point in the history
  • Loading branch information
hej2010 committed Feb 15, 2024
1 parent 697d213 commit 2c79d42
Show file tree
Hide file tree
Showing 10 changed files with 526 additions and 30 deletions.
59 changes: 59 additions & 0 deletions parse/src/main/java/com/parse/ParseCountingUriHttpBody.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/*
* Copyright (c) 2015-present, Parse, LLC.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.parse;

import android.net.Uri;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;

class ParseCountingUriHttpBody extends ParseUriHttpBody {

private static final int DEFAULT_CHUNK_SIZE = 4096;
private static final int EOF = -1;

private final ProgressCallback progressCallback;

public ParseCountingUriHttpBody(Uri uri, ProgressCallback progressCallback) {
this(uri, null, progressCallback);
}

public ParseCountingUriHttpBody(
Uri uri, String contentType, ProgressCallback progressCallback) {
super(uri, contentType);
this.progressCallback = progressCallback;
}

@Override
public void writeTo(OutputStream output) throws IOException {
if (output == null) {
throw new IllegalArgumentException("Output stream may not be null");
}

final InputStream fileInput = Parse.getApplicationContext().getContentResolver().openInputStream(uri);
try {
byte[] buffer = new byte[DEFAULT_CHUNK_SIZE];
int n;
long totalLength = getContentLength();
long position = 0;
while (EOF != (n = fileInput.read(buffer))) {
output.write(buffer, 0, n);
position += n;

if (progressCallback != null) {
int progress = (int) (100 * position / totalLength);
progressCallback.done(progress);
}
}
} finally {
ParseIOUtils.closeQuietly(fileInput);
}
}
}
71 changes: 49 additions & 22 deletions parse/src/main/java/com/parse/ParseFile.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
*/
package com.parse;

import android.net.Uri;
import android.os.Parcel;
import android.os.Parcelable;
import com.parse.boltsinternal.Continuation;
Expand Down Expand Up @@ -64,6 +65,7 @@ public ParseFile[] newArray(int size) {
*/
/* package for tests */ byte[] data;
/* package for tests */ File file;
/* package for tests */ Uri uri;
private State state;

/**
Expand Down Expand Up @@ -102,6 +104,21 @@ public ParseFile(String name, byte[] data, String contentType) {
this.data = data;
}

/**
* Creates a new file from a content uri, file name, and content type. Content type will be used
* instead of auto-detection by file extension.
*
* @param name The file's name, ideally with extension. The file name must begin with an
* alphanumeric character, and consist of alphanumeric characters, periods, spaces,
* underscores, or dashes.
* @param uri The file uri.
* @param contentType The file's content type.
*/
public ParseFile(String name, Uri uri, String contentType) {
this(new State.Builder().name(name).mimeType(contentType).build());
this.uri = uri;
}

/**
* Creates a new file from a byte array.
*
Expand Down Expand Up @@ -263,28 +280,38 @@ private Task<Void> saveAsync(
return Task.cancelled();
}

Task<State> saveTask;
if (data != null) {
saveTask =
getFileController()
.saveAsync(
state,
data,
sessionToken,
progressCallbackOnMainThread(
uploadProgressCallback),
cancellationToken);
} else {
saveTask =
getFileController()
.saveAsync(
state,
file,
sessionToken,
progressCallbackOnMainThread(
uploadProgressCallback),
cancellationToken);
}
Task<State> saveTask;
if (data != null) {
saveTask =
getFileController()
.saveAsync(
state,
data,
sessionToken,
progressCallbackOnMainThread(
uploadProgressCallback),
cancellationToken);
} else if (uri != null) {
saveTask =
getFileController()
.saveAsync(
state,
uri,
sessionToken,
progressCallbackOnMainThread(
uploadProgressCallback),
cancellationToken);
} else {
saveTask =
getFileController()
.saveAsync(
state,
file,
sessionToken,
progressCallbackOnMainThread(
uploadProgressCallback),
cancellationToken);
}

return saveTask.onSuccessTask(
task1 -> {
Expand Down
45 changes: 45 additions & 0 deletions parse/src/main/java/com/parse/ParseFileController.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
*/
package com.parse;

import android.net.Uri;

import com.parse.boltsinternal.Task;
import com.parse.http.ParseHttpRequest;
import java.io.File;
Expand Down Expand Up @@ -163,6 +165,49 @@ public Task<ParseFile.State> saveAsync(
ParseExecutors.io());
}

public Task<ParseFile.State> saveAsync(
final ParseFile.State state,
final Uri uri,
String sessionToken,
ProgressCallback uploadProgressCallback,
Task<Void> 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())
.uri(uri)
.contentType(state.mimeType())
.sessionToken(sessionToken)
.build();

return command.executeAsync(restClient, uploadProgressCallback, null, cancellationToken)
.onSuccess(
task -> {
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.writeUriToFile(getCacheFile(newState), uri);
} catch (IOException e) {
// do nothing
}

return newState;
},
ParseExecutors.io());
}

public Task<File> fetchAsync(
final ParseFile.State state,
@SuppressWarnings("UnusedParameters") String sessionToken,
Expand Down
32 changes: 30 additions & 2 deletions parse/src/main/java/com/parse/ParseFileUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,13 @@
*/
package com.parse;

import android.net.Uri;

import androidx.annotation.NonNull;

import org.json.JSONException;
import org.json.JSONObject;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
Expand All @@ -27,8 +33,6 @@
import java.nio.channels.FileChannel;
import java.nio.charset.Charset;
import java.util.List;
import org.json.JSONException;
import org.json.JSONObject;

/** General file manipulation utilities. */
public class ParseFileUtils {
Expand Down Expand Up @@ -115,6 +119,30 @@ public static void writeByteArrayToFile(File file, byte[] data) throws IOExcepti
}
}

/**
* Writes a content uri to a file creating the file if it does not exist.
*
* <p>NOTE: As from v1.3, the parent directories of the file will be created if they do not
* exist.
*
* @param file the file to write to
* @param uri the content uri with data to write to the file
* @throws IOException in case of an I/O error
* @since Commons IO 1.1
*/
public static void writeUriToFile(File file, Uri uri) throws IOException {
OutputStream out = null;
InputStream in = null;
try {
in = Parse.getApplicationContext().getContentResolver().openInputStream(uri);
out = openOutputStream(file);
ParseIOUtils.copyLarge(in, out);
} finally {
ParseIOUtils.closeQuietly(out);
ParseIOUtils.closeQuietly(in);
}
}

// -----------------------------------------------------------------------

/**
Expand Down
36 changes: 30 additions & 6 deletions parse/src/main/java/com/parse/ParseRESTFileCommand.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
*/
package com.parse;

import android.net.Uri;

import com.parse.http.ParseHttpBody;
import com.parse.http.ParseHttpRequest;
import java.io.File;
Expand All @@ -18,15 +20,23 @@ class ParseRESTFileCommand extends ParseRESTCommand {
private final byte[] data;
private final String contentType;
private final File file;
private final Uri uri;

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");
}
if (builder.uri != null && builder.data != null) {
throw new IllegalArgumentException("URI and data can not be set at the same time");
}
if (builder.file != null && builder.uri != null) {
throw new IllegalArgumentException("File and URI can not be set at the same time");
}
this.data = builder.data;
this.contentType = builder.contentType;
this.file = builder.file;
this.uri = builder.uri;
}

@Override
Expand All @@ -35,20 +45,29 @@ protected ParseHttpBody newBody(final ProgressCallback progressCallback) {
// file
// in ParseFileController
if (progressCallback == null) {
return data != null
? new ParseByteArrayHttpBody(data, contentType)
: new ParseFileHttpBody(file, contentType);
if (data != null) {
return new ParseByteArrayHttpBody(data, contentType);
} else if (uri != null) {
return new ParseUriHttpBody(uri, contentType);
} else {
return new ParseFileHttpBody(file, contentType);
}
}
if (data != null) {
return new ParseCountingByteArrayHttpBody(data, contentType, progressCallback);
} else if (uri != null) {
return new ParseCountingUriHttpBody(uri, contentType, progressCallback);
} else {
return new ParseCountingFileHttpBody(file, contentType, progressCallback);
}
return data != null
? new ParseCountingByteArrayHttpBody(data, contentType, progressCallback)
: new ParseCountingFileHttpBody(file, contentType, progressCallback);
}

public static class Builder extends Init<Builder> {

private byte[] data = null;
private String contentType = null;
private File file;
private Uri uri;

public Builder() {
// We only ever use ParseRESTFileCommand for file uploads, so default to POST.
Expand All @@ -74,6 +93,11 @@ public Builder file(File file) {
return this;
}

public Builder uri(Uri uri) {
this.uri = uri;
return this;
}

@Override
/* package */ Builder self() {
return this;
Expand Down
Loading

0 comments on commit 2c79d42

Please sign in to comment.