Skip to content

Commit

Permalink
feat: Add support for uploading a ParseFile from a URI (#1207)
Browse files Browse the repository at this point in the history
  • Loading branch information
hej2010 committed Feb 18, 2024
1 parent 697d213 commit 83aec68
Show file tree
Hide file tree
Showing 10 changed files with 510 additions and 6 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);
}
}
}
27 changes: 27 additions & 0 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 @@ -274,6 +291,16 @@ private Task<Void> saveAsync(
progressCallbackOnMainThread(
uploadProgressCallback),
cancellationToken);
} else if (uri != null) {
saveTask =
getFileController()
.saveAsync(
state,
uri,
sessionToken,
progressCallbackOnMainThread(
uploadProgressCallback),
cancellationToken);
} else {
saveTask =
getFileController()
Expand Down
44 changes: 44 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,7 @@
*/
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 +164,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
25 changes: 25 additions & 0 deletions parse/src/main/java/com/parse/ParseFileUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
*/
package com.parse;

import android.net.Uri;
import androidx.annotation.NonNull;
import java.io.File;
import java.io.FileInputStream;
Expand Down Expand Up @@ -115,6 +116,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
35 changes: 29 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,7 @@
*/
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 +19,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 +44,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 +92,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
96 changes: 96 additions & 0 deletions parse/src/main/java/com/parse/ParseUriHttpBody.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
/*
* 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 static com.parse.Parse.getApplicationContext;

import android.content.res.AssetFileDescriptor;
import android.database.Cursor;
import android.net.Uri;
import android.os.ParcelFileDescriptor;
import android.provider.OpenableColumns;
import com.parse.http.ParseHttpBody;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;

class ParseUriHttpBody extends ParseHttpBody {

/* package */ final Uri uri;

public ParseUriHttpBody(Uri uri) {
this(uri, null);
}

public ParseUriHttpBody(Uri uri, String contentType) {
super(contentType, getUriLength(uri));
this.uri = uri;
}

private static long getUriLength(Uri uri) {
long length = -1;

try (Cursor cursor =
getApplicationContext()
.getContentResolver()
.query(uri, null, null, null, null, null)) {
if (cursor != null && cursor.moveToFirst()) {
int sizeIndex = cursor.getColumnIndex(OpenableColumns.SIZE);
if (!cursor.isNull(sizeIndex)) {
length = cursor.getLong(sizeIndex);
}
}
}
if (length == -1) {
try {
ParcelFileDescriptor parcelFileDescriptor =
getApplicationContext().getContentResolver().openFileDescriptor(uri, "r");
if (parcelFileDescriptor != null) {
length = parcelFileDescriptor.getStatSize();
parcelFileDescriptor.close();
}
} catch (IOException ignored) {
}
}
if (length == -1) {
try {
AssetFileDescriptor assetFileDescriptor =
getApplicationContext()
.getContentResolver()
.openAssetFileDescriptor(uri, "r");
if (assetFileDescriptor != null) {
length = assetFileDescriptor.getLength();
assetFileDescriptor.close();
}
} catch (IOException ignored) {
}
}
return length;
}

@Override
public InputStream getContent() throws IOException {
return getApplicationContext().getContentResolver().openInputStream(uri);
}

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

final InputStream fileInput =
getApplicationContext().getContentResolver().openInputStream(uri);
try {
ParseIOUtils.copy(fileInput, out);
} finally {
ParseIOUtils.closeQuietly(fileInput);
}
}
}

0 comments on commit 83aec68

Please sign in to comment.