Merge "Add support for uploading binary content through the edit rest api"
This commit is contained in:
@@ -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.
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user