Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce @PartFile for including TypedFile parts in a multi-part request #1188

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions retrofit/src/main/java/retrofit2/RequestAction.java
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,26 @@ static final class PartMap<T> extends RequestAction<Map<String, T>> {
}
}

static final class PartFile extends RequestAction {
private final String partName;

PartFile(String partName) {
this.partName = partName;
}

@Override void perform(RequestBuilder builder, Object value) {
if (value == null) return; // Skip null values.

TypedFile typedFile = (TypedFile) value;
Headers headers = Headers.of(
"Content-Disposition", "form-data; name=\"" + partName + "\"; filename=\""
+ typedFile.fileName() + "\"",
"Content-Transfer-Encoding", "binary");
RequestBody body = RequestBody.create(typedFile.mediaType(), typedFile.file());
builder.addPart(headers, body);
}
}

static final class Body<T> extends RequestAction<T> {
private final Converter<T, RequestBody> converter;

Expand Down
13 changes: 13 additions & 0 deletions retrofit/src/main/java/retrofit2/RequestFactoryParser.java
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
import retrofit2.http.POST;
import retrofit2.http.PUT;
import retrofit2.http.Part;
import retrofit2.http.PartFile;
import retrofit2.http.PartMap;
import retrofit2.http.Path;
import retrofit2.http.Query;
Expand Down Expand Up @@ -456,6 +457,18 @@ private void parseParameters(Retrofit retrofit) {
action = new RequestAction.PartMap<>(valueConverter, partMap.encoding());
gotPart = true;

} else if (methodParameterAnnotation instanceof PartFile) {
if (!isMultipart) {
throw parameterError(i,
"@PartFile parameters can only be used with multipart encoding.");
}
if (methodParameterType != TypedFile.class) {
throw parameterError(i, "@PartFile parameter type must be TypedFile.");
}
PartFile partFile = (PartFile) methodParameterAnnotation;
action = new RequestAction.PartFile(partFile.value());
gotPart = true;

} else if (methodParameterAnnotation instanceof Body) {
if (isFormEncoded || isMultipart) {
throw parameterError(i,
Expand Down
69 changes: 69 additions & 0 deletions retrofit/src/main/java/retrofit2/TypedFile.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/*
* Copyright (C) 2015 Square, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package retrofit2;

import com.squareup.okhttp.MediaType;
import java.io.File;

import static retrofit2.Utils.checkNotNull;

/**
* Wrapper for a File, its MediaType, and optionally alternative filename.
*/
public final class TypedFile {
private final MediaType mediaType;
private final File file;
private final String fileName;

public TypedFile(MediaType mediaType, File file) {
this(mediaType, file, file != null ? file.getName() : null);
}

public TypedFile(MediaType mediaType, File file, String fileName) {
this.mediaType = checkNotNull(mediaType, "MediaType cannot be null.");
this.file = checkNotNull(file, "File cannot be null.");
this.fileName = checkNotNull(fileName, "Filename cannot be null.");
}

public MediaType mediaType() {
return mediaType;
}

public File file() {
return file;
}

public String fileName() {
return fileName;
}

@Override public String toString() {
return file.getAbsolutePath() + " (" + mediaType() + ")";
}

@Override public boolean equals(Object o) {
if (this == o) return true;
if (o instanceof TypedFile) {
TypedFile rhs = (TypedFile) o;
return file.equals(rhs.file);
}
return false;
}

@Override public int hashCode() {
return file.hashCode();
}
}
51 changes: 51 additions & 0 deletions retrofit/src/main/java/retrofit2/http/PartFile.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/*
* Copyright (C) 2015 Square, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package retrofit2.http;

import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import static java.lang.annotation.ElementType.PARAMETER;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

/**
* Denotes a single {@link retrofit.TypedFile} part of a multi-part request.
* <p>
* Values may be {@code null} which will omit them from the request body.
* <p>
* The {@code Content-Transfer-Encoding} will be {@code binary} and the {@code filename} parameter
* value will be read from the {@code File} field by default. If you need finer control, consider
* using {@link PartMap} instead.
* <p>
* <pre>
* &#64;Multipart
* &#64;POST("/")
* Call&lt;ResponseBody&gt; upload(
* &#64;PartFile("image") TypedFile imageFile);
* </pre>
* <p>
* PartFile parameters may not be {@code null}.
*
* @see Multipart
* @see PartMap
*/
@Documented
@Target(PARAMETER)
@Retention(RUNTIME)
public @interface PartFile {
String value();
}
71 changes: 71 additions & 0 deletions retrofit/src/test/java/retrofit2/RequestBuilderTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import com.squareup.okhttp.RequestBody;
import com.squareup.okhttp.Response;
import com.squareup.okhttp.ResponseBody;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Method;
import java.math.BigInteger;
Expand All @@ -19,7 +20,9 @@
import java.util.concurrent.atomic.AtomicReference;
import okio.Buffer;
import org.junit.Ignore;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import retrofit2.http.Body;
import retrofit2.http.DELETE;
import retrofit2.http.Field;
Expand All @@ -36,6 +39,7 @@
import retrofit2.http.POST;
import retrofit2.http.PUT;
import retrofit2.http.Part;
import retrofit2.http.PartFile;
import retrofit2.http.PartMap;
import retrofit2.http.Path;
import retrofit2.http.Query;
Expand All @@ -50,6 +54,8 @@
public final class RequestBuilderTest {
private static final MediaType TEXT_PLAIN = MediaType.parse("text/plain");

@Rule public final TemporaryFolder tempFolder = new TemporaryFolder();

@Test public void customMethodNoBody() {
class Example {
@HTTP(method = "CUSTOM1", path = "/foo")
Expand Down Expand Up @@ -1433,6 +1439,71 @@ Call<ResponseBody> method(@PartMap List<Object> parts) {
}
}

@Test public void multipartPartFile() throws IOException {
class Example {
@Multipart
@POST("/foo/bar/") //
Call<ResponseBody> method(@PartFile("image") TypedFile typedFile) {
return null;
}
}
MediaType mediaType = MediaType.parse("image/png");
File file = tempFolder.newFile("file.png");
TypedFile typedFile = new TypedFile(mediaType, file);

Request request = buildRequest(Example.class, typedFile);
assertThat(request.method()).isEqualTo("POST");
assertThat(request.headers().size()).isZero();
assertThat(request.urlString()).isEqualTo("http://example.com/foo/bar/");

RequestBody body = request.body();
Buffer buffer = new Buffer();
body.writeTo(buffer);
String bodyString = buffer.readUtf8();

assertThat(bodyString)
.contains("Content-Disposition: form-data;")
.contains("name=\"image\";")
.contains("filename=\"file.png\"");
assertThat(bodyString)
.contains("Content-Type: image/png");
assertThat(bodyString)
.contains("Content-Transfer-Encoding: binary");
}

@Test public void multipartPartFileWithFileName() throws IOException {
class Example {
@Multipart
@POST("/foo/bar/") //
Call<ResponseBody> method(@PartFile("image") TypedFile typedFile) {
return null;
}
}
MediaType mediaType = MediaType.parse("image/png");
File file = tempFolder.newFile("file.png");
String fileName = "profile.png";
TypedFile typedFile = new TypedFile(mediaType, file, fileName);

Request request = buildRequest(Example.class, typedFile);
assertThat(request.method()).isEqualTo("POST");
assertThat(request.headers().size()).isZero();
assertThat(request.urlString()).isEqualTo("http://example.com/foo/bar/");

RequestBody body = request.body();
Buffer buffer = new Buffer();
body.writeTo(buffer);
String bodyString = buffer.readUtf8();

assertThat(bodyString)
.contains("Content-Disposition: form-data;")
.contains("name=\"image\";")
.contains("filename=\"profile.png\"");
assertThat(bodyString)
.contains("Content-Type: image/png");
assertThat(bodyString)
.contains("Content-Transfer-Encoding: binary");
}

@Test public void multipartNullRemovesPart() throws IOException {
class Example {
@Multipart //
Expand Down