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 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 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 content isn't provided, it is wiped out for that file. As response
"`204 No Content`" is returned. "`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. */ /** Content to be added to a file (new or existing) via change edit. */
public class FileContentInput { public class FileContentInput {
@DefaultInput public RawInput content; @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.base.Strings;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
import com.google.common.io.ByteStreams; import com.google.common.io.ByteStreams;
import com.google.gerrit.common.RawInputUtil;
import com.google.gerrit.entities.Change; import com.google.gerrit.entities.Change;
import com.google.gerrit.entities.Patch; import com.google.gerrit.entities.Patch;
import com.google.gerrit.entities.PatchSet; 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.ChildCollection;
import com.google.gerrit.extensions.restapi.DefaultInput; import com.google.gerrit.extensions.restapi.DefaultInput;
import com.google.gerrit.extensions.restapi.IdString; 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.ResourceConflictException;
import com.google.gerrit.extensions.restapi.ResourceNotFoundException; import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
import com.google.gerrit.extensions.restapi.Response; import com.google.gerrit.extensions.restapi.Response;
@@ -66,9 +68,12 @@ import com.google.inject.Singleton;
import java.io.IOException; import java.io.IOException;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevWalk; import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.util.Base64;
import org.kohsuke.args4j.Option; import org.kohsuke.args4j.Option;
@Singleton @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. */ /** Put handler that is activated when PUT request is called on collection element. */
@Singleton @Singleton
public static class Put implements RestModifyView<ChangeEditResource, FileContentInput> { 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 ChangeEditModifier editModifier;
private final GitRepositoryManager repositoryManager; private final GitRepositoryManager repositoryManager;
private final EditMessage editMessage; 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) public Response<Object> apply(ChangeResource rsrc, String path, FileContentInput input)
throws AuthException, BadRequestException, ResourceConflictException, IOException, throws AuthException, BadRequestException, ResourceConflictException, IOException,
PermissionBackendException { 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(); EditMessage.Input editCommitMessageInput = new EditMessage.Input();
editCommitMessageInput.message = editCommitMessageInput.message =
new String(ByteStreams.toByteArray(input.content.getInputStream()), UTF_8); new String(ByteStreams.toByteArray(newContent.getInputStream()), UTF_8);
return editMessage.apply(rsrc, editCommitMessageInput); return editMessage.apply(rsrc, editCommitMessageInput);
} }
if (Strings.isNullOrEmpty(path) || path.charAt(0) == '/') { if (Strings.isNullOrEmpty(path) || path.charAt(0) == '/') {
throw new ResourceConflictException("Invalid path: " + path); throw new ResourceConflictException("Invalid path: " + path);
} }
try (Repository repository = repositoryManager.openRepository(rsrc.getProject())) { 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) { } catch (InvalidChangeOperationException e) {
throw new ResourceConflictException(e.getMessage()); 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 byte[] CONTENT_NEW = "baz".getBytes(UTF_8);
private static final String CONTENT_NEW2_STR = "quxÄÜÖßµ"; private static final String CONTENT_NEW2_STR = "quxÄÜÖßµ";
private static final byte[] CONTENT_NEW2 = CONTENT_NEW2_STR.getBytes(UTF_8); 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 ProjectOperations projectOperations;
@Inject private RequestScopeOperations requestScopeOperations; @Inject private RequestScopeOperations requestScopeOperations;
@@ -316,7 +325,7 @@ public class ChangeEditIT extends AbstractDaemonTest {
assertThrows( assertThrows(
BadRequestException.class, BadRequestException.class,
() -> gApi.changes().id(changeId).edit().modifyFile(Patch.COMMIT_MSG, (RawInput) null)); () -> 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 @Test
@@ -559,12 +568,31 @@ public class ChangeEditIT extends AbstractDaemonTest {
ensureSameBytes(getFileContentOfEdit(changeId, FILE_NAME), CONTENT_NEW); 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 @Test
public void changeEditNoContentProvidedRest() throws Exception { public void changeEditNoContentProvidedRest() throws Exception {
createEmptyEditFor(changeId); createEmptyEditFor(changeId);
adminRestSession
.put(urlEditFile(changeId, FILE_NAME), new FileContentInput()) FileContentInput in = new FileContentInput();
.assertBadRequest(); in.binary_content = null;
adminRestSession.put(urlEditFile(changeId, FILE_NAME), in).assertBadRequest();
} }
@Test @Test