Repository: square/retrofit
Issue: Explicit usage of File objects in @Multipart methods with @Part and @PartMap

I want to use File object explicitly in Retrofit interfaces without manually creating `MultipartBody.Part`.  It should be available in single parameters with `@Part` and `@PartMap` annotations.

Example :

```
public interface FileService {
    @Multipart
    @POST("/upload")
    Call<ResponseBody> sendFile(@Part("file") File fileToSend);
}
```

I will provide a pull request with tests and implementation.


Issue comments:
This can be accomplished with:
```java
public final class FileBodyConverter implements Converter<File, RequestBody> {
  public static final Converter.Factory FACTORY = new Factory() {
    @Override public Converter<?, RequestBody> requestBodyConverter(Type type,
        Annotation[] parameterAnnotations, Annotation[] methodAnnotations, Retrofit retrofit) {
      return File.class.equals(type) ? new FileBodyConverter() : null;
    }
  }
  
  @Override public RequestBody convert(File value) throws IOException {
    return RequestBody.create(null, value);
  }
}
```
and since it's unclear what content-type to use for a `File` I don't want to ship a general version of this.

Pull request review comments:

Patch that resolved the issue:
diff --git a/retrofit/src/main/java/retrofit2/ParameterHandler.java b/retrofit/src/main/java/retrofit2/ParameterHandler.java
index c332fad501..fbdde7380c 100644
--- a/retrofit/src/main/java/retrofit2/ParameterHandler.java
+++ b/retrofit/src/main/java/retrofit2/ParameterHandler.java
@@ -15,6 +15,7 @@
  */
 package retrofit2;
 
+import java.io.File;
 import java.io.IOException;
 import java.lang.reflect.Array;
 import java.util.Map;
@@ -247,15 +248,27 @@ private RawPart() {
     }
   }
 
-  static final class PartMap<T> extends ParameterHandler<Map<String, T>> {
-    private final Converter<T, RequestBody> valueConverter;
-    private final String transferEncoding;
+  static final class FilePart extends ParameterHandler<File> {
+    private final String name;
 
-    PartMap(Converter<T, RequestBody> valueConverter, String transferEncoding) {
-      this.valueConverter = valueConverter;
-      this.transferEncoding = transferEncoding;
+    FilePart(String name) {
+      this.name = name;
+    }
+
+    @Override void apply(RequestBuilder builder, File value) throws IOException {
+      apply(builder, name, value);
     }
 
+    static void apply(RequestBuilder builder, String name, File value) {
+      if (value != null) { // Skip null values.
+        builder.addPart(MultipartBody.Part.createFormData(name,
+                value.getName(),
+                RequestBody.create(null, value)));
+      }
+    }
+  }
+
+  abstract static class BasePartMap<T> extends ParameterHandler<Map<String, T>> {
     @Override void apply(RequestBuilder builder, Map<String, T> value) throws IOException {
       if (value == null) {
         throw new IllegalArgumentException("Part map was null.");
@@ -271,13 +284,36 @@ static final class PartMap<T> extends ParameterHandler<Map<String, T>> {
           throw new IllegalArgumentException(
               "Part map contained null value for key '" + entryKey + "'.");
         }
+        applyEntry(builder, entryKey, entryValue);
+      }
+    }
 
-        Headers headers = Headers.of(
-            "Content-Disposition", "form-data; name=\"" + entryKey + "\"",
-            "Content-Transfer-Encoding", transferEncoding);
+    abstract void applyEntry(RequestBuilder builder, String key, T value) throws IOException;
+  }
 
-        builder.addPart(headers, valueConverter.convert(entryValue));
-      }
+  static final class FilePartMap extends BasePartMap<File> {
+    static final FilePartMap INSTANCE = new FilePartMap();
+
+    @Override void applyEntry(RequestBuilder builder, String key, File value) throws IOException {
+      FilePart.apply(builder, key, value);
+    }
+  }
+
+  static final class PartMap<T> extends BasePartMap<T> {
+    private final Converter<T, RequestBody> valueConverter;
+    private final String transferEncoding;
+
+    PartMap(Converter<T, RequestBody> valueConverter, String transferEncoding) {
+      this.valueConverter = valueConverter;
+      this.transferEncoding = transferEncoding;
+    }
+
+    @Override void applyEntry(RequestBuilder builder, String key, T value) throws IOException {
+      Headers headers = Headers.of(
+          "Content-Disposition", "form-data; name=\"" + key + "\"",
+          "Content-Transfer-Encoding", transferEncoding);
+
+      builder.addPart(headers, valueConverter.convert(value));
     }
   }
 
diff --git a/retrofit/src/main/java/retrofit2/ServiceMethod.java b/retrofit/src/main/java/retrofit2/ServiceMethod.java
index 3a4325a7ef..8e82ae1e2a 100644
--- a/retrofit/src/main/java/retrofit2/ServiceMethod.java
+++ b/retrofit/src/main/java/retrofit2/ServiceMethod.java
@@ -15,6 +15,7 @@
  */
 package retrofit2;
 
+import java.io.File;
 import java.io.IOException;
 import java.lang.annotation.Annotation;
 import java.lang.reflect.Method;
@@ -589,6 +590,8 @@ private ParameterHandler<?> parseParameterAnnotation(
             return ParameterHandler.RawPart.INSTANCE.array();
           } else if (MultipartBody.Part.class.isAssignableFrom(rawParameterType)) {
             return ParameterHandler.RawPart.INSTANCE;
+          } else if (File.class.isAssignableFrom(rawParameterType)) {
+            throw parameterError(p, "File parts must have defined name.");
           } else {
             throw parameterError(p,
                 "@Part annotation must supply a name or use MultipartBody.Part parameter type.");
@@ -626,6 +629,8 @@ private ParameterHandler<?> parseParameterAnnotation(
           } else if (MultipartBody.Part.class.isAssignableFrom(rawParameterType)) {
             throw parameterError(p, "@Part parameters using the MultipartBody.Part must not "
                 + "include a part name in the annotation.");
+          } else if (File.class.isAssignableFrom(rawParameterType)) {
+            return new ParameterHandler.FilePart(partName);
           } else {
             Converter<?, RequestBody> converter =
                 retrofit.requestBodyConverter(type, annotations, methodAnnotations);
@@ -658,6 +663,9 @@ private ParameterHandler<?> parseParameterAnnotation(
           throw parameterError(p, "@PartMap values cannot be MultipartBody.Part. "
               + "Use @Part List<Part> or a different value type instead.");
         }
+        if (File.class.isAssignableFrom(Utils.getRawType(valueType))) {
+          return ParameterHandler.FilePartMap.INSTANCE;
+        }
 
         Converter<?, RequestBody> valueConverter =
             retrofit.requestBodyConverter(valueType, annotations, methodAnnotations);


Test patch:
diff --git a/retrofit/src/test/java/retrofit2/RequestBuilderTest.java b/retrofit/src/test/java/retrofit2/RequestBuilderTest.java
index 0f3b5ebe66..6a1a9ee032 100644
--- a/retrofit/src/test/java/retrofit2/RequestBuilderTest.java
+++ b/retrofit/src/test/java/retrofit2/RequestBuilderTest.java
@@ -15,6 +15,7 @@
  */
 package retrofit2;
 
+import java.io.File;
 import java.io.IOException;
 import java.lang.reflect.Method;
 import java.math.BigInteger;
@@ -2071,6 +2072,82 @@ Call<ResponseBody> method(@Part("ping") RequestBody ping) {
     }
   }
 
+  @Test public void multipartWithFileContents() throws IOException {
+    class Example {
+      @Multipart
+      @POST("/foo/bar/")
+      Call<ResponseBody> method(@Part("filepart") File content) { return null; }
+    }
+
+    File multipartFile = File.createTempFile("multipartFileTest", ".txt");
+
+    Request request = buildRequest(Example.class, multipartFile);
+    RequestBody body = request.body();
+    Buffer buffer = new Buffer();
+    body.writeTo(buffer);
+    String bodyString = buffer.readUtf8();
+
+    assertThat(bodyString)
+            .contains("name=\"filepart\"; filename=\"" + multipartFile.getName() + "\"");
+  }
+
+  @Test public void multipartMapWithFiles() throws IOException {
+    class Example {
+      @Multipart
+      @POST("/foo/bar")
+      Call<ResponseBody> method(@PartMap Map<String, File> files) { return null; }
+    }
+
+    Map<String, File> fileMap = new HashMap<>(3);
+    fileMap.put("file[0]", File.createTempFile("multipartFileTest", ".txt"));
+    fileMap.put("file[1]", File.createTempFile("multipartFileTest", ".txt"));
+    fileMap.put("file[2]", File.createTempFile("multipartFileTest", ".txt"));
+
+    Request request = buildRequest(Example.class, fileMap);
+    RequestBody body = request.body();
+    Buffer buffer = new Buffer();
+    body.writeTo(buffer);
+    String bodyString = buffer.readUtf8();
+
+    assertThat(bodyString)
+        .contains("name=\"file[0]\"; filename=\"" + fileMap.get("file[0]").getName() + "\"")
+        .contains("name=\"file[1]\"; filename=\"" + fileMap.get("file[1]").getName() + "\"")
+        .contains("name=\"file[2]\"; filename=\"" + fileMap.get("file[2]").getName() + "\"");
+  }
+
+  @Test public void multipartFileContentsNullValue() throws IOException {
+    class Example {
+      @Multipart
+      @POST("/foo/bar/")
+      Call<ResponseBody> method(@Part("text") String text, @Part("filepart") File content) { return null; }
+    }
+
+    Request request = buildRequest(Example.class, "param", (File) null);
+    RequestBody body = request.body();
+    Buffer buffer = new Buffer();
+    body.writeTo(buffer);
+    String bodyString = buffer.readUtf8();
+
+    assertThat(bodyString)
+        .contains("name=\"text\"")
+        .doesNotContain("name=\"filepart\"");
+  }
+
+  @Test public void multipartFileContentsWithoutPartName() {
+    class Example {
+      @Multipart
+      @POST("/foo/bar/")
+      Call<ResponseBody> method(@Part File content) { return null; }
+    }
+
+    try {
+      buildRequest(Example.class, null);
+      fail();
+    } catch (IllegalArgumentException e) {
+      assertThat(e.getMessage()).isEqualTo("File parts must have defined name. (parameter #1)\n    for method Example.method");
+    }
+  }
+
   @Test public void simpleFormEncoded() {
     class Example {
       @FormUrlEncoded //