diff --git a/Documentation/rest-api-changes.txt b/Documentation/rest-api-changes.txt index fc304ba9f6..bf817798aa 100644 --- a/Documentation/rest-api-changes.txt +++ b/Documentation/rest-api-changes.txt @@ -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. diff --git a/java/com/google/gerrit/extensions/api/changes/FileContentInput.java b/java/com/google/gerrit/extensions/api/changes/FileContentInput.java index 93c253d11b..0cfe908cbf 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 @@ 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; } diff --git a/java/com/google/gerrit/server/restapi/change/ChangeEdits.java b/java/com/google/gerrit/server/restapi/change/ChangeEdits.java index 68e39e71c3..9a25f520f6 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 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 { + 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 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 64349a427a..b717eb757c 100644 --- a/javatests/com/google/gerrit/acceptance/edit/ChangeEditIT.java +++ b/javatests/com/google/gerrit/acceptance/edit/ChangeEditIT.java @@ -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