Add support for uploading binary content through the edit rest api
Adding UI support will be done in a followup.
Change-Id: I3db1bc0503d83e58fadcc9642224fb8cec74ce92
diff --git a/Documentation/rest-api-changes.txt b/Documentation/rest-api-changes.txt
index f7019d6..677c246 100644
--- a/Documentation/rest-api-changes.txt
+++ b/Documentation/rest-api-changes.txt
@@ -2728,6 +2728,20 @@
PUT /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/edit/foo HTTP/1.0
----
+To upload a file as binary data in the request body:
+
+.Request
+----
+ PUT /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/edit/foo HTTP/1.0
+ Content-Type: application/json; charset=UTF-8
+
+ {
+ "binary_content": "data:text/plain;base64,SGVsbG8sIFdvcmxkIQ=="
+ }
+----
+
+Note that it must be base-64 encoded data uri.
+
When change edit doesn't exist for this change yet it is created. When file
content isn't provided, it is wiped out for that file. As response
"`204 No Content`" is returned.
diff --git a/java/com/google/gerrit/extensions/api/changes/FileContentInput.java b/java/com/google/gerrit/extensions/api/changes/FileContentInput.java
index 93c253d..0cfe908 100644
--- a/java/com/google/gerrit/extensions/api/changes/FileContentInput.java
+++ b/java/com/google/gerrit/extensions/api/changes/FileContentInput.java
@@ -20,4 +20,5 @@
/** Content to be added to a file (new or existing) via change edit. */
public class FileContentInput {
@DefaultInput public RawInput content;
+ public String binary_content;
}
diff --git a/java/com/google/gerrit/server/restapi/change/ChangeEdits.java b/java/com/google/gerrit/server/restapi/change/ChangeEdits.java
index 68e39e7..9a25f52 100644
--- a/java/com/google/gerrit/server/restapi/change/ChangeEdits.java
+++ b/java/com/google/gerrit/server/restapi/change/ChangeEdits.java
@@ -20,6 +20,7 @@
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
import com.google.common.io.ByteStreams;
+import com.google.gerrit.common.RawInputUtil;
import com.google.gerrit.entities.Change;
import com.google.gerrit.entities.Patch;
import com.google.gerrit.entities.PatchSet;
@@ -35,6 +36,7 @@
import com.google.gerrit.extensions.restapi.ChildCollection;
import com.google.gerrit.extensions.restapi.DefaultInput;
import com.google.gerrit.extensions.restapi.IdString;
+import com.google.gerrit.extensions.restapi.RawInput;
import com.google.gerrit.extensions.restapi.ResourceConflictException;
import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
import com.google.gerrit.extensions.restapi.Response;
@@ -66,9 +68,12 @@
import java.io.IOException;
import java.util.List;
import java.util.Optional;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.util.Base64;
import org.kohsuke.args4j.Option;
@Singleton
@@ -277,6 +282,10 @@
/** Put handler that is activated when PUT request is called on collection element. */
@Singleton
public static class Put implements RestModifyView<ChangeEditResource, FileContentInput> {
+ private static final Pattern BINARY_DATA_PATTERN =
+ Pattern.compile("data:([\\w/.-]*);([\\w]+),(.*)");
+ private static final String BASE64 = "base64";
+
private final ChangeEditModifier editModifier;
private final GitRepositoryManager repositoryManager;
private final EditMessage editMessage;
@@ -301,22 +310,36 @@
public Response<Object> apply(ChangeResource rsrc, String path, FileContentInput input)
throws AuthException, BadRequestException, ResourceConflictException, IOException,
PermissionBackendException {
- if (input.content == null) {
- throw new BadRequestException("new content required");
+
+ if (input.content == null && input.binary_content == null) {
+ throw new BadRequestException("either content or binary_content is required");
}
- if (Patch.COMMIT_MSG.equals(path)) {
+ RawInput newContent;
+ if (input.binary_content != null) {
+ Matcher m = BINARY_DATA_PATTERN.matcher(input.binary_content);
+ if (m.matches() && BASE64.equals(m.group(2))) {
+ newContent = RawInputUtil.create(Base64.decode(m.group(3)));
+ } else {
+ throw new BadRequestException("binary_content must be encoded as base64 data uri");
+ }
+ } else {
+ newContent = input.content;
+ }
+
+ if (Patch.COMMIT_MSG.equals(path) && input.binary_content == null) {
EditMessage.Input editCommitMessageInput = new EditMessage.Input();
editCommitMessageInput.message =
- new String(ByteStreams.toByteArray(input.content.getInputStream()), UTF_8);
+ new String(ByteStreams.toByteArray(newContent.getInputStream()), UTF_8);
return editMessage.apply(rsrc, editCommitMessageInput);
}
+
if (Strings.isNullOrEmpty(path) || path.charAt(0) == '/') {
throw new ResourceConflictException("Invalid path: " + path);
}
try (Repository repository = repositoryManager.openRepository(rsrc.getProject())) {
- editModifier.modifyFile(repository, rsrc.getNotes(), path, input.content);
+ editModifier.modifyFile(repository, rsrc.getNotes(), path, newContent);
} catch (InvalidChangeOperationException e) {
throw new ResourceConflictException(e.getMessage());
}
diff --git a/javatests/com/google/gerrit/acceptance/edit/ChangeEditIT.java b/javatests/com/google/gerrit/acceptance/edit/ChangeEditIT.java
index 64349a4..b717eb7 100644
--- a/javatests/com/google/gerrit/acceptance/edit/ChangeEditIT.java
+++ b/javatests/com/google/gerrit/acceptance/edit/ChangeEditIT.java
@@ -96,6 +96,15 @@
private static final byte[] CONTENT_NEW = "baz".getBytes(UTF_8);
private static final String CONTENT_NEW2_STR = "quxÄÜÖßµ";
private static final byte[] CONTENT_NEW2 = CONTENT_NEW2_STR.getBytes(UTF_8);
+ private static final String CONTENT_BINARY_ENCODED_NEW =
+ "data:text/plain;base64,SGVsbG8sIFdvcmxkIQ==";
+ private static final byte[] CONTENT_BINARY_DECODED_NEW = "Hello, World!".getBytes(UTF_8);
+ private static final String CONTENT_BINARY_ENCODED_NEW2 =
+ "data:text/plain;base64,VXBsb2FkaW5nIHRvIGFuIGVkaXQgd29ya2VkIQ==";
+ private static final byte[] CONTENT_BINARY_DECODED_NEW2 =
+ "Uploading to an edit worked!".getBytes(UTF_8);
+ private static final String CONTENT_BINARY_ENCODED_NEW3 =
+ "data:text/plain,VXBsb2FkaW5nIHRvIGFuIGVkaXQgd29ya2VkIQ==";
@Inject private ProjectOperations projectOperations;
@Inject private RequestScopeOperations requestScopeOperations;
@@ -316,7 +325,7 @@
assertThrows(
BadRequestException.class,
() -> gApi.changes().id(changeId).edit().modifyFile(Patch.COMMIT_MSG, (RawInput) null));
- assertThat(ex).hasMessageThat().isEqualTo("new content required");
+ assertThat(ex).hasMessageThat().isEqualTo("either content or binary_content is required");
}
@Test
@@ -560,11 +569,30 @@
}
@Test
+ public void createAndUploadBinaryInChangeEditOneRequestRest() throws Exception {
+ FileContentInput in = new FileContentInput();
+ in.binary_content = CONTENT_BINARY_ENCODED_NEW;
+ adminRestSession.put(urlEditFile(changeId, FILE_NAME), in).assertNoContent();
+ ensureSameBytes(getFileContentOfEdit(changeId, FILE_NAME), CONTENT_BINARY_DECODED_NEW);
+ in.binary_content = CONTENT_BINARY_ENCODED_NEW2;
+ adminRestSession.put(urlEditFile(changeId, FILE_NAME), in).assertNoContent();
+ ensureSameBytes(getFileContentOfEdit(changeId, FILE_NAME), CONTENT_BINARY_DECODED_NEW2);
+ }
+
+ @Test
+ public void invalidBase64UploadBinaryInChangeEditOneRequestRest() throws Exception {
+ FileContentInput in = new FileContentInput();
+ in.binary_content = CONTENT_BINARY_ENCODED_NEW3;
+ adminRestSession.put(urlEditFile(changeId, FILE_NAME), in).assertBadRequest();
+ }
+
+ @Test
public void changeEditNoContentProvidedRest() throws Exception {
createEmptyEditFor(changeId);
- adminRestSession
- .put(urlEditFile(changeId, FILE_NAME), new FileContentInput())
- .assertBadRequest();
+
+ FileContentInput in = new FileContentInput();
+ in.binary_content = null;
+ adminRestSession.put(urlEditFile(changeId, FILE_NAME), in).assertBadRequest();
}
@Test