Merge "Add support for uploading binary content through the edit rest api"

This commit is contained in:
David Pursehouse
2020-04-06 12:46:06 +00:00
committed by Gerrit Code Review
4 changed files with 75 additions and 9 deletions

View File

@@ -2728,6 +2728,20 @@ Put content of a file to a change edit.
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.

View File

@@ -20,4 +20,5 @@ import com.google.gerrit.extensions.restapi.RawInput;
/** Content to be added to a file (new or existing) via change edit. */
public class FileContentInput {
@DefaultInput public RawInput content;
public String binary_content;
}

View File

@@ -20,6 +20,7 @@ import static java.nio.charset.StandardCharsets.UTF_8;
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.BinaryResult;
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 com.google.inject.Singleton;
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 @@ public class ChangeEdits implements ChildCollection<ChangeResource, ChangeEditRe
/** 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 class ChangeEdits implements ChildCollection<ChangeResource, ChangeEditRe
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());
}

View File

@@ -96,6 +96,15 @@ public class ChangeEditIT extends AbstractDaemonTest {
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 @@ public class ChangeEditIT extends AbstractDaemonTest {
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
@@ -559,12 +568,31 @@ public class ChangeEditIT extends AbstractDaemonTest {
ensureSameBytes(getFileContentOfEdit(changeId, FILE_NAME), CONTENT_NEW);
}
@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